Import pentobi_16.2.orig.tar.xz
authorJuhani Numminen <juhaninumminen0@gmail.com>
Thu, 17 Jan 2019 14:42:18 +0000 (14:42 +0000)
committerJuhani Numminen <juhaninumminen0@gmail.com>
Thu, 17 Jan 2019 14:42:18 +0000 (14:42 +0000)
[dgit import orig pentobi_16.2.orig.tar.xz]

508 files changed:
AUTHORS [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
COPYING [new file with mode: 0644]
CTestCustom.cmake [new file with mode: 0644]
INSTALL [new file with mode: 0644]
NEWS [new file with mode: 0644]
README [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/io.sourceforge.pentobi.appdata.xml.in [new file with mode: 0644]
data/io.sourceforge.pentobi.desktop.in [new file with mode: 0755]
data/io.sourceforge.pentobi.kde_thumbnailer.metainfo.xml.in [new file with mode: 0644]
data/pentobi-mime.xml [new file with mode: 0644]
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/gtp/Pentobi-GTP.html [new file with mode: 0644]
doc/help.qrc [new file with mode: 0644]
doc/help/C/pentobi/analysis.jpg [new file with mode: 0644]
doc/help/C/pentobi/become_stronger.html [new file with mode: 0644]
doc/help/C/pentobi/board_callisto.png [new file with mode: 0644]
doc/help/C/pentobi/board_classic.png [new file with mode: 0644]
doc/help/C/pentobi/board_duo.png [new file with mode: 0644]
doc/help/C/pentobi/board_gembloq.png [new file with mode: 0644]
doc/help/C/pentobi/board_nexos.png [new file with mode: 0644]
doc/help/C/pentobi/board_trigon.jpg [new file with mode: 0644]
doc/help/C/pentobi/callisto_rules.html [new file with mode: 0644]
doc/help/C/pentobi/classic_rules.html [new file with mode: 0644]
doc/help/C/pentobi/duo_rules.html [new file with mode: 0644]
doc/help/C/pentobi/gembloq_rules.html [new file with mode: 0644]
doc/help/C/pentobi/index.html [new file with mode: 0644]
doc/help/C/pentobi/junior_rules.html [new file with mode: 0644]
doc/help/C/pentobi/license.html [new file with mode: 0644]
doc/help/C/pentobi/nexos_rules.html [new file with mode: 0644]
doc/help/C/pentobi/pieces.png [new file with mode: 0644]
doc/help/C/pentobi/pieces_callisto.png [new file with mode: 0644]
doc/help/C/pentobi/pieces_gembloq.jpg [new file with mode: 0644]
doc/help/C/pentobi/pieces_junior.png [new file with mode: 0644]
doc/help/C/pentobi/pieces_nexos.png [new file with mode: 0644]
doc/help/C/pentobi/pieces_trigon.jpg [new file with mode: 0644]
doc/help/C/pentobi/position_callisto.png [new file with mode: 0644]
doc/help/C/pentobi/position_classic.png [new file with mode: 0644]
doc/help/C/pentobi/position_duo.png [new file with mode: 0644]
doc/help/C/pentobi/position_gembloq.png [new file with mode: 0644]
doc/help/C/pentobi/position_nexos.png [new file with mode: 0644]
doc/help/C/pentobi/position_trigon.jpg [new file with mode: 0644]
doc/help/C/pentobi/rating.jpg [new file with mode: 0644]
doc/help/C/pentobi/shortcuts.html [new file with mode: 0644]
doc/help/C/pentobi/stylesheet.css [new file with mode: 0644]
doc/help/C/pentobi/system.html [new file with mode: 0644]
doc/help/C/pentobi/trigon_rules.html [new file with mode: 0644]
doc/help/C/pentobi/user_interface.html [new file with mode: 0644]
doc/help/C/pentobi/window_menu.html [new file with mode: 0644]
doc/help/de/pentobi/become_stronger.html [new file with mode: 0644]
doc/help/de/pentobi/callisto_rules.html [new file with mode: 0644]
doc/help/de/pentobi/classic_rules.html [new file with mode: 0644]
doc/help/de/pentobi/duo_rules.html [new file with mode: 0644]
doc/help/de/pentobi/gembloq_rules.html [new file with mode: 0644]
doc/help/de/pentobi/index.html [new file with mode: 0644]
doc/help/de/pentobi/junior_rules.html [new file with mode: 0644]
doc/help/de/pentobi/license.html [new file with mode: 0644]
doc/help/de/pentobi/nexos_rules.html [new file with mode: 0644]
doc/help/de/pentobi/shortcuts.html [new file with mode: 0644]
doc/help/de/pentobi/system.html [new file with mode: 0644]
doc/help/de/pentobi/trigon_rules.html [new file with mode: 0644]
doc/help/de/pentobi/user_interface.html [new file with mode: 0644]
doc/help/de/pentobi/window_menu.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/book_callisto.blksgf [new file with mode: 0644]
src/books/book_callisto_2.blksgf [new file with mode: 0644]
src/books/book_callisto_2_4.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_gembloq.blksgf [new file with mode: 0644]
src/books/book_gembloq_2.blksgf [new file with mode: 0644]
src/books/book_gembloq_2_4.blksgf [new file with mode: 0644]
src/books/book_gembloq_3.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/icon/pentobi-16.svg [new file with mode: 0644]
src/icon/pentobi-32.svg [new file with mode: 0644]
src/icon/pentobi-64.svg [new file with mode: 0644]
src/icon/pentobi.svg [new file with mode: 0644]
src/icon/pentobi_icon.qrc [new file with mode: 0644]
src/icon/pentobi_icon_desktop.qrc [new file with mode: 0644]
src/learn_tool/CMakeLists.txt [new file with mode: 0644]
src/learn_tool/Main.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/Reader.cpp [new file with mode: 0644]
src/libboardgame_sgf/Reader.h [new file with mode: 0644]
src/libboardgame_sgf/SgfError.cpp [new file with mode: 0644]
src/libboardgame_sgf/SgfError.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/Main.cpp [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_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.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/GembloQGeometry.cpp [new file with mode: 0644]
src/libpentobi_base/GembloQGeometry.h [new file with mode: 0644]
src/libpentobi_base/GembloQTransform.cpp [new file with mode: 0644]
src/libpentobi_base/GembloQTransform.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/PieceTransformsGembloQ.cpp [new file with mode: 0644]
src/libpentobi_base/PieceTransformsGembloQ.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.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_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/LocalPoints.cpp [new file with mode: 0644]
src/libpentobi_mcts/LocalPoints.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.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_paint/CMakeLists.txt [new file with mode: 0644]
src/libpentobi_paint/Paint.cpp [new file with mode: 0644]
src/libpentobi_paint/Paint.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/AnalyzeGameModel.cpp [new file with mode: 0644]
src/pentobi/AnalyzeGameModel.h [new file with mode: 0644]
src/pentobi/AndroidUtils.cpp [new file with mode: 0644]
src/pentobi/AndroidUtils.h [new file with mode: 0644]
src/pentobi/CMakeLists.txt [new file with mode: 0644]
src/pentobi/GameModel.cpp [new file with mode: 0644]
src/pentobi/GameModel.h [new file with mode: 0644]
src/pentobi/ImageProvider.cpp [new file with mode: 0644]
src/pentobi/ImageProvider.h [new file with mode: 0644]
src/pentobi/Main.cpp [new file with mode: 0644]
src/pentobi/Pentobi.pro [new file with mode: 0644]
src/pentobi/PieceModel.cpp [new file with mode: 0644]
src/pentobi/PieceModel.h [new file with mode: 0644]
src/pentobi/PlayerModel.cpp [new file with mode: 0644]
src/pentobi/PlayerModel.h [new file with mode: 0644]
src/pentobi/RatingModel.cpp [new file with mode: 0644]
src/pentobi/RatingModel.h [new file with mode: 0644]
src/pentobi/SyncSettings.h [new file with mode: 0644]
src/pentobi/android/AndroidManifest.xml [new file with mode: 0644]
src/pentobi/android/res/drawable-hdpi/icon.png [new file with mode: 0644]
src/pentobi/android/res/drawable-mdpi/icon.png [new file with mode: 0644]
src/pentobi/android/res/drawable/splash.xml [new file with mode: 0644]
src/pentobi/android/res/values/theme.xml [new file with mode: 0644]
src/pentobi/android_icons_svg/icon48.svg [new file with mode: 0644]
src/pentobi/android_icons_svg/icon72.svg [new file with mode: 0644]
src/pentobi/qml/AboutDialog.qml [new file with mode: 0644]
src/pentobi/qml/AnalyzeDialog.qml [new file with mode: 0644]
src/pentobi/qml/AnalyzeGame.qml [new file with mode: 0644]
src/pentobi/qml/AppearanceDialog.qml [new file with mode: 0644]
src/pentobi/qml/AsciiArtSaveDialog.qml [new file with mode: 0644]
src/pentobi/qml/Board.qml [new file with mode: 0644]
src/pentobi/qml/BoardContextMenu.qml [new file with mode: 0644]
src/pentobi/qml/BusyIndicator.qml [new file with mode: 0644]
src/pentobi/qml/Button.qml [new file with mode: 0644]
src/pentobi/qml/ButtonApply.qml [new file with mode: 0644]
src/pentobi/qml/ButtonCancel.qml [new file with mode: 0644]
src/pentobi/qml/ButtonClose.qml [new file with mode: 0644]
src/pentobi/qml/ButtonOk.qml [new file with mode: 0644]
src/pentobi/qml/ButtonToolTip.qml [new file with mode: 0644]
src/pentobi/qml/Comment.qml [new file with mode: 0644]
src/pentobi/qml/ComputerDialog.qml [new file with mode: 0644]
src/pentobi/qml/Controls.js [new file with mode: 0644]
src/pentobi/qml/Dialog.qml [new file with mode: 0644]
src/pentobi/qml/DialogButtonBox.qml [new file with mode: 0644]
src/pentobi/qml/DialogButtonBoxOkCancel.qml [new file with mode: 0644]
src/pentobi/qml/DialogLoader.qml [new file with mode: 0644]
src/pentobi/qml/ExportImageDialog.qml [new file with mode: 0644]
src/pentobi/qml/FatalMessage.qml [new file with mode: 0644]
src/pentobi/qml/FileDialog.qml [new file with mode: 0644]
src/pentobi/qml/GameDisplay.js [new file with mode: 0644]
src/pentobi/qml/GameDisplayDesktop.qml [new file with mode: 0644]
src/pentobi/qml/GameDisplayMobile.qml [new file with mode: 0644]
src/pentobi/qml/GameInfoDialog.qml [new file with mode: 0644]
src/pentobi/qml/GameVariantDialog.qml [new file with mode: 0644]
src/pentobi/qml/GotoMoveDialog.qml [new file with mode: 0644]
src/pentobi/qml/HelpWindow.qml [new file with mode: 0644]
src/pentobi/qml/ImageSaveDialog.qml [new file with mode: 0644]
src/pentobi/qml/InitialRatingDialog.qml [new file with mode: 0644]
src/pentobi/qml/LineSegment.qml [new file with mode: 0644]
src/pentobi/qml/Main.js [new file with mode: 0644]
src/pentobi/qml/Main.qml [new file with mode: 0644]
src/pentobi/qml/Menu.qml [new file with mode: 0644]
src/pentobi/qml/MenuComputer.qml [new file with mode: 0644]
src/pentobi/qml/MenuEdit.qml [new file with mode: 0644]
src/pentobi/qml/MenuExport.qml [new file with mode: 0644]
src/pentobi/qml/MenuGame.qml [new file with mode: 0644]
src/pentobi/qml/MenuGo.qml [new file with mode: 0644]
src/pentobi/qml/MenuHelp.qml [new file with mode: 0644]
src/pentobi/qml/MenuItem.qml [new file with mode: 0644]
src/pentobi/qml/MenuRecentFiles.qml [new file with mode: 0644]
src/pentobi/qml/MenuSeparator.qml [new file with mode: 0644]
src/pentobi/qml/MenuTools.qml [new file with mode: 0644]
src/pentobi/qml/MenuView.qml [new file with mode: 0644]
src/pentobi/qml/MessageDialog.qml [new file with mode: 0644]
src/pentobi/qml/MoveAnnotationDialog.qml [new file with mode: 0644]
src/pentobi/qml/NavigationButtons.qml [new file with mode: 0644]
src/pentobi/qml/NavigationPanel.qml [new file with mode: 0644]
src/pentobi/qml/NewFolderDialog.qml [new file with mode: 0644]
src/pentobi/qml/OpenDialog.qml [new file with mode: 0644]
src/pentobi/qml/PieceCallisto.qml [new file with mode: 0644]
src/pentobi/qml/PieceClassic.qml [new file with mode: 0644]
src/pentobi/qml/PieceFlipAnimation.qml [new file with mode: 0644]
src/pentobi/qml/PieceGembloQ.qml [new file with mode: 0644]
src/pentobi/qml/PieceList.qml [new file with mode: 0644]
src/pentobi/qml/PieceManipulator.qml [new file with mode: 0644]
src/pentobi/qml/PieceNexos.qml [new file with mode: 0644]
src/pentobi/qml/PieceRotationAnimation.qml [new file with mode: 0644]
src/pentobi/qml/PieceSelectorDesktop.qml [new file with mode: 0644]
src/pentobi/qml/PieceSelectorMobile.qml [new file with mode: 0644]
src/pentobi/qml/PieceSwitchedFlipAnimation.qml [new file with mode: 0644]
src/pentobi/qml/PieceTrigon.qml [new file with mode: 0644]
src/pentobi/qml/QuarterSquare.qml [new file with mode: 0644]
src/pentobi/qml/QuestionDialog.qml [new file with mode: 0644]
src/pentobi/qml/RatingDialog.qml [new file with mode: 0644]
src/pentobi/qml/RatingGraph.qml [new file with mode: 0644]
src/pentobi/qml/SaveDialog.qml [new file with mode: 0644]
src/pentobi/qml/ScoreDisplay.qml [new file with mode: 0644]
src/pentobi/qml/ScoreElement.qml [new file with mode: 0644]
src/pentobi/qml/ScoreElement2.qml [new file with mode: 0644]
src/pentobi/qml/Square.qml [new file with mode: 0644]
src/pentobi/qml/ToolBar.qml [new file with mode: 0644]
src/pentobi/qml/Triangle.qml [new file with mode: 0644]
src/pentobi/qml/i18n/qml_de.ts [new file with mode: 0644]
src/pentobi/qml/i18n/qml_en.ts [new file with mode: 0644]
src/pentobi/qml/i18n/qml_fr.ts [new file with mode: 0644]
src/pentobi/qml/i18n/qml_nb_NO.ts [new file with mode: 0644]
src/pentobi/qml/i18n/translations.qrc [new file with mode: 0644]
src/pentobi/qml/icons/filedialog-folder.svg [new file with mode: 0644]
src/pentobi/qml/icons/filedialog-newfolder.svg [new file with mode: 0644]
src/pentobi/qml/icons/filedialog-parent.svg [new file with mode: 0644]
src/pentobi/qml/themes/colorblind-dark/Theme.qml [new file with mode: 0644]
src/pentobi/qml/themes/colorblind-light/Theme.qml [new file with mode: 0644]
src/pentobi/qml/themes/dark/Theme.qml [new file with mode: 0644]
src/pentobi/qml/themes/dark/pentobi-rated-game.svg [new file with mode: 0644]
src/pentobi/qml/themes/dark/piece-manipulator-desktop-legal.svg [new file with mode: 0644]
src/pentobi/qml/themes/dark/piece-manipulator-desktop.svg [new file with mode: 0644]
src/pentobi/qml/themes/dark/piece-manipulator-legal.svg [new file with mode: 0644]
src/pentobi/qml/themes/dark/piece-manipulator.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/Theme.qml [new file with mode: 0644]
src/pentobi/qml/themes/light/menu.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-backward.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-backward10.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-beginning.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-computer-colors.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-end.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-forward.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-forward10.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-newgame.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-next-variation.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-play.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-previous-variation.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-rated-game.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-stop.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/pentobi-undo.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/piece-manipulator-desktop-legal.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/piece-manipulator-desktop.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/piece-manipulator-legal.svg [new file with mode: 0644]
src/pentobi/qml/themes/light/piece-manipulator.svg [new file with mode: 0644]
src/pentobi/qml/themes/system/Theme.qml [new file with mode: 0644]
src/pentobi/qml/themes/themes.qrc [new file with mode: 0644]
src/pentobi/resources.qrc [new file with mode: 0644]
src/pentobi/resources_desktop.qrc [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_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/SgfTreeTest.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/PentobiSgfUtilTest.cpp [new file with mode: 0644]
src/unittest/libpentobi_base/PentobiTreeTest.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]

diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..3919f9a
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,8 @@
+Main developer:
+
+Markus Enzenberger <enz@users.sourceforge.net>
+
+Translators:
+
+Allan Nordhøy <epost@anotheragency.no> (Norsk bokmål)
+Markus Enzenberger <enz@users.sourceforge.net> (German, French)
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..a997ba5
--- /dev/null
@@ -0,0 +1,46 @@
+cmake_minimum_required(VERSION 3.1.0)
+
+project(Pentobi)
+set(PENTOBI_VERSION 16.2)
+set(PENTOBI_RELEASE_DATE 2019-01-16)
+
+cmake_policy(SET CMP0043 NEW)
+cmake_policy(SET CMP0071 NEW)
+
+include(GNUInstallDirs)
+
+option(PENTOBI_BUILD_GTP "Build GTP interface" OFF)
+option(PENTOBI_BUILD_GUI "Build GUI" ON)
+option(PENTOBI_BUILD_TESTS "Build unit tests" OFF)
+option(PENTOBI_BUILD_THUMBNAILER "Build Gnome thumbnailer" ON)
+option(PENTOBI_BUILD_KDE_THUMBNAILER "Build KDE thumbnailer" OFF)
+option(PENTOBI_OPEN_HELP_EXTERNALLY "Force using web browser for displaying help" OFF)
+
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+  message(STATUS "No build type selected, default to Release")
+  set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
+endif()
+
+set(CMAKE_CXX_STANDARD 14)
+if(CMAKE_COMPILER_IS_GNUCXX OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang"))
+  add_compile_options(-ffast-math -Wall -Wextra)
+endif()
+
+if(PENTOBI_BUILD_TESTS)
+  if(PENTOBI_BUILD_KDE_THUMBNAILER)
+    configure_file(CTestCustom.cmake ${CMAKE_BINARY_DIR} COPYONLY)
+  endif()
+  enable_testing()
+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)
+
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..f288702
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://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 <https://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
+<https://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
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/CTestCustom.cmake b/CTestCustom.cmake
new file mode 100644 (file)
index 0000000..fb76026
--- /dev/null
@@ -0,0 +1,4 @@
+# We don't want to run appstreamtest added by KDECMakeSettings if
+# PENTOBI_BUILD_KDE_THUMBNAILER because it requires an Internet connection
+# to check the screenshot images.
+set(CTEST_CUSTOM_TESTS_IGNORE appstreamtest)
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
index 0000000..c2a7aa9
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,58 @@
+This file explains how to compile and install Pentobi from the sources.
+
+
+== Requirements ==
+
+Pentobi requires the Qt libraries (>=5.11). The C++ compiler needs C++14
+support (GCC >=4.9). The build system uses CMake (>=3.1.0).
+
+In Debian-based distributions that support Qt >=5.11, the necessary tools
+and libraries can be installed with the command:
+
+  sudo apt-get install cmake g++ libqt5svg5-dev libqt5webview5-dev make \
+    qml-module-qt-labs-folderlistmodel qml-module-qt-labs-settings \
+    qml-module-qtquick2 qml-module-qtquick-controls2 \
+    qml-module-qtquick-layouts qml-module-qtquick-window2 \
+    qml-module-qtwebview qtquickcontrols2-5-dev qttools5-dev
+
+
+== Building ==
+
+Pentobi can be compiled from the source directory with the command:
+
+  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 libkf5kio-dev and extra-cmake-modules on
+Debian-based distributions). Note that the plugin might 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 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 Debian-based distributions 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 ==
+
+Because building, deploying and debugging for Android is not yet functional
+for CMake projects in QtCreator, there exists a project file in
+src/pentobi/Pentobi.pro for building the Android app.
diff --git a/NEWS b/NEWS
new file mode 100644 (file)
index 0000000..de40122
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,798 @@
+Version 16.2 (16 Jan 2019)
+==========================
+
+* As a workaround for platforms without support for Qt5WebView, Pentobi can
+  now be built such that the help is displayed in an external web browser.
+  This option will automatically be used if Qt5WebView is not found or if the
+  cmake option -DPENTOBI_OPEN_HELP_EXTERNALLY=ON is used. Note that this
+  requires that a web browser is installed.
+* The help files are no longer compiled into the resources but installed
+  again in DATAROOTDIR/help.
+* Fixed keyboard navigation in file dialog.
+* Status message was not shown after successful Export/ASCII Art and
+  Android Media Scanner was not informed to make the saved file
+  immediately visible to MTP-connected devices.
+* Changed new-game icon, which looked too much like a ratings/bookmarks
+  icon.
+* Fixed compilation on systems without sys/sysctl.h header.
+* Enabled QML compiler again, now that QTBUG-70976 has been fixed, which
+  broke translations in Qt 5.12 beta releases.
+* Changed android.app_extract_android_style in Android manifest from none
+  to minimal, which is recommended for Quick Controls 2 apps (see
+  QTBUG-69810 and comments in QTBUG-71902)
+
+
+Version 16.1 (11 Oct 2018)
+==========================
+
+* Fixed alignment issues of pieces on board if high-DPI scaling is used.
+
+
+Version 16.0 (10 Oct 2018)
+==========================
+
+* The desktop version of Pentobi now uses the same QtQuick-based GUI as
+  the Android version, which makes the desktop version support all
+  features of the Android version like piece animations, dark and light
+  themes and more of the state saved between sessions (e.g. position in
+  game tree, modifications to loaded file, current analysis)
+* The minimum required Qt version is now 5.11 also for the desktop version
+  See INSTALL for the new run-time and compile-time dependencies.
+* The installation directory /usr/share/pentobi does not exist anymore.
+  The translations, opening books and user manual are compiled as resources
+  into the binary executable.
+* New themes optimized for colorblindness.
+* New appearance option in desktop mode that handles the comment visibility
+  after a position change.
+* New context menu to go to any played move on board or edit its annotation.
+* Additional warning dialogs to reduce the likelihood that an autosaved
+  game is lost, for example because it was changed by another instance
+  of Pentobi.
+* The computer level is now set in the computer colors dialog. It is no
+  longer stored in the settings separately for each game variant.
+* The visibility of the move number and variation information in the status
+  bar can now be configured in the appearance dialog and is off by default.
+* New toolbar button to stop computer play or game analysis.
+* Play and undo buttons now support autorepeat.
+* Reintroduced forward10/backward10 toolbuttons on desktop.
+* New shortcut keys for moving the selected piece in larger steps on the
+  board.
+* New menu item Recent Files/Clear List.
+* The locations and file format for the rated game history is not compatible
+  with Pentobi 15.0, the rating history will be lost.
+* New shortcut Ctrl+Shift+H, which behaves like Find Move (Ctrl+H) but
+  iterates backwards through the list of legal moves.
+* The game analysis now always contains a value for the position after
+  the last move, which is useful for analyzing unfinished games.
+* The Android version now shows an error before open/save if permission
+  to access storage have not been granted.
+* Android: color dot of the color to play is no longer surrounded by a border
+  because the color to play is already indicated by having its unplayed pieces
+  at the top.
+* The translation source strings for menu items and actions no longer use
+  an ampersand to mark a mnemonic but a separate translation string for
+  the mnemonic.
+
+Bug fixes (both desktop and Android version of Pentobi 15.0):
+
+* Fixed bugs in handling AE (add empty) SGF property.
+
+Bug fixes (Android version of Pentobi 15.0):
+
+* Picking up a piece from board in setup mode sometimes switched piece
+  instances in game variants with multiple instances per piece.
+* Workaround for a bug that made the analysis graph only partially visible
+  on Android low-density devices (QTBUG-69102)
+* Program could hang or crash if quit during running game analysis.
+* Board was not updated if it became empty after opening a file failed.
+* Running computer move was not aborted after opening a file failed.
+* Disable menu item Analyze Game if game has no moves.
+* Show a meaningful error message if startup fails due to low memory.
+* Show a warning if current game has unsaved changes before opening
+  a file from clipboard.
+* Changed text color on purple pieces to white to make it more readable.
+* Game info was not updated after loading a file.
+* Rating dialog did not show game variant in Callisto (2 players, 4 colors)
+* Don't crash if game analysis stored in settings was not valid.
+* Game was not marked as modified after changing move annotation.
+
+
+Version 15.0 (28 Jun 2018)
+==========================
+
+General:
+
+* New UI translations: French, Norsk bokmål (thanks to Allan Nordhøy)
+* Added a workaround for a compiler issue with GCC 7/8, which slowed
+  down the startup time of Pentobi.
+* Disable menu item "Keep Only Position" if board is empty.
+
+Android version:
+
+* The minimum required Qt version is now 5.11.
+* Games table in rating dialog did not show the correct level used.
+* Saved files should now immediately be visible from computers connecting
+  with the Android device via MTP (might not work on all devices).
+* An error message is now shown when an invalid loaded SGF file causes
+  a problem later (e.g. invalid move property value in a side variation).
+
+
+Version 14.1 (03 Jan 2018)
+==========================
+
+General:
+
+* Fixed a potential race condition during move generation.
+* Reduced maximum memory usage to a quarter instead of a third of the
+  total system memory.
+* Made unit tests work again.
+
+Android version:
+
+* Migrated QML files from Qt 5.6 to Qt >=5.7.
+* The binary translation files are now automatically created by the
+  qmake project file.
+
+
+Version 14.0 (26 Oct 2017)
+==========================
+
+General:
+
+* Increased playing strength in almost all game variants (except for
+  Nexos), especially in Trigon, GembloQ and Callisto.
+* Duo now uses the colors purple/orange.
+* Junior now uses the colors green/orange.
+* File format: accept whitespaces before and after property identifiers.
+
+Desktop version:
+
+* Minimum required Qt version is now 5.6.
+* Bugfix: dot indicating color to play in orientation selector was not
+  always updated correctly after loading a file of a different game
+  variant.
+* Bugfix: added missing include that broke compilation on FreeBSD 11.
+
+
+Version 13.1 (06 Jun 2017)
+==========================
+
+General:
+
+* Fixed some crashes that could be triggered by invalid SGF files.
+
+Desktop version:
+
+* Callisto: selected piece was wrongly rendered as one-piece in some
+  situations if partially outside board.
+* Fixed Leave Fullscreen button positioning if multiple screens exist.
+* Window close button did not work in message dialogs with detailed text.
+* Ctrl-W now closes application.
+* Use reverse-domain file names for appstream and desktop file.
+* Removed no longer needed workaround for disabling appstreamtest
+  added by KDECMakeSettings.
+
+Android version:
+
+* Displayed game variant was not changed when loading a file of a
+  different game variant with SGF errors.
+
+
+Version 13.0 (17 Mar 2017)
+==========================
+
+General:
+
+* New game variant GembloQ.
+* New game subvariant Callisto Two-Player Four-Color.
+* Slightly increased playing strength in Callisto, Trigon and Nexos.
+* New menu item Game/Open From Clipboard.
+* The engine now uses up to 8 threads (instead of 4) by default if the CPU
+  has enough hardware threads.
+* Support for SGF file encodings other than ISO-8859-1 and UTF-8.
+
+Desktop version:
+
+* Install AppData file to /usr/share/metainfo instead of /usr/share/appdata.
+* Added AppStream file for the KDE thumbnailer.
+* Disabled AppStream tests added by KDECMakeSettings that are broken in some
+  versions of KDE and made the project tests fail.
+* The compilation now requires CMake >=3.1.0.
+
+Android version:
+
+* The Android version now supports most features of the desktop version,
+  including comments, move annotations, setup positions, game analysis (only
+  a very fast mode) and rated games. The playing levels are still restricted
+  to 1-7 because the top levels would be too slow on mobile devices.
+* Current game position, associated file name and file modification status
+  are now remembered between sessions.
+* Opened games now show the initial instead of the last position if the
+  initial position contains either a setup or a comment.
+* Game/Find Move now behaves like in the desktop version and will cycle
+  through all legal moves if called repeatedly.
+* Added a light theme in addition to the default dark theme.
+* New menu item View/Fullscreen to make better use of small-screen displays.
+* Bugfix: game variant Junior erroneously used level set for Classic.
+* The minimum required Android version is now 4.1.
+
+
+Version 12.2 (05 Jan 2017)
+==========================
+
+Desktop version:
+
+* Added patterns for Nexos and Callisto SGF files to MIME type
+  specification for detecting them independently of the file ending.
+* 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 in updating the analysis window that could cause
+  a crash 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..0096826
--- /dev/null
+++ b/README
@@ -0,0 +1,29 @@
+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,
+GembloQ and Callisto.
+
+See INSTALL for instructions how to build the program from the sources.
+See NEWS for release notes.
+See AUTHORS for a full list of copyright holders.
+The homepage of Pentobi is at https://pentobi.sourceforge.io
+
+Copyright (C) 2011-2019 Markus Enzenberger <enz@users.sourceforge.net>
+
+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 <https://www.gnu.org/licenses/>.
+
+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.
diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt
new file mode 100644 (file)
index 0000000..af3d669
--- /dev/null
@@ -0,0 +1,68 @@
+if(PENTOBI_BUILD_GUI)
+
+foreach(icon application-x-blokus-sgf application-x-blokus-sgf-16
+        application-x-blokus-sgf-32 application-x-blokus-sgf-64)
+  set(icon_svg "${CMAKE_CURRENT_SOURCE_DIR}/${icon}.svg")
+  set(icon_png "${CMAKE_CURRENT_BINARY_DIR}/${icon}.png")
+  add_custom_command(OUTPUT "${icon_png}"
+    COMMAND convert "${icon_svg}" "${icon_png}" DEPENDS "${icon_svg}")
+  list(APPEND png_icons "${icon_png}")
+endforeach()
+foreach(icon pentobi pentobi-16 pentobi-32 pentobi-64)
+  set(icon_svg "${CMAKE_SOURCE_DIR}/src/icon/${icon}.svg")
+  set(icon_png "${CMAKE_CURRENT_BINARY_DIR}/${icon}.png")
+  add_custom_command(OUTPUT "${icon_png}"
+    COMMAND convert "${icon_svg}" "${icon_png}" DEPENDS "${icon_svg}")
+  list(APPEND png_icons "${icon_png}")
+endforeach()
+add_custom_target(data_icons ALL DEPENDS ${png_icons})
+
+configure_file(io.sourceforge.pentobi.desktop.in io.sourceforge.pentobi.desktop @ONLY)
+configure_file(io.sourceforge.pentobi.appdata.xml.in io.sourceforge.pentobi.appdata.xml @ONLY)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.png
+  DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi-16.png
+  DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps
+  RENAME pentobi.png)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/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/icon/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 ${CMAKE_CURRENT_BINARY_DIR}/application-x-blokus-sgf-32.png
+  DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/mimetypes
+  RENAME application-x-blokus-sgf.png)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/application-x-blokus-sgf-64.png
+  DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/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}/io.sourceforge.pentobi.desktop
+  DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications)
+install(FILES pentobi-mime.xml
+  DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/mime/packages)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/io.sourceforge.pentobi.appdata.xml
+  DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo)
+
+endif(PENTOBI_BUILD_GUI)
+
+if(PENTOBI_BUILD_THUMBNAILER)
+  configure_file(pentobi.thumbnailer.in pentobi.thumbnailer @ONLY)
+  install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.thumbnailer
+    DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/thumbnailers)
+endif()
+
+if(PENTOBI_BUILD_KDE_THUMBNAILER)
+  configure_file(io.sourceforge.pentobi.kde_thumbnailer.metainfo.xml.in
+    io.sourceforge.pentobi.kde_thumbnailer.metainfo.xml @ONLY)
+  install(FILES ${CMAKE_CURRENT_BINARY_DIR}/io.sourceforge.pentobi.kde_thumbnailer.metainfo.xml
+    DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo)
+endif()
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..7232a07
--- /dev/null
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect stroke-linejoin="round" ry="1.15" height="15" width="13" stroke="#7c7f79" y=".5" x="1.5" fill="#eeeeec"/>
+ <path d="m4.0004 10-0.0004 1.321c-0.001 0.375 0.3007 0.677 0.675 0.679h1.325v-1.999l-1.3324-0.001z" stroke-width=".2" fill="#edd400"/>
+ <g id="f" transform="matrix(.2 0 0 .2 3.2 3.2)">
+  <rect height="10" width="10" y="4" x="24" fill="#73d216"/>
+  <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+  <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use xlink:href="#f" transform="translate(-8,-8)" height="100%" width="100%" y="10" x="10"/>
+ <use xlink:href="#f" transform="translate(-8,-16)" height="100%" width="100%" y="20" x="10"/>
+ <g id="e" transform="matrix(.2 0 0 .2 3.2 3.2)">
+  <rect height="10" width="10" y="24" x="14" fill="#3465a4"/>
+  <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+  <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use xlink:href="#e" transform="translate(-8)" height="100%" width="100%" y="0" x="10"/>
+ <use xlink:href="#e" transform="translate(-8,-8)" height="100%" width="100%" y="10" x="10"/>
+ <g id="d" transform="matrix(.2 0 0 .2 3.2 3.2)">
+  <rect height="10" width="10" y="14" x="4" fill="#edd400"/>
+  <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+  <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use xlink:href="#d" transform="translate(0,-8)" height="100%" width="100%" y="10" x="0"/>
+ <use xlink:href="#d" transform="translate(-8,-16)" height="100%" width="100%" y="20" x="10"/>
+ <g id="b" transform="matrix(.2 0 0 .2 5.2 3.2)">
+  <rect height="10" width="10" y="4" x="4" fill="#c00"/>
+  <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(.2 0 0 .2 8.641 2.306)">
+  <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+  <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 0.002 0.002z" fill="#ef2929"/>
+ </g>
+ <path d="m5.9992 9.9986-0.19922 0.19922v1.6h-1.6l-0.00196 0.002c0.12197 0.12174 0.29018 0.19698 0.47656 0.19805h1.3254v-1.9992h-0.00078z" stroke-width=".2" fill="#c4a000"/>
+ <path d="m4.0004 9.998-0.0004 1.321c-0.0005 0.189 0.0752 0.358 0.198 0.481l0.002-0.002v-1.6h1.6l0.19922-0.19922-1.3316-0.00078h-0.66718z" stroke-width=".2" fill="#fce94f"/>
+ <g transform="matrix(.2 0 0 .2 -3.005 4.6281)">
+  <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+  <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+  <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(.2 0 0 .2 -.9998 -1.3)">
+  <path d="m54.999 26.5v9.9961l6.664 0.004h3.336v-6.627c-0.006-1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+  <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-0.000355-0.000355-0.0016 0.000355-0.002 0z" fill="#4e9a06"/>
+  <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" xlink:href="#b" transform="translate(0,2)" height="100%" width="100%" y="0" x="0"/>
+ <use xlink:href="#a" transform="translate(2)" height="100%" width="100%" y="0" x="0"/>
+</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..35516b0
--- /dev/null
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect stroke-linejoin="round" ry="1.15" height="27" width="23" stroke="#555753" y="2.5" x="4.5" fill="#eeeeec"/>
+ <path d="m10.001 19-0.001 1.982c-0.0015 0.563 0.451 1.015 1.012 1.018h1.988v-2.999l-1.999-0.001z" stroke-width="0.3" fill="#edd400"/>
+ <g id="f" transform="matrix(.3 0 0 .3 8.8 8.8)">
+  <rect height="10" width="10" y="4" x="24" fill="#73d216"/>
+  <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+  <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use xlink:href="#f" transform="translate(-7,-7)" height="100%" width="100%" y="10" x="10"/>
+ <use xlink:href="#f" transform="translate(-7,-14)" height="100%" width="100%" y="20" x="10"/>
+ <g id="e" transform="matrix(.3 0 0 .3 8.8 8.8)">
+  <rect height="10" width="10" y="24" x="14" fill="#3465a4"/>
+  <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+  <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use xlink:href="#e" transform="translate(-7)" height="100%" width="100%" y="0" x="10"/>
+ <use xlink:href="#e" transform="translate(-7,-7)" height="100%" width="100%" y="10" x="10"/>
+ <g id="d" transform="matrix(.3 0 0 .3 8.8 8.8)">
+  <rect height="10" width="10" y="14" x="4" fill="#edd400"/>
+  <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+  <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use xlink:href="#d" transform="translate(0,-7)" height="100%" width="100%" y="10" x="0"/>
+ <use xlink:href="#d" transform="translate(-7,-14)" height="100%" width="100%" y="20" x="10"/>
+ <g id="b" transform="matrix(.3 0 0 .3 11.8 8.8)">
+  <rect height="10" width="10" y="4" x="4" fill="#c00"/>
+  <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(.3 0 0 .3 16.962 7.459)">
+  <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+  <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 0.002 0.002z" fill="#ef2929"/>
+ </g>
+ <path d="m12.999 18.998-0.29883 0.29883v2.4h-2.4l-0.0029 0.0029c0.18296 0.1826 0.43527 0.29547 0.71484 0.29707h1.9881v-2.9988h-0.0012z" stroke-width="0.3" fill="#c4a000"/>
+ <path d="m10.001 18.997-0.001 1.982c-0.00078 0.2823 0.11277 0.5367 0.29706 0.7209l0.003-0.003v-2.4h2.4l0.29882-0.29883-1.9974-0.0012h-1.0008z" stroke-width="0.3" fill="#fce94f"/>
+ <g transform="matrix(.3 0 0 .3 -.5075 10.942)">
+  <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+  <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+  <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(.3 0 0 .3 2.5003 2.05)">
+  <path d="m54.999 26.5v9.9961l6.664 0.004h3.336v-6.627c-0.006-1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+  <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-0.000355-0.000355-0.0016 0.000355-0.002 0z" fill="#4e9a06"/>
+  <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" xlink:href="#b" transform="translate(0,3)" height="100%" width="100%" y="0" x="0"/>
+ <use xlink:href="#a" transform="translate(3)" height="100%" width="100%" y="0" x="0"/>
+</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..83c234a
--- /dev/null
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect stroke-linejoin="round" ry="1.15" height="55" width="47" stroke="#555753" y="4.5" x="8.5" fill="#eeeeec"/>
+ <path d="m18.001 39-0.001 4.624c-0.0036 1.3125 1.0523 2.3674 2.3625 2.3751h4.638v-6.9971l-4.6635-0.0028z" stroke-width=".69999" fill="#edd400"/>
+ <g id="f" transform="matrix(.7 0 0 .69999 15.2 15.2)">
+  <rect height="10" width="10" y="4" x="24" fill="#73d216"/>
+  <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+  <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use xlink:href="#f" transform="translate(-3 -2.9999)" height="100%" width="100%" y="10" x="10"/>
+ <use xlink:href="#f" transform="translate(-3 -5.9999)" height="100%" width="100%" y="20" x="10"/>
+ <g id="e" transform="matrix(.7 0 0 .69999 15.2 15.2)">
+  <rect height="10" width="10" y="24" x="14" fill="#3465a4"/>
+  <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+  <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use xlink:href="#e" transform="translate(-3,2.4e-4)" height="100%" width="100%" y="0" x="10"/>
+ <use xlink:href="#e" transform="translate(-3 -2.9997)" height="100%" width="100%" y="10" x="10"/>
+ <g id="d" transform="matrix(.7 0 0 .69999 15.2 15.2)">
+  <rect height="10" width="10" y="14" x="4" fill="#edd400"/>
+  <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+  <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use xlink:href="#d" transform="translate(0 -2.9998)" height="100%" width="100%" y="10" x="0"/>
+ <use xlink:href="#d" transform="translate(-3 -5.9998)" height="100%" width="100%" y="20" x="10"/>
+ <g id="b" transform="matrix(.7 0 0 .69999 22.2 15.2)">
+  <rect height="10" width="10" y="4" x="4" fill="#c00"/>
+  <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(.7 0 0 .69999 34.244 12.071)">
+  <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+  <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 0.002 0.002z" fill="#ef2929"/>
+ </g>
+ <path d="m24.997 38.995-0.69726 0.69725v5.5999h-5.6l-0.0069 0.0069c0.4269 0.42607 1.0156 0.68941 1.668 0.69315h4.6389v-6.9972h-0.0027z" stroke-width=".69999" fill="#c4a000"/>
+ <path d="m18.001 38.993-0.001 4.624c-0.0018 0.65869 0.26313 1.2523 0.69314 1.6821l0.007-0.006v-5.5999h5.5999l0.69726-0.69725-4.6607-0.0027h-2.3351z" stroke-width=".69999" fill="#fce94f"/>
+ <g transform="matrix(.7 0 0 .69999 -6.5175 20.198)">
+  <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+  <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+  <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(.7 0 0 .69999 .5007 -.54978)">
+  <path d="m54.999 26.5v9.9961l6.664 0.004h3.336v-6.627c-0.006-1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+  <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-0.000355-0.000355-0.0016 0.000355-0.002 0z" fill="#4e9a06"/>
+  <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" xlink:href="#b" transform="translate(0 7)" height="100%" width="100%" y="0" x="0"/>
+ <use xlink:href="#a" transform="translate(7,10e-5)" height="100%" width="100%" y="0" x="0"/>
+</svg>
diff --git a/data/application-x-blokus-sgf.svg b/data/application-x-blokus-sgf.svg
new file mode 100644 (file)
index 0000000..9f93f57
--- /dev/null
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect stroke-linejoin="round" ry="1.15" height="41" width="35" stroke="#555753" y="3.5" x="6.5" fill="#eeeeec"/>
+ <path d="m14.001 29-0.001 3.303c-0.0025 0.93748 0.75165 1.691 1.6875 1.6965h3.312v-4.9979l-3.331-0.002z" stroke-width=".49999" fill="#edd400"/>
+ <g id="f" transform="matrix(.5 0 0 .49999 12 12)">
+  <rect height="10" width="10" y="4" x="24" fill="#73d216"/>
+  <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+  <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use xlink:href="#f" transform="translate(-5 -4.9999)" height="100%" width="100%" y="10" x="10"/>
+ <use xlink:href="#f" transform="translate(-5 -9.9999)" height="100%" width="100%" y="20" x="10"/>
+ <g id="e" transform="matrix(.5 0 0 .49999 12 12)">
+  <rect height="10" width="10" y="24" x="14" fill="#3465a4"/>
+  <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+  <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use xlink:href="#e" transform="translate(-5,3.4e-4)" height="100%" width="100%" y="0" x="10"/>
+ <use xlink:href="#e" transform="translate(-5 -4.9997)" height="100%" width="100%" y="10" x="10"/>
+ <g id="d" transform="matrix(.5 0 0 .49999 12 12)">
+  <rect height="10" width="10" y="14" x="4" fill="#edd400"/>
+  <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+  <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use xlink:href="#d" transform="translate(0 -4.9998)" height="100%" width="100%" y="10" x="0"/>
+ <use xlink:href="#d" transform="translate(-5 -9.9998)" height="100%" width="100%" y="20" x="10"/>
+ <g id="b" transform="matrix(.5 0 0 .49999 17 12)">
+  <rect height="10" width="10" y="4" x="4" fill="#c00"/>
+  <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(.5 0 0 .49999 25.602 9.765)">
+  <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+  <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 0.002 0.002z" fill="#ef2929"/>
+ </g>
+ <path d="m18.998 28.996-0.49804 0.49804v3.9999h-4l-0.0049 0.0049c0.30493 0.30433 0.72545 0.49244 1.1914 0.4951h3.3135v-4.998h-0.002z" stroke-width=".49999" fill="#c4a000"/>
+ <path d="m14.001 28.995-0.001 3.303c-0.0013 0.47049 0.18795 0.89448 0.4951 1.2015l0.005-0.005v-3.9999h4l0.49804-0.49804-3.329-0.002h-1.668z" stroke-width=".49999" fill="#fce94f"/>
+ <g transform="matrix(.5 0 0 .49999 -3.5125 15.57)">
+  <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+  <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+  <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(.5 0 0 .49999 1.5005 .75022)">
+  <path d="m54.999 26.5v9.9961l6.664 0.004h3.336v-6.627c-0.006-1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+  <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-0.000355-0.000355-0.0016 0.000355-0.002 0z" fill="#4e9a06"/>
+  <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" xlink:href="#b" transform="translate(0,4.9999)" height="100%" width="100%" y="0" x="0"/>
+ <use xlink:href="#a" transform="translate(5,2.4e-4)" height="100%" width="100%" y="0" x="0"/>
+</svg>
diff --git a/data/io.sourceforge.pentobi.appdata.xml.in b/data/io.sourceforge.pentobi.appdata.xml.in
new file mode 100644 (file)
index 0000000..f248e87
--- /dev/null
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<component type="desktop-application">
+  <id>io.sourceforge.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, GembloQ.</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, GembloQ.</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.5 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,5 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">
+      https://pentobi.sourceforge.io/pentobi-classic.png</image>
+      <caption>Game variant Classic</caption>
+      <caption xml:lang="de">Spielvariante Klassisch</caption>
+    </screenshot>
+    <screenshot>
+      <image width="1248" height="702">
+      https://pentobi.sourceforge.io/pentobi-duo.png</image>
+      <caption>Game variant Duo</caption>
+      <caption xml:lang="de">Spielvariante Duo</caption>
+    </screenshot>
+    <screenshot>
+      <image width="1248" height="702">
+      https://pentobi.sourceforge.io/pentobi-trigon.png</image>
+      <caption>Game variant Trigon</caption>
+      <caption xml:lang="de">Spielvariante Trigon</caption>
+    </screenshot>
+    <screenshot>
+      <image width="1248" height="702">
+      https://pentobi.sourceforge.io/pentobi-nexos.png</image>
+      <caption>Game variant Nexos</caption>
+      <caption xml:lang="de">Spielvariante Nexos</caption>
+    </screenshot>
+    <screenshot>
+      <image width="1248" height="702">
+      https://pentobi.sourceforge.io/pentobi-gembloq.png</image>
+      <caption>Game variant GembloQ</caption>
+      <caption xml:lang="de">Spielvariante GembloQ</caption>
+    </screenshot>
+  </screenshots>
+
+  <url type="homepage">https://pentobi.sourceforge.io/</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>
+  <translation type="qt">pentobi</translation>
+
+  <releases>
+     <release version="@PENTOBI_VERSION@" date="@PENTOBI_RELEASE_DATE@"/>
+  </releases>
+</component>
diff --git a/data/io.sourceforge.pentobi.desktop.in b/data/io.sourceforge.pentobi.desktop.in
new file mode 100755 (executable)
index 0000000..1cef503
--- /dev/null
@@ -0,0 +1,13 @@
+[Desktop Entry]
+Name=Pentobi
+Comment=Computer opponent for the board game Blokus
+Comment[de]=Computer-Gegner für das Brettspiel Blokus
+Comment[fr]=Un adversaire d'ordinateur pour le jeu Blokus.
+Comment[nb_NO]=Datamaskinmotstander for brettspillet Blokus.
+Keywords=Blokus;Blokus Duo;Blokus Trigon;Blokus Junior;Nexos;Callisto;Gemblo Q;GembloQ
+Exec=@CMAKE_INSTALL_FULL_BINDIR@/pentobi %f
+Icon=pentobi
+Type=Application
+Categories=Game;BoardGame;
+MimeType=application/x-blokus-sgf;
+StartupWMClass=Pentobi
diff --git a/data/io.sourceforge.pentobi.kde_thumbnailer.metainfo.xml.in b/data/io.sourceforge.pentobi.kde_thumbnailer.metainfo.xml.in
new file mode 100644 (file)
index 0000000..80c4074
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<component type="addon">
+  <id>io.sourceforge.pentobi.kde_thumnailer</id>
+  <extends>org.kde.dolphin.desktop</extends>
+  <metadata_license>CC0-1.0</metadata_license>
+  <project_license>GPL-3.0+</project_license>
+  <name>Pentobi KDE Thumbnailer</name>
+  <name xml:lang="de">Pentobi-Vorschaubilder unter KDE</name>
+  <summary>Enables previews of game files written by Pentobi on KDE</summary>
+  <summary xml:lang="de">Ermöglicht Vorschaubilder von Spieldateien, die von Pentobi geschrieben wurden, unter KDE</summary>
+
+  <description>
+    <p>Plugin that enables previews of Blokus game files as written by the
+    program Pentobi in the Dolphin file manager of the KDE desktop
+    environment.</p>
+    <p xml:lang="de">Plug-in, das Vorschaubilder von Blokus-Spieldateien,
+    wie vom Programm Pentobi erzeugt, im Dateimanager Dolphin der
+    KDE-Desktop-Umgebung ermöglicht.</p>
+  </description>
+
+  <screenshots>
+    <screenshot type="default">
+      <image width="1248" height="702">
+      https://pentobi.sourceforge.io/pentobi-kde-thumbnailer.png</image>
+      <caption>Game file previews in Dolphin</caption>
+      <caption xml:lang="de">Vorschaubilder von Spieldateien in Dolphin</caption>
+    </screenshot>
+  </screenshots>
+
+  <url type="homepage">https://pentobi.sourceforge.io/</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>
+
+  <releases>
+     <release version="@PENTOBI_VERSION@" date="@PENTOBI_RELEASE_DATE@"/>
+  </releases>
+</component>
diff --git a/data/pentobi-mime.xml b/data/pentobi-mime.xml
new file mode 100644 (file)
index 0000000..8a0c9c2
--- /dev/null
@@ -0,0 +1,30 @@
+<?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>
+<comment xml:lang="fr">Partie de Blokus</comment>
+<comment xml:lang="nb_NO">Blokus-spill</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[Callisto Two-Player Four-Color]"/>
+<match type="string" offset="0:256" value="GM[GembloQ]"/>
+<match type="string" offset="0:256" value="GM[GembloQ Three-Player]"/>
+<match type="string" offset="0:256" value="GM[GembloQ Two-Player]"/>
+<match type="string" offset="0:256" value="GM[GembloQ Two-Player Four-Color]"/>
+<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.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..cbc7956
--- /dev/null
@@ -0,0 +1,7 @@
+if(PENTOBI_BUILD_GUI)
+    install(DIRECTORY help DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}
+        FILES_MATCHING PATTERN "*.css" PATTERN "*.html" PATTERN "*.png"
+        PATTERN "*.jpg")
+endif()
+
+add_subdirectory(man)
diff --git a/doc/blksgf/Pentobi-SGF.html b/doc/blksgf/Pentobi-SGF.html
new file mode 100644 (file)
index 0000000..befdf5f
--- /dev/null
@@ -0,0 +1,174 @@
+<!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">
+<style type="text/css">
+html { background-color: lightgray; }
+body {
+  background-color:white;
+  color:black;
+  font-size:17px;
+  line-height:23px;
+  max-width:60em;
+  margin:auto;
+  padding:15px;
+  min-height: 100vh;
+}
+a:link { text-decoration:none; color:blue; }
+a:visited { text-decoration:none; color:purple; }
+</style>
+</head>
+<body>
+<h1>Pentobi SGF Files</h1>
+<div style="font-size:small">Author: Markus Enzenberger<br>
+Last modified: 2017-09-16</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="https://pentobi.sourceforge.io">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>
+<p style="font-size:small"><b>Note</b><br>
+Older versions of Pentobi (up to version 13.1) did not accept whitespaces
+before and after property identifiers, so it is recommended to avoid them for
+compatibility.</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><a href="http://en.wikipedia.org/wiki/UTF-8">UTF-8</a> should be used as the
+character set. Pentobi always writes files in UTF-8 and indicates that with the
+<tt>CA</tt> property. Pentobi versions before 13.0 can only read SGF files
+encoded in UTF-8 or ISO-8859-1 (Latin1). 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 Two-Player Four-Color, Callisto Three-Player, GembloQ,
+GembloQ Two-Player, GembloQ Three-Player, GembloQ Two-Player Four-Color.</p>
+The strings are case-sensitive, words must be separated by exactly one space
+and must not contain whitespaces at the beginning or end of the string.
+<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>PB</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 must not 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
+GembloQ.</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>
+<p>In GembloQ, each square field is divided into four triangles with their own
+coordinates, like in this example:</p>
+<pre>
+       4 | / | \ | / | \ | /
+       3 | \ | / | \ | / | \
+       2 | / | \ | / | \ | /
+       1 | \ | / | \ | / | \
+          a b c d e f g h i
+</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/gtp/Pentobi-GTP.html b/doc/gtp/Pentobi-GTP.html
new file mode 100644 (file)
index 0000000..16fcc1b
--- /dev/null
@@ -0,0 +1,332 @@
+<!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">
+<style type="text/css">
+html { background-color: lightgray; }
+body {
+  background-color:white;
+  color:black;
+  font-size:17px;
+  line-height:23px;
+  max-width:60em;
+  margin:auto;
+  padding:15px;
+  min-height: 100vh;
+}
+a:link { text-decoration:none; color:blue; }
+a:visited { text-decoration:none; color:purple; }
+</style>
+</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="https://pentobi.sourceforge.io">Pentobi</a>. The interface is
+an adaption of the <a href="https://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=
+"https://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, nexos, nexos_2, gembloq,
+gembloq_2, gembloq_3, gembloq_2_4, callisto, callisto_2, callisto_3,
+callisto_2_4 or the abbreviations c, c2, d, t, t2, t3, j, n, n2, g, g2, g3,
+g24, ca, ca2, ca3, ca24. 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 disabled, 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 (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 8) is untested
+and might reduce the playing strength 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 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=
+"https://pentobi.sourceforge.io/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>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>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=
+"https://pentobi.sourceforge.io/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/help.qrc b/doc/help.qrc
new file mode 100644 (file)
index 0000000..61cdc15
--- /dev/null
@@ -0,0 +1,54 @@
+<!-- Help files for use in pentobi_qml -->
+<RCC>
+    <qresource prefix="/qml">
+        <file>help/C/pentobi/analysis.jpg</file>
+        <file>help/C/pentobi/become_stronger.html</file>
+        <file>help/C/pentobi/board_callisto.png</file>
+        <file>help/C/pentobi/board_classic.png</file>
+        <file>help/C/pentobi/board_duo.png</file>
+        <file>help/C/pentobi/board_gembloq.png</file>
+        <file>help/C/pentobi/board_nexos.png</file>
+        <file>help/C/pentobi/board_trigon.jpg</file>
+        <file>help/C/pentobi/callisto_rules.html</file>
+        <file>help/C/pentobi/classic_rules.html</file>
+        <file>help/C/pentobi/duo_rules.html</file>
+        <file>help/C/pentobi/gembloq_rules.html</file>
+        <file>help/C/pentobi/index.html</file>
+        <file>help/C/pentobi/junior_rules.html</file>
+        <file>help/C/pentobi/license.html</file>
+        <file>help/C/pentobi/nexos_rules.html</file>
+        <file>help/C/pentobi/pieces_callisto.png</file>
+        <file>help/C/pentobi/pieces_gembloq.jpg</file>
+        <file>help/C/pentobi/pieces_junior.png</file>
+        <file>help/C/pentobi/pieces_nexos.png</file>
+        <file>help/C/pentobi/pieces.png</file>
+        <file>help/C/pentobi/pieces_trigon.jpg</file>
+        <file>help/C/pentobi/position_callisto.png</file>
+        <file>help/C/pentobi/position_classic.png</file>
+        <file>help/C/pentobi/position_duo.png</file>
+        <file>help/C/pentobi/position_gembloq.png</file>
+        <file>help/C/pentobi/position_nexos.png</file>
+        <file>help/C/pentobi/position_trigon.jpg</file>
+        <file>help/C/pentobi/rating.jpg</file>
+        <file>help/C/pentobi/shortcuts.html</file>
+        <file>help/C/pentobi/stylesheet.css</file>
+        <file>help/C/pentobi/system.html</file>
+        <file>help/C/pentobi/trigon_rules.html</file>
+        <file>help/C/pentobi/user_interface.html</file>
+        <file>help/C/pentobi/window_menu.html</file>
+        <file>help/de/pentobi/become_stronger.html</file>
+        <file>help/de/pentobi/callisto_rules.html</file>
+        <file>help/de/pentobi/classic_rules.html</file>
+        <file>help/de/pentobi/duo_rules.html</file>
+        <file>help/de/pentobi/gembloq_rules.html</file>
+        <file>help/de/pentobi/index.html</file>
+        <file>help/de/pentobi/junior_rules.html</file>
+        <file>help/de/pentobi/license.html</file>
+        <file>help/de/pentobi/nexos_rules.html</file>
+        <file>help/de/pentobi/shortcuts.html</file>
+        <file>help/de/pentobi/system.html</file>
+        <file>help/de/pentobi/trigon_rules.html</file>
+        <file>help/de/pentobi/user_interface.html</file>
+        <file>help/de/pentobi/window_menu.html</file>
+    </qresource>
+</RCC>
diff --git a/doc/help/C/pentobi/analysis.jpg b/doc/help/C/pentobi/analysis.jpg
new file mode 100644 (file)
index 0000000..3abdcd9
Binary files /dev/null and b/doc/help/C/pentobi/analysis.jpg differ
diff --git a/doc/help/C/pentobi/become_stronger.html b/doc/help/C/pentobi/become_stronger.html
new file mode 100644 (file)
index 0000000..bc58961
--- /dev/null
@@ -0,0 +1,61 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="user_interface.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="window_menu.html">Next</a></div>
+<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>
+<div class="fig"><img src="analysis.jpg" alt=""></div>
+<div 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 800.</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 whether the
+game was won, lost or a tie. The exact number of score points does not
+matter.</p>
+<div class="fig"><img src="rating.jpg" alt=""></div>
+<div 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 50 games as a graph. The last 50 games are automatically
+saved and can be loaded by opening a context menu in the game table below the
+graph.</p>
+<div class="nav"><a href="user_interface.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="window_menu.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/board_callisto.png b/doc/help/C/pentobi/board_callisto.png
new file mode 100644 (file)
index 0000000..89a2fc7
Binary files /dev/null and b/doc/help/C/pentobi/board_callisto.png differ
diff --git a/doc/help/C/pentobi/board_classic.png b/doc/help/C/pentobi/board_classic.png
new file mode 100644 (file)
index 0000000..9d9a543
Binary files /dev/null and b/doc/help/C/pentobi/board_classic.png differ
diff --git a/doc/help/C/pentobi/board_duo.png b/doc/help/C/pentobi/board_duo.png
new file mode 100644 (file)
index 0000000..b1f5259
Binary files /dev/null and b/doc/help/C/pentobi/board_duo.png differ
diff --git a/doc/help/C/pentobi/board_gembloq.png b/doc/help/C/pentobi/board_gembloq.png
new file mode 100644 (file)
index 0000000..a79ea02
Binary files /dev/null and b/doc/help/C/pentobi/board_gembloq.png differ
diff --git a/doc/help/C/pentobi/board_nexos.png b/doc/help/C/pentobi/board_nexos.png
new file mode 100644 (file)
index 0000000..4fd32c8
Binary files /dev/null and b/doc/help/C/pentobi/board_nexos.png differ
diff --git a/doc/help/C/pentobi/board_trigon.jpg b/doc/help/C/pentobi/board_trigon.jpg
new file mode 100644 (file)
index 0000000..886514d
Binary files /dev/null and b/doc/help/C/pentobi/board_trigon.jpg differ
diff --git a/doc/help/C/pentobi/callisto_rules.html b/doc/help/C/pentobi/callisto_rules.html
new file mode 100644 (file)
index 0000000..22f2b5c
--- /dev/null
@@ -0,0 +1,43 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="gembloq_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="user_interface.html">Next</a></div>
+<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>
+<div class="fig"><img src="pieces_callisto.png" alt=""></div>
+<div 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>
+<div class="fig"><img src="board_callisto.png" alt=""></div>
+<div class="caption">The board with the center having a darker color</div>
+<p>All larger pieces may be placed anywhere on the board but must touch an
+existing piece of the same color edge-to-edge.</p>
+<div class="fig"><img src="position_callisto.png" alt=""></div>
+<div 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. In addition to the standard two-player
+variant, Pentobi also supports a game variant for two players like in Blokus,
+in which each player plays two colors.</p>
+<div class="nav"><a href="gembloq_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="user_interface.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/classic_rules.html b/doc/help/C/pentobi/classic_rules.html
new file mode 100644 (file)
index 0000000..c454db3
--- /dev/null
@@ -0,0 +1,57 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="index.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="duo_rules.html">Next</a></div>
+<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 from a number of squares
+connected along the edges.</p>
+<div class="fig"><img src="pieces.png" alt=""></div>
+<div class="caption">The 21 pieces</div>
+<p>The players alternate in placing one of their pieces on the board. The first
+piece of a player must cover its starting square. The starting squares are
+located in the corners of the board.</p>
+<div class="fig"><img src="board_classic.png" alt=""></div>
+<div class="caption">The 20×20 board with the starting 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>
+<div class="fig"><img src="position_classic.png" alt=""></div>
+<div 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>
+<div class="nav"><a href="index.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="duo_rules.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/duo_rules.html b/doc/help/C/pentobi/duo_rules.html
new file mode 100644 (file)
index 0000000..feadbd7
--- /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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="classic_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="trigon_rules.html">Next</a></div>
+<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 (Purple and Orange) and the starting squares are not in the corners, but
+on the square with the coordinates (5,10) for Purple, and on (10,5) for
+Orange.</p>
+<div class="fig"><img src="board_duo.png" alt=""></div>
+<div class="caption">The 14×14 board used in game variant Duo with the starting
+squares marked with colored dots</div>
+<div class="fig"><img src="position_duo.png" alt=""></div>
+<div class="caption">An example position in game variant Duo</div>
+<div class="nav"><a href="classic_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="trigon_rules.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/gembloq_rules.html b/doc/help/C/pentobi/gembloq_rules.html
new file mode 100644 (file)
index 0000000..df6fd3e
--- /dev/null
@@ -0,0 +1,43 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="nexos_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="callisto_rules.html">Next</a></div>
+<h2>GembloQ Rules</h2>
+<p>GembloQ is a board game similar to Blokus. The squares of the board are
+rotated by 45 degrees. The board has a diagonal size of 27 squares. In addition
+to the full squares, the edges also contain half squares such that the edges
+are straight lines.</p>
+<div class="fig"><img src="pieces_gembloq.jpg" alt=""></div>
+<div class="caption">The 21 pieces</div>
+<p>Each player has a set of 21 pieces, which include a subset of the pieces
+used in Blokus, but also some pieces that contain a half square.</p>
+<div class="fig"><img src="board_gembloq.png" alt=""></div>
+<div class="caption">The board for GembloQ with the starting points marked with
+colored dots</div>
+<p>As in Blokus, the starting squares are in the corners, the first move must
+fully cover the starting square of the color and subsequent moves must touch a
+piece of the player color at a vertex, but not edge-to-edge. Moves are also
+legal, if a vertex of a half square touches an edge of a piece of the same
+color.</p>
+<div class="fig"><img src="position_gembloq.png" alt=""></div>
+<div class="caption">An example position after a few moves</div>
+<p>Scoring is done like in Blokus, half squares count 0.5 points. Bonus points
+are not used. Tie breaking by assigning additional values to the pieces, as
+described in the official GembloQ rules, is currently not supported by
+Pentobi.</p>
+<h3>Rules for Two and Three Players</h3>
+<p>The game variants for two and three players use smaller boards and different
+starting point locations. In addition to the standard two-player variant,
+Pentobi also supports a game variant for two players like in Blokus, in which
+each player plays two colors.</p>
+<div class="nav"><a href="nexos_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="callisto_rules.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/index.html b/doc/help/C/pentobi/index.html
new file mode 100644 (file)
index 0000000..1b8d11e
--- /dev/null
@@ -0,0 +1,31 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="classic_rules.html">Next</a></div>
+<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, GembloQ 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="gembloq_rules.html">GembloQ 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">Window Menu and Toolbar</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>
+<div class="nav"><a href="classic_rules.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/junior_rules.html b/doc/help/C/pentobi/junior_rules.html
new file mode 100644 (file)
index 0000000..c55712a
--- /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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="trigon_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="nexos_rules.html">Next</a></div>
+<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 polyominoes
+up to size five and the players get two of each of those polyominoes.</p>
+<div class="fig"><img src="pieces_junior.png" alt=""></div>
+<div class="caption">The 24 pieces used in Junior</div>
+<p>Bonus points are not used in Junior.</p>
+<div class="nav"><a href="trigon_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="nexos_rules.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/license.html b/doc/help/C/pentobi/license.html
new file mode 100644 (file)
index 0000000..c96d9df
--- /dev/null
@@ -0,0 +1,29 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="system.html">Previous</a> | <a href=
+"index.html">Contents</a></div>
+<h2>License</h2>
+<p>Copyright © 2011–2018 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>
+<div class="nav"><a href="system.html">Previous</a> | <a href=
+"index.html">Contents</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/nexos_rules.html b/doc/help/C/pentobi/nexos_rules.html
new file mode 100644 (file)
index 0000000..5989590
--- /dev/null
@@ -0,0 +1,42 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="junior_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="gembloq_rules.html">Next</a></div>
+<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>
+<div class="fig"><img src="pieces_nexos.png" alt=""></div>
+<div 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>
+<div class="fig"><img src="board_nexos.png" alt=""></div>
+<div class="caption">The board for Nexos with the 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>
+<div class="fig"><img src="position_nexos.png" alt=""></div>
+<div 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>
+<div class="nav"><a href="junior_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="gembloq_rules.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/pieces.png b/doc/help/C/pentobi/pieces.png
new file mode 100644 (file)
index 0000000..0bce3bf
Binary files /dev/null and b/doc/help/C/pentobi/pieces.png differ
diff --git a/doc/help/C/pentobi/pieces_callisto.png b/doc/help/C/pentobi/pieces_callisto.png
new file mode 100644 (file)
index 0000000..2d5d01b
Binary files /dev/null and b/doc/help/C/pentobi/pieces_callisto.png differ
diff --git a/doc/help/C/pentobi/pieces_gembloq.jpg b/doc/help/C/pentobi/pieces_gembloq.jpg
new file mode 100644 (file)
index 0000000..522d440
Binary files /dev/null and b/doc/help/C/pentobi/pieces_gembloq.jpg differ
diff --git a/doc/help/C/pentobi/pieces_junior.png b/doc/help/C/pentobi/pieces_junior.png
new file mode 100644 (file)
index 0000000..724bd0e
Binary files /dev/null and b/doc/help/C/pentobi/pieces_junior.png differ
diff --git a/doc/help/C/pentobi/pieces_nexos.png b/doc/help/C/pentobi/pieces_nexos.png
new file mode 100644 (file)
index 0000000..228495f
Binary files /dev/null and b/doc/help/C/pentobi/pieces_nexos.png differ
diff --git a/doc/help/C/pentobi/pieces_trigon.jpg b/doc/help/C/pentobi/pieces_trigon.jpg
new file mode 100644 (file)
index 0000000..a22a5ef
Binary files /dev/null and b/doc/help/C/pentobi/pieces_trigon.jpg differ
diff --git a/doc/help/C/pentobi/position_callisto.png b/doc/help/C/pentobi/position_callisto.png
new file mode 100644 (file)
index 0000000..c4097ce
Binary files /dev/null and b/doc/help/C/pentobi/position_callisto.png differ
diff --git a/doc/help/C/pentobi/position_classic.png b/doc/help/C/pentobi/position_classic.png
new file mode 100644 (file)
index 0000000..4bdf32b
Binary files /dev/null and b/doc/help/C/pentobi/position_classic.png differ
diff --git a/doc/help/C/pentobi/position_duo.png b/doc/help/C/pentobi/position_duo.png
new file mode 100644 (file)
index 0000000..96e021f
Binary files /dev/null and b/doc/help/C/pentobi/position_duo.png differ
diff --git a/doc/help/C/pentobi/position_gembloq.png b/doc/help/C/pentobi/position_gembloq.png
new file mode 100644 (file)
index 0000000..768ed05
Binary files /dev/null and b/doc/help/C/pentobi/position_gembloq.png differ
diff --git a/doc/help/C/pentobi/position_nexos.png b/doc/help/C/pentobi/position_nexos.png
new file mode 100644 (file)
index 0000000..be1c5bc
Binary files /dev/null and b/doc/help/C/pentobi/position_nexos.png differ
diff --git a/doc/help/C/pentobi/position_trigon.jpg b/doc/help/C/pentobi/position_trigon.jpg
new file mode 100644 (file)
index 0000000..fa1dd82
Binary files /dev/null and b/doc/help/C/pentobi/position_trigon.jpg differ
diff --git a/doc/help/C/pentobi/rating.jpg b/doc/help/C/pentobi/rating.jpg
new file mode 100644 (file)
index 0000000..2bb05d4
Binary files /dev/null and b/doc/help/C/pentobi/rating.jpg differ
diff --git a/doc/help/C/pentobi/shortcuts.html b/doc/help/C/pentobi/shortcuts.html
new file mode 100644 (file)
index 0000000..589ffc4
--- /dev/null
@@ -0,0 +1,77 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="window_menu.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="system.html">Next</a></div>
+<h2>Keyboard Shortcuts</h2>
+<p>In addition to the 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 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>Escape</dt>
+<dd>
+<p>Clear selected piece</p>
+</dd>
+<dt>Left, Right, Up, Down, Shift+Left, Shift+Right, Shift+Up, Shift+Down</dt>
+<dd>
+<p>Move the selected piece. The Shift key makes the piece move faster.</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>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>
+<dt>Ctrl+Home, Ctrl+Shift+Left, Ctrl+Left, Ctrl+Right, Ctrl+Shift+Right,
+Ctrl+End, Ctrl+Up, Ctrl+Down</dt>
+<dd>
+<p>Navigate in the game: beginning, ten moves backward, backward, forward, ten
+moves forward, end, previous variation, next variation.</p>
+</dd>
+<dt>Ctrl+Shift+H</dt>
+<dd>
+<p>Like <i>Find Move</i> (Ctrl+H) but iterates backwards through the list of
+legal moves.</p>
+</dd>
+<dt>Ctrl+T</dt>
+<dd>
+<p>Switch view between comment and game analysis.</p>
+</dd>
+<dt>Alt+M</dt>
+<dd>
+<p>Open menu.</p>
+</dd>
+</dl>
+<div class="nav"><a href="window_menu.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="system.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/stylesheet.css b/doc/help/C/pentobi/stylesheet.css
new file mode 100644 (file)
index 0000000..9f1d52f
--- /dev/null
@@ -0,0 +1,51 @@
+html {
+background-color: #eee;
+}
+
+body {
+color: black;
+background-color: white;
+max-width: 60em;
+margin: auto;
+padding: 0.7em;
+min-height: 100vh;
+}
+
+:link {
+text-decoration: none;
+color: blue;
+}
+
+:visited {
+text-decoration: none;
+color: purple;
+}
+
+:focus {
+outline-color: darkorange;
+}
+
+.fig {
+text-align: center;
+}
+
+.fig img {
+width: auto;
+height: auto;
+max-width: 90%;
+max-height: 90%;
+margin: 0.5em;
+}
+
+.caption {
+font-size: 90%;
+text-align: center;
+margin-left: 15%;
+margin-right: 15%;
+}
+
+.nav {
+text-align: right;
+margin-top: 0.5em;
+margin-bottom: 0.5em;
+}
diff --git a/doc/help/C/pentobi/system.html b/doc/help/C/pentobi/system.html
new file mode 100644 (file)
index 0000000..1a5139c
--- /dev/null
@@ -0,0 +1,23 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="shortcuts.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="license.html">Next</a></div>
+<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.5&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>
+<div class="nav"><a href="shortcuts.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="license.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/trigon_rules.html b/doc/help/C/pentobi/trigon_rules.html
new file mode 100644 (file)
index 0000000..d04a3e4
--- /dev/null
@@ -0,0 +1,41 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="duo_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="junior_rules.html">Next</a></div>
+<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 from a number of equilateral triangles connected
+along the edges.</p>
+<div class="fig"><img src="pieces_trigon.jpg" alt=""></div>
+<div 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>
+<div class="fig"><img src="board_trigon.jpg" alt=""></div>
+<div class="caption">The board with the starting 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>
+<div class="fig"><img src="position_trigon.jpg" alt=""></div>
+<div 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>
+<div class="nav"><a href="duo_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="junior_rules.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/user_interface.html b/doc/help/C/pentobi/user_interface.html
new file mode 100644 (file)
index 0000000..b762b15
--- /dev/null
@@ -0,0 +1,59 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="callisto_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="become_stronger.html">Next</a></div>
+<h2>How to Use Pentobi</h2>
+<h3>Board</h3>
+<p>Pieces can be selected by clicking on one of the unplayed pieces or by using
+<a href="shortcuts.html">shortcut keys</a>. Pieces can be played by dragging
+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>
+<p>Played pieces on the board can have numbers on them that indicate the move
+number in which the piece was played. A letter after the move number indicates
+that there exists a variation to this move (see below).</p>
+<p>The score display shows 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>Settings</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. 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 and the navigation buttons.</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
+pressing the <i>Forward</i> button). 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>
+<div class="nav"><a href="callisto_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="become_stronger.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/C/pentobi/window_menu.html b/doc/help/C/pentobi/window_menu.html
new file mode 100644 (file)
index 0000000..35113c3
--- /dev/null
@@ -0,0 +1,222 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="become_stronger.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="shortcuts.html">Next</a></div>
+<h2>Window Menu and Toolbar</h2>
+<h3>Navigation Buttons in Toolbar</h3>
+<dl>
+<dt>Beginning</dt>
+<dd>Go to the beginning of the game.</dd>
+<dt>Backward 10</dt>
+<dd>Go ten moves backward in the current variation. The button supports
+autorepeat if pressed and held.</dd>
+<dt>Backward</dt>
+<dd>Go one move backward in the current variation. The button 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 button supports
+autorepeat if pressed and held.</dd>
+<dt>Forward 10</dt>
+<dd>Go ten moves forward in the current variation. If a 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 button 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>
+</dl>
+<h3>Game Menu</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>Open Clipboard</dt>
+<dd>Open a game from a text copied to the clipboard. The text must be a valid
+game in Pentobi SGF file format.</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). For a crisp image, the image width should be an integer
+multiple of the number of board columns in the current game variant.</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 Menu</h3>
+<dl>
+<dt>Move Number</dt>
+<dd>Go to the move with a given move number in the current variation.</dd>
+<dt>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>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 Menu</h3>
+<dl>
+<dt>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>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>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 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 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 Subtree</dt>
+<dd>Like <i>Keep 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 Menu</h3>
+<dl>
+<dt>Appearance</dt>
+<dd>
+<dl>
+<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>Move number</dt>
+<dd>This option exists only in desktop mode and shows the move number, move
+annotation and variation information at the right side of the status bar.</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>Show comment</dt>
+<dd>This option exists only in desktop mode and configures the visibility of
+the comment area when the position changes. By default, the comment area is
+only shown if a comment exists for the current position.</dd>
+</dl>
+</dd>
+<dt>Comment</dt>
+<dd>Toggle the visibility of the comment area in the current position.</dd>
+</dl>
+<dl>
+<dt>Fullscreen</dt>
+<dd>Make the main window full screen or leave full screen mode. To leave full
+screen mode without using the window menu, press the F11 key.</dd>
+</dl>
+<h3>Computer Menu</h3>
+<dl>
+<dt>Settings</dt>
+<dd>Select which colors are played by the computer and the playing strength for
+the computer. Higher levels are stronger but can make the computer take a long
+time for playing moves on slow computers.</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 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>
+</dl>
+<h3>Tools Menu</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 Menu</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>
+<div class="nav"><a href="become_stronger.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="shortcuts.html">Next</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/become_stronger.html b/doc/help/de/pentobi/become_stronger.html
new file mode 100644 (file)
index 0000000..de35692
--- /dev/null
@@ -0,0 +1,66 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="user_interface.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="window_menu.html">Weiter</a></div>
+<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>
+<div class="fig"><img src="../../C/pentobi/analysis.jpg" alt=""></div>
+<div 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 800.</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 nicht 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>
+<div class="fig"><img src="../../C/pentobi/rating.jpg" alt=""></div>
+<div 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 50 Spiele als Graph gezeigt wird. Die letzten 50
+Spiele werden automatisch gespeichert und können durch Öffnen eines
+Kontextmenüs in der Spieltabelle unter dem Graph geladen werden.</p>
+<div class="nav"><a href="user_interface.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="window_menu.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/callisto_rules.html b/doc/help/de/pentobi/callisto_rules.html
new file mode 100644 (file)
index 0000000..0329956
--- /dev/null
@@ -0,0 +1,48 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="gembloq_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="user_interface.html">Weiter</a></div>
+<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>
+<div class="fig"><img src="../../C/pentobi/pieces_callisto.png" alt=""></div>
+<div 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>
+<div class="fig"><img src="../../C/pentobi/board_callisto.png" alt=""></div>
+<div 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>
+<div class="fig"><img src="../../C/pentobi/position_callisto.png" alt=""></div>
+<div 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. Zusätzlich zur Standardvariante für zwei Spieler unterstützt
+Pentobi auch eine Spielvariante für zwei Spieler wie in Blokus, in der jeder
+Spieler zwei Farben spielt.</p>
+<div class="nav"><a href="gembloq_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="user_interface.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/classic_rules.html b/doc/help/de/pentobi/classic_rules.html
new file mode 100644 (file)
index 0000000..857849c
--- /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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="index.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="duo_rules.html">Weiter</a></div>
+<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>
+<div class="fig"><img src="../../C/pentobi/pieces.png" alt=""></div>
+<div class="caption">Die 21 Spielsteine</div>
+<p>Die Spieler setzen abwechselnd einen ihrer Spielsteine aufs Brett. Der erste
+Spielstein eines Spielers muss sein Startfeld abdecken. Die Startfelder
+befinden sich in den Ecken des Bretts.</p>
+<div class="fig"><img src="../../C/pentobi/board_classic.png" alt=""></div>
+<div class="caption">Das 20×20-Brett mit den durch farbige Punkte markierten
+Startfeldern</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>
+<div class="fig"><img src="../../C/pentobi/position_classic.png" alt=""></div>
+<div 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>
+<div class="nav"><a href="index.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="duo_rules.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/duo_rules.html b/doc/help/de/pentobi/duo_rules.html
new file mode 100644 (file)
index 0000000..36d2041
--- /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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="classic_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="trigon_rules.html">Weiter</a></div>
+<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 (Lila und Orange) und die Startfelder befinden sich nicht in
+den Ecken, sondern auf dem Feld mit den Koordinaten (5,10) für Lila und auf
+(10,5) für Orange.</p>
+<div class="fig"><img src="../../C/pentobi/board_duo.png" alt=""></div>
+<div class="caption">Das 14×14-Brett, das in der Spielvariante Duo benutzt
+wird, mit den durch farbige Punkte markierten Startfeldern</div>
+<div class="fig"><img src="../../C/pentobi/position_duo.png" alt=""></div>
+<div class="caption">Eine Beispielstellung in der Spielvariante Duo</div>
+<div class="nav"><a href="classic_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="trigon_rules.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/gembloq_rules.html b/doc/help/de/pentobi/gembloq_rules.html
new file mode 100644 (file)
index 0000000..c73d0ee
--- /dev/null
@@ -0,0 +1,45 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="nexos_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="callisto_rules.html">Weiter</a></div>
+<h2>GembloQ-Regeln</h2>
+<p>GembloQ ist ein Brettspiel ähnlich wie Blokus. Die quadratischen Spielfelder
+des Bretts sind um 45 Grad gedreht. Die Diagonale des Bretts hat eine Größe von
+27 Quadraten. Zusätzlich zu den vollen Quadraten enthalten die Ränder noch
+halbe Quadrate, so dass die Ränder gerade Linien sind.</p>
+<div class="fig"><img src="../../C/pentobi/pieces_gembloq.jpg" alt=""></div>
+<div class="caption">Die 21 Spielsteine</div>
+<p>Jeder Spieler besitzt 21 Spielsteine, unter denen eine Teilmenge der in
+Blokus benutzten Spielsteine ist, jedoch zusätzlich einige Spielsteine, die ein
+halbes Quadrat beinhalten.</p>
+<div class="fig"><img src="../../C/pentobi/board_gembloq.png" alt=""></div>
+<div class="caption">Das Brett für GembloQ mit den durch farbige Punkte
+markierten Startfeldern</div>
+<p>Wie in Blokus sind die Startfelder in den Ecken, der erste Zug muss das
+Startfeld der Farbe vollständig abdecken und folgende Züge müssen einen
+Spielstein der Farbe am Zug an einer Ecke berühren, jedoch noch entlang der
+Kanten. Züge sind auch legal, wenn eine Spitze eines halben Quadrats die Kante
+eines Spielsteins der selben Farbe berührt.</p>
+<div class="fig"><img src="../../C/pentobi/position_gembloq.png" alt=""></div>
+<div class="caption">Eine Beispielstellung nach ein paar Zügen</div>
+<p>Die Punktzahl am Spielende wird wie in Blokus ermittelt, wobei halbe
+Quadrate 0,5 Punkte zählen. Bonuspunkte werden nicht verwendet. Ein Auflösen
+von Unentschieden durch Zuweisen von zusätzlichen Werten zu den Spielsteinen,
+wie in den offiziellen GembloQ-Regeln beschrieben, wird von Pentobi gegenwärtig
+nicht unterstützt.</p>
+<h3>Regeln für zwei und drei Spieler</h3>
+<p>Die Spielvarianten für zwei und drei Spieler benutzen kleinere Spielbretter
+und andere Positionen für die Startfelder. Zusätzlich zur Standardvariante für
+zwei Spieler unterstützt Pentobi auch eine Spielvariante für zwei Spieler wie
+in Blokus, in der jeder Spieler zwei Farben spielt.</p>
+<div class="nav"><a href="nexos_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="callisto_rules.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/index.html b/doc/help/de/pentobi/index.html
new file mode 100644 (file)
index 0000000..19e4a35
--- /dev/null
@@ -0,0 +1,32 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="classic_rules.html">Weiter</a></div>
+<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, GembloQ 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="gembloq_rules.html">GembloQ-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">Fenstermenü und Werkzeugleiste</a><br>
+<a href="shortcuts.html">Tastenkürzel</a><br>
+<a href="system.html">Systemvoraussetzungen</a><br>
+<a href="license.html">Lizenz</a></p>
+<div class="nav"><a href="classic_rules.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/junior_rules.html b/doc/help/de/pentobi/junior_rules.html
new file mode 100644 (file)
index 0000000..b95897d
--- /dev/null
@@ -0,0 +1,23 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="trigon_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="nexos_rules.html">Weiter</a></div>
+<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 Polyominos bis zur Größe fünf und die Spieler bekommen zwei von
+jedem dieser Polyominos.</p>
+<div class="fig"><img src="../../C/pentobi/pieces_junior.png" alt=""></div>
+<div class="caption">Die 24 Spielsteine, die in Junior benutzt werden</div>
+<p>Bonuspunkte werden in Junior nicht benutzt.</p>
+<div class="nav"><a href="trigon_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="nexos_rules.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/license.html b/doc/help/de/pentobi/license.html
new file mode 100644 (file)
index 0000000..d154427
--- /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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="system.html">Zurück</a> | <a href=
+"index.html">Inhalt</a></div>
+<h2>Lizenz</h2>
+<p>Copyright © 2011–2018 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>
+<div class="nav"><a href="system.html">Zurück</a> | <a href=
+"index.html">Inhalt</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/nexos_rules.html b/doc/help/de/pentobi/nexos_rules.html
new file mode 100644 (file)
index 0000000..e9501f5
--- /dev/null
@@ -0,0 +1,43 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="junior_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="gembloq_rules.html">Weiter</a></div>
+<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>
+<div class="fig"><img src="../../C/pentobi/pieces_nexos.png" alt=""></div>
+<div 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>
+<div class="fig"><img src="../../C/pentobi/board_nexos.png" alt=""></div>
+<div class="caption">Das Brett für Nexos mit den durch farbige Punkte
+markierten Startkreuzungspunkten</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>
+<div class="fig"><img src="../../C/pentobi/position_nexos.png" alt=""></div>
+<div 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>
+<div class="nav"><a href="junior_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="gembloq_rules.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/shortcuts.html b/doc/help/de/pentobi/shortcuts.html
new file mode 100644 (file)
index 0000000..4e51174
--- /dev/null
@@ -0,0 +1,81 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="window_menu.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="system.html">Weiter</a></div>
+<h2>Tastenkürzel</h2>
+<p>Zusätzlich zu den Tastenkürzeln, 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 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>Escape</dt>
+<dd>
+<p>Spielsteinauswahl löschen</p>
+</dd>
+<dt>Links, Rechts, Oben, Unten, Umschalt+Links, Umschalt+Rechts, Umschalt+Oben,
+Umschalt+Unten</dt>
+<dd>
+<p>Bewegen des ausgewählten Spielsteins. Mit der Umschalttaste wird der
+Spielstein schneller bewegt.</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>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>
+<dt>Strg+Pos1, Strg+Umschalt+Links, Strg+Links, Strg+Rechts,
+Strg+Umschalt+Rechts, Strg+Ende, Strg+Oben, Strg+Unten</dt>
+<dd>
+<p>Im Spiel navigieren: Anfang, zehn Züge zurück, zurück, vorwärts, zehn Züge
+vorwärts, Ende, vorherige Variante, nächste Variante.</p>
+</dd>
+<dt>Strg+Umschalt+H</dt>
+<dd>
+<p>Wie <i>Zug finden</i> (Strg+H), jedoch wird rückwärts durch die Liste der
+legalen Züge iteriert.</p>
+</dd>
+<dt>Strg+T</dt>
+<dd>
+<p>Ansicht zwischen Kommentar und Spielanalyse umschalten.</p>
+</dd>
+<dt>Alt+M</dt>
+<dd>
+<p>Menü öffnen.</p>
+</dd>
+</dl>
+<div class="nav"><a href="window_menu.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="system.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/system.html b/doc/help/de/pentobi/system.html
new file mode 100644 (file)
index 0000000..3ebaf8a
--- /dev/null
@@ -0,0 +1,23 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="shortcuts.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="license.html">Weiter</a></div>
+<h2>Systemvoraussetzungen</h2>
+<p>Minimum: 1&nbsp;GB RAM, 1&nbsp;GHz CPU<br>
+Empfohlen für Spielstufe 9: 4&nbsp;GB RAM, 2,5&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>
+<div class="nav"><a href="shortcuts.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="license.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/trigon_rules.html b/doc/help/de/pentobi/trigon_rules.html
new file mode 100644 (file)
index 0000000..7cfb6a9
--- /dev/null
@@ -0,0 +1,44 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="duo_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="junior_rules.html">Weiter</a></div>
+<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>
+<div class="fig"><img src="../../C/pentobi/pieces_trigon.jpg" alt=""></div>
+<div 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>
+<div class="fig"><img src="../../C/pentobi/board_trigon.jpg" alt=""></div>
+<div class="caption">Das Brett mit den durch graue Punkte markierten
+Startfeldern</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>
+<div class="fig"><img src="../../C/pentobi/position_trigon.jpg" alt=""></div>
+<div 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>
+<div class="nav"><a href="duo_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="junior_rules.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/user_interface.html b/doc/help/de/pentobi/user_interface.html
new file mode 100644 (file)
index 0000000..96fd211
--- /dev/null
@@ -0,0 +1,69 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="callisto_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="become_stronger.html">Weiter</a></div>
+<h2>Wie Sie Pentobi benutzen</h2>
+<h3>Spielbrett</h3>
+<p>Spielsteine können durch Klicken auf einen ungespielten Spielstein
+ausgewählt werden oder durch Benutzen von <a href=
+"shortcuts.html">Tastenkürzeln</a>. 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>
+<p>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>Die Punkteanzeige zeigt die gegenwärtigen Punkte für jede Farbe oder jeden
+Spieler an. 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>Einstellungen</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 für ähnliche Aufgaben benutzen will. 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> und den Navigations-Buttons
+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
+den <i>Vorwärts</i>-Button drücken). 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>
+<div class="nav"><a href="callistorules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="become_stronger.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/help/de/pentobi/window_menu.html b/doc/help/de/pentobi/window_menu.html
new file mode 100644 (file)
index 0000000..eb6b3a5
--- /dev/null
@@ -0,0 +1,245 @@
+<!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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="become_stronger.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="shortcuts.html">Weiter</a></div>
+<h2>Fenstermenü und Werkzeugleiste</h2>
+<h3>Navigations-Buttons in der Werkzeugleiste</h3>
+<dl>
+<dt>Anfang</dt>
+<dd>Geht zum Anfang des Spiels.</dd>
+<dt>Zurück 10</dt>
+<dd>Geht zehn Züge in der gegenwärtigen Variante zurück. Der Button unterstützt
+automatische Wiederholung, wenn er gedrückt gehalten wird.</dd>
+<dt>Zurück</dt>
+<dd>Geht einen Zug in der gegenwärtigen Variante zurück. Der Button 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 Button unterstützt automatische Wiederholung, wenn er
+gedrückt gehalten wird.</dd>
+<dt>Vorwärts 10</dt>
+<dd>Geht zehn Züge in der gegenwärtigen Variante vorwärts. Wenn eine
+Brettstellung mehrerer nachfolgende Varianten hat (d.&nbsp;h. der gegenwärtige
+Knoten im Spielbaum mehrere Kindknoten hat), wird die erste Variante benutzt.
+Der Button 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>
+</dl>
+<h3>Spiel-Menü</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>Zwischenablage öffnen</dt>
+<dd>Öffnet ein Spiel von einem Text, der in die Zwischenablage kopiert wurde.
+Der Text muss ein gültiges Spiel im Pentobi-SGF-Dateiformat sein.</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). Für ein gestochen scharfes
+Bild sollte die Bildbreite ein ganzzahliges Vielfaches der Spaltenanzahl des
+Bretts in der gegenwärtigen Spielvariante sein.</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-Menü</h3>
+<dl>
+<dt>Zugnummer</dt>
+<dd>Geht zum Zug mit einer bestimmten Nummer in der gegenwärtigen
+Variante.</dd>
+<dt>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ächster Kommentar</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-Menü</h3>
+<dl>
+<dt>Annotierung</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</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</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>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>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>Teilbaum behalten</dt>
+<dd>Wie <i>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-Menü</h3>
+<dl>
+<dt>Erscheinungsbild</dt>
+<dd>
+<dl>
+<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>Zugnummer</dt>
+<dd>Diese Option existiert nur im Desktop-Modus und zeigt die Zugnummer,
+Zugannotierung und Varianteninformation auf der rechten Seite der Statusleiste
+an.</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>Kommentar zeigen</dt>
+<dt>Sichtbarkeit des Kommentarbereichs, wenn sich die Stellung ändert. Die</dt>
+<dd>Diese Option existiert nur im Desktop-Modus und konfiguriert die
+Standardeinstellung ist, dass der Kommentarbereich nur sichtbar ist, wenn ein
+Kommentar zur aktuellen Stellung existiert.</dd>
+</dl>
+</dd>
+<dt>Kommentar</dt>
+<dd>Mach den Kommentarbereich in der aktuellen Stellung sichtbar oder nicht
+sichtbar.</dd>
+<dt>Vollbild</dt>
+<dd>Schaltet das Hauptfenster in den Vollbildmodus oder verlässt den
+Vollbildmodus. Um den Vollbildmodus ohne das Benutzen des Fenstermenüs zu
+verlassen, drücken Sie die F11-Taste.</dd>
+</dl>
+<h3>Computer-Menü</h3>
+<dl>
+<dt>Einstellungen</dt>
+<dd>Wählt aus, welche Farben vom Computer gespielt werden und 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>
+<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>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>
+</dl>
+<h3>Extras-Menü</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-Menü</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>
+<div class="nav"><a href="become_stronger.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="shortcuts.html">Weiter</a></div>
+</body>
+</html>
diff --git a/doc/man/CMakeLists.txt b/doc/man/CMakeLists.txt
new file mode 100644 (file)
index 0000000..ec3a324
--- /dev/null
@@ -0,0 +1,11 @@
+configure_file(pentobi.6.in pentobi.6 @ONLY)
+install(FILES
+  ${CMAKE_CURRENT_BINARY_DIR}/pentobi.6
+  DESTINATION ${CMAKE_INSTALL_MANDIR}/man6)
+
+if(PENTOBI_BUILD_THUMBNAILER)
+    configure_file(pentobi-thumbnailer.6.in pentobi-thumbnailer.6 @ONLY)
+    install(FILES
+      ${CMAKE_CURRENT_BINARY_DIR}/pentobi-thumbnailer.6
+      DESTINATION ${CMAKE_INSTALL_MANDIR}/man6)
+endif()
diff --git a/doc/man/pentobi-thumbnailer.6.in b/doc/man/pentobi-thumbnailer.6.in
new file mode 100644 (file)
index 0000000..1d84ade
--- /dev/null
@@ -0,0 +1,38 @@
+.TH PENTOBI-THUMBNAILER 6 "2017-04-17" "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.
+.TP
+.B \-h, \-\-help
+Display help and exit.
+
+.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..0c774df
--- /dev/null
@@ -0,0 +1,76 @@
+.TH PENTOBI 6 "2018-07-30" "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.
+.PP
+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 \-h, \-\-help
+Display help and exit.
+.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 quarter
+of the physical memory available on the system).
+Reducing the maximum level to 8 currently reduces this amount by a factor
+of 3 to 4 and lower maximum levels even more.
+.TP
+.B \-\-mobile
+Use a window layout optimized for smartphones and apply some user interface
+changes that assume that a touchscreen is the main input device. If this option
+is not used, the default layout depends on the platform. Using this option also
+changes the default style for GUI elements of QQuickControls 2 to Default if
+the style is not explicitly set with option \-style.
+.TP
+.B \-\-nobook
+Do not use opening books.
+.TP
+.B \-\-nodelay
+Do not delay fast computer moves. By default, the computer player adds a
+small delay if the move generation took less than a second to make it easier
+for the human to follow the game if the computer plays several moves in a row.
+.TP
+.B \-\-seed
+Set the seed for the random generator. Using a fixed seed makes the move
+generation deterministic if no multi-threading is used (see option --threads).
+.TP
+.B \-\-threads
+The number of threads to use in the search. By default, up to 8 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.
+.PP
+Standard options for Qt applications:
+.TP
+.B \-display
+Switches displays on X11.
+.TP
+.B \-geometry
+Window geometry using the X11 syntax.
+.TP
+.B \-style
+Set the style for the GUI elements of QQuickControls 2. If no style is chosen,
+the default style is Default if option \-\-mobile is set and Fusion otherwise.
+.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..a343873
--- /dev/null
@@ -0,0 +1,38 @@
+add_subdirectory(libboardgame_sys)
+add_subdirectory(libboardgame_util)
+add_subdirectory(libboardgame_sgf)
+add_subdirectory(libboardgame_base)
+add_subdirectory(libpentobi_base)
+if(PENTOBI_BUILD_GUI OR PENTOBI_BUILD_GTP)
+    add_subdirectory(libboardgame_mcts)
+    add_subdirectory(libpentobi_mcts)
+endif()
+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()
+    add_subdirectory(learn_tool)
+endif()
+if(PENTOBI_BUILD_GUI OR PENTOBI_BUILD_THUMBNAILER)
+    add_subdirectory(libpentobi_paint)
+endif()
+if(PENTOBI_BUILD_GUI)
+    add_subdirectory(convert)
+    add_subdirectory(pentobi)
+endif()
+if(PENTOBI_BUILD_TESTS)
+    add_subdirectory(libboardgame_test)
+    add_subdirectory(unittest)
+endif()
+if(PENTOBI_BUILD_THUMBNAILER)
+    add_subdirectory(libpentobi_thumbnail)
+    add_subdirectory(pentobi_thumbnailer)
+endif()
+if(PENTOBI_BUILD_KDE_THUMBNAILER)
+    add_subdirectory(libpentobi_kde_thumbnailer)
+    add_subdirectory(pentobi_kde_thumbnailer)
+endif()
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..ccddbbf
--- /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]TE[1]
+ )
+)
+)
diff --git a/src/books/book_callisto_2_4.blksgf b/src/books/book_callisto_2_4.blksgf
new file mode 100644 (file)
index 0000000..4a1a478
--- /dev/null
@@ -0,0 +1,12 @@
+(
+;GM[Callisto Two-Player Four-Color]CA[UTF-8]
+(
+ ;1[h12]TE[1]
+)
+(
+ ;1[g11]TE[1]
+)
+(
+ ;1[h13]TE[1]
+)
+)
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..f31b5bc
--- /dev/null
@@ -0,0 +1,225 @@
+(
+;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]
+  )
+ )
+ (
+  ;W[j5,i6,j6,k6,j7]TE[1]
+ )
+ (
+  ;W[j5,h6,i6,j6,i7]TE[1]
+  ;B[g6,g7,h7,h8,i8]TE[1]
+ )
+ (
+  ;W[j5,i6,j6,h7,i7]
+  ;B[g4,f5,g5,g6,g7]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_gembloq.blksgf b/src/books/book_gembloq.blksgf
new file mode 100644 (file)
index 0000000..c32e5d6
--- /dev/null
@@ -0,0 +1,7 @@
+(
+;GM[GembloQ]CA[UTF-8]
+;1[j23,k23,h24,i24,j24,k24,f25,g25,h25,i25,d26,e26,f26,g26,b27,c27,d27,e27,b28,c28]TE[1]
+;2[at23,au23,at24,au24,av24,aw24,av25,aw25,ax25,ay25,ax26,ay26,az26,ba26,az27,ba27,bb27,bc27,bb28,bc28]TE[1]
+;3[bb1,bc1,az2,ba2,bb2,bc2,ax3,ay3,az3,ba3,av4,aw4,ax4,ay4,at5,au5,av5,aw5,at6,au6]TE[1]
+;4[b1,c1,b2,c2,d2,e2,d3,e3,f3,g3,f4,g4,h4,i4,h5,i5,j5,k5,j6,k6]TE[1]
+)
diff --git a/src/books/book_gembloq_2.blksgf b/src/books/book_gembloq_2.blksgf
new file mode 100644 (file)
index 0000000..7b63df6
--- /dev/null
@@ -0,0 +1,4 @@
+(
+;GM[GembloQ Two-Player]CA[UTF-8]
+;B[v17,w17,t18,u18,v18,w18,r19,s19,t19,u19,p20,q20,r20,s20,n21,o21,p21,q21,n22,o22]TE[1]
+)
diff --git a/src/books/book_gembloq_2_4.blksgf b/src/books/book_gembloq_2_4.blksgf
new file mode 100644 (file)
index 0000000..ba9e1b2
--- /dev/null
@@ -0,0 +1,7 @@
+(
+;GM[GembloQ Two-Player Four-Color]CA[UTF-8]
+;1[j23,k23,h24,i24,j24,k24,f25,g25,h25,i25,d26,e26,f26,g26,b27,c27,d27,e27,b28,c28]TE[1]
+;2[at23,au23,at24,au24,av24,aw24,av25,aw25,ax25,ay25,ax26,ay26,az26,ba26,az27,ba27,bb27,bc27,bb28,bc28]TE[1]
+;3[bb1,bc1,az2,ba2,bb2,bc2,ax3,ay3,az3,ba3,av4,aw4,ax4,ay4,at5,au5,av5,aw5,at6,au6]TE[1]
+;4[b1,c1,b2,c2,d2,e2,d3,e3,f3,g3,f4,g4,h4,i4,h5,i5,j5,k5,j6,k6]TE[1]
+)
diff --git a/src/books/book_gembloq_3.blksgf b/src/books/book_gembloq_3.blksgf
new file mode 100644 (file)
index 0000000..63ddfac
--- /dev/null
@@ -0,0 +1,4 @@
+(
+;GM[GembloQ Three-Player]CA[UTF-8]
+;1[z1,aa1,x2,y2,z2,aa2,v3,w3,x3,y3,t4,u4,v4,w4,t5,u5,v5,w5,v6,w6]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..aa1545f
--- /dev/null
@@ -0,0 +1,39 @@
+(
+;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]
+  ;3[j7,k7,l7,m7,m8,n8]TE[1]
+  (
+   ;4[v11,w11,w12,x12,y12,z12]TE[1]
+  )
+  (
+   ;4[w10,x10,x11,y11,y12,z12]
+   ;1[n9,o9,o10,p10,p11,q11]
+   (
+    ;2[j6,k6,l6,m6,n6,n7]TE[1]
+   )
+   (
+    ;2[k5,j6,k6,l6,m6,n6]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..eb5fd22
--- /dev/null
@@ -0,0 +1,22 @@
+<RCC>
+    <qresource prefix="/pentobi_books">
+        <file>book_callisto.blksgf</file>
+        <file>book_callisto_2.blksgf</file>
+        <file>book_callisto_2_4.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_gembloq.blksgf</file>
+        <file>book_gembloq_2.blksgf</file>
+        <file>book_gembloq_2_4.blksgf</file>
+        <file>book_gembloq_3.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..7e05d01
--- /dev/null
@@ -0,0 +1,5 @@
+find_package(Qt5Gui REQUIRED)
+
+add_executable(convert Main.cpp)
+
+target_link_libraries(convert Qt5::Gui)
diff --git a/src/convert/Main.cpp b/src/convert/Main.cpp
new file mode 100644 (file)
index 0000000..ddf2b10
--- /dev/null
@@ -0,0 +1,41 @@
+//-----------------------------------------------------------------------------
+/** @file convert/Main.cpp
+    Converts images using the Qt library.
+    Used for creating PNG icons from the SVG sources at build time.
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include <iostream>
+#include <QCoreApplication>
+#include <QImageReader>
+#include <QImageWriter>
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char* argv[])
+{
+    QCoreApplication app(argc, argv);
+    try
+    {
+        if (argc != 3)
+            throw QStringLiteral("Need two arguments");
+        auto in = QString::fromLocal8Bit(argv[1]);
+        auto out = QString::fromLocal8Bit(argv[2]);
+        QImageReader reader(in);
+        QImage image = reader.read();
+        if (image.isNull())
+            throw QStringLiteral("%1: %2").arg(in, reader.errorString());
+        QImageWriter writer(out);
+        if (! writer.write(image))
+            throw QStringLiteral("%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..65e1bd0
--- /dev/null
@@ -0,0 +1,80 @@
+/**
+
+@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_alphago_2016  Mastering the game of Go with deep neural networks and tree search.
+D. Silver, A. Huang, et al. Nature 529 (7587), pp. 484-489, 2016.
+<a href="https://storage.googleapis.com/deepmind-media/alphago/AlphaGoNaturePaper.pdf">(PDF)</a>
+
+@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..99dc581
--- /dev/null
@@ -0,0 +1,60 @@
+/** @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)
+    - learn_tool -
+      Tool for learning the weights used for move priors in the
+      Monte-Carlo tree search in libpentobi_mcts
+
+    @section mainpage_gui Pentobi GUI Modules
+
+    The Pentobi GUI modules implement a user interface based on
+    <a href="https://www.qt.io/">Qt</a>/QtQuick.
+    They are used for the desktop versions of Pentobi.
+
+    - convert -
+      Small helper program to convert SVG icons to bitmaps at build time
+    - pentobi -
+      Main program that provides a GUI for the player in libpentobi_mcts
+    - 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
+*/
diff --git a/src/icon/pentobi-16.svg b/src/icon/pentobi-16.svg
new file mode 100644 (file)
index 0000000..1aab930
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m8e-4 12-8e-4 2.643c-0.00204 0.75 0.60132 1.353 1.35 1.357h2.65v-3.998l-2.6648-2e-3z" fill="#edd400" stroke-width=".4"/>
+ <g id="f" transform="matrix(.4 0 0 .4 -1.6 -1.6)">
+  <rect x="24" y="4" width="10" height="10" fill="#73d216"/>
+  <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+  <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use transform="translate(-6,-6)" x="10" y="10" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(-6,-12)" x="10" y="20" width="100%" height="100%" xlink:href="#f"/>
+ <g id="e" transform="matrix(.4 0 0 .4 -1.6 -1.6)">
+  <rect x="14" y="24" width="10" height="10" fill="#3465a4"/>
+  <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+  <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use transform="translate(-6)" x="10" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(-6,-6)" x="10" y="10" width="100%" height="100%" xlink:href="#e"/>
+ <g id="d" transform="matrix(.4 0 0 .4 -1.6 -1.6)">
+  <rect x="4" y="14" width="10" height="10" fill="#edd400"/>
+  <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+  <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use transform="translate(4,-2)" y="10" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(-10 -16)" x="10" y="20" width="100%" height="100%" xlink:href="#d"/>
+ <g id="b" transform="matrix(.4 0 0 .4 2.4 -1.6)">
+  <rect x="4" y="4" width="10" height="10" fill="#c00"/>
+  <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(.4 0 0 .4 9.282 -3.388)">
+  <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+  <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 2e-3 2e-3z" fill="#ef2929"/>
+ </g>
+ <path d="m3.9984 11.997-0.39844 0.39844v3.2h-3.2l-0.00392 0.0039c0.24394 0.24347 0.58036 0.39396 0.95312 0.39609h2.6508v-3.9984h-0.00156z" fill="#c4a000" stroke-width=".4"/>
+ <path d="m8e-4 11.996-8e-4 2.643c-0.00104 0.376 0.15036 0.715 0.39608 0.961l0.00392-4e-3v-3.2h3.2l0.39843-0.39844-2.6632-0.0016h-1.3344z" fill="#fce94f" stroke-width=".4"/>
+ <g transform="matrix(.4 0 0 .4 -14.01 1.2562)">
+  <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+  <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+  <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(.4 0 0 .4 -9.9996 -10.6)">
+  <path d="m54.999 26.5v9.9961l6.664 4e-3h3.336v-6.627c-6e-3 -1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+  <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-3.55e-4 -3.55e-4 -0.0016 3.55e-4 -2e-3 0z" fill="#4e9a06"/>
+  <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" transform="translate(4,4)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(-4)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
diff --git a/src/icon/pentobi-32.svg b/src/icon/pentobi-32.svg
new file mode 100644 (file)
index 0000000..01f632a
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32" height="32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m2.0014 23-0.0014 4.6248c-0.00357 1.3125 1.0523 2.3674 2.3625 2.3751h4.6375v-6.9971l-4.6635-0.0028z" fill="#edd400" stroke-width=".69999"/>
+ <g id="f" transform="matrix(.7 0 0 .69999 -.79998 -.79993)">
+  <rect x="24" y="4" width="10" height="10" fill="#73d216"/>
+  <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+  <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use transform="translate(-3 -2.9999)" x="10" y="10" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(-3 -5.9999)" x="10" y="20" width="100%" height="100%" xlink:href="#f"/>
+ <g id="e" transform="matrix(.7 0 0 .69999 -.79998 -.79993)">
+  <rect x="14" y="24" width="10" height="10" fill="#3465a4"/>
+  <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+  <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use transform="translate(-3 .00017)" x="10" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(-3 -2.9997)" x="10" y="10" width="100%" height="100%" xlink:href="#e"/>
+ <g id="d" transform="matrix(.7 0 0 .69999 -.79998 -.79993)">
+  <rect x="4" y="14" width="10" height="10" fill="#edd400"/>
+  <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+  <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use transform="translate(-2e-5 -2.9998)" y="10" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(-3 -5.9998)" x="10" y="20" width="100%" height="100%" xlink:href="#d"/>
+ <g id="b" transform="matrix(.7 0 0 .69999 6.2 -.79993)">
+  <rect x="4" y="4" width="10" height="10" fill="#c00"/>
+  <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(.7 0 0 .69999 18.244 -3.9289)">
+  <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+  <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 2e-3 2e-3z" fill="#ef2929"/>
+ </g>
+ <path d="m8.9972 22.995-0.69726 0.69725v5.5999h-5.6l-0.00686 0.0069c0.4269 0.42607 1.0156 0.68941 1.668 0.69315h4.6389v-6.9972h-0.00273z" fill="#c4a000" stroke-width=".69999"/>
+ <path d="m2.0014 22.993-0.0014 4.6248c-0.00182 0.65869 0.26313 1.2523 0.69314 1.6821l0.00686-0.0063v-5.5999h5.5999l0.69726-0.69725-4.6607-0.0027h-2.3351z" fill="#fce94f" stroke-width=".69999"/>
+ <g transform="matrix(.7 0 0 .69999 -22.517 4.1983)">
+  <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+  <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+  <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(.7 0 0 .69999 -15.499 -16.55)">
+  <path d="m54.999 26.5v9.9961l6.664 4e-3h3.336v-6.627c-6e-3 -1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+  <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-3.55e-4 -3.55e-4 -0.0016 3.55e-4 -2e-3 0z" fill="#4e9a06"/>
+  <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" transform="translate(7 7)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(-7 1e-4)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
diff --git a/src/icon/pentobi-64.svg b/src/icon/pentobi-64.svg
new file mode 100644 (file)
index 0000000..1bc65fa
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="64" height="64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m6.0026 45-0.0026 8.5891c-0.00663 2.4375 1.9543 4.3966 4.3875 4.4109h8.6125v-12.995l-8.6607-0.0052z" fill="#edd400" stroke-width="1.3"/>
+ <g id="f" transform="matrix(1.3 0 0 1.3 .80003 .80012)">
+  <rect x="24" y="4" width="10" height="10" fill="#73d216"/>
+  <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+  <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use transform="translate(3 2.9999)" x="10" y="10" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(3 5.9999)" x="10" y="20" width="100%" height="100%" xlink:href="#f"/>
+ <g id="e" transform="matrix(1.3 0 0 1.3 .80003 .80012)">
+  <rect x="14" y="24" width="10" height="10" fill="#3465a4"/>
+  <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+  <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use transform="translate(3 -.00012)" x="10" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(3 2.9999)" x="10" y="10" width="100%" height="100%" xlink:href="#e"/>
+ <g id="d" transform="matrix(1.3 0 0 1.3 .80003 .80012)">
+  <rect x="4" y="14" width="10" height="10" fill="#edd400"/>
+  <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+  <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use transform="translate(-3e-5 2.9999)" y="10" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(3 5.9999)" x="10" y="20" width="100%" height="100%" xlink:href="#d"/>
+ <g id="b" transform="matrix(1.3 0 0 1.3 13.8 .80012)">
+  <rect x="4" y="4" width="10" height="10" fill="#c00"/>
+  <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(1.3 0 0 1.3 36.167 -5.0109)">
+  <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+  <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 2e-3 2e-3z" fill="#ef2929"/>
+ </g>
+ <path d="m18.995 44.991-1.2949 1.2949v10.4h-10.4l-0.01274 0.01274c0.79282 0.79128 1.8862 1.2804 3.0976 1.2873h8.6151v-12.995h-0.0051z" fill="#c4a000" stroke-width="1.3"/>
+ <path d="m6.0026 44.987-0.0026 8.5891c-0.00338 1.2233 0.48867 2.3257 1.2873 3.1239l0.01274-0.0117v-10.4h10.4l1.2949-1.2949-8.6555-0.0051h-4.3367z" fill="#fce94f" stroke-width="1.3"/>
+ <g transform="matrix(1.3 0 0 1.3 -39.532 10.083)">
+  <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+  <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+  <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(1.3 0 0 1.3 -26.499 -28.45)">
+  <path d="m54.999 26.5v9.9961l6.664 4e-3h3.336v-6.627c-6e-3 -1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+  <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-3.55e-4 -3.55e-4 -0.0016 3.55e-4 -2e-3 0z" fill="#4e9a06"/>
+  <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" transform="translate(13 13)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(-13)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
diff --git a/src/icon/pentobi.svg b/src/icon/pentobi.svg
new file mode 100644 (file)
index 0000000..a5b8b0e
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="48" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m4.002 34-2e-3 6.6074c-0.0051 1.8742 1.5033 3.3818 3.375 3.3926h6.625v-9.9961l-6.6621-0.0039h-3.3359z" fill="#edd400"/>
+ <g id="a">
+  <rect x="24" y="4" width="10" height="10" fill="#73d216"/>
+  <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+  <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use x="10" y="10" xlink:href="#a"/>
+ <use x="10" y="20" xlink:href="#a"/>
+ <g id="b">
+  <rect x="14" y="24" width="10" height="10" fill="#3465a4"/>
+  <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+  <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use x="10" xlink:href="#b"/>
+ <use x="10" y="10" xlink:href="#b"/>
+ <g id="c">
+  <rect x="4" y="14" width="10" height="10" fill="#edd400"/>
+  <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+  <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use y="10" xlink:href="#c"/>
+ <use x="10" y="20" xlink:href="#c"/>
+ <g id="d" transform="translate(10)">
+  <rect x="4" y="4" width="10" height="10" fill="#c00"/>
+  <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="translate(27.205 -4.47)">
+  <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.3359h-6.627z" fill="#c00"/>
+  <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+  <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 2e-3 2e-3 -2e-3 -2e-3z" fill="#ef2929"/>
+ </g>
+ <path d="m13.996 33.993-0.99609 0.99609v8h-8l-0.0098 0.0098c0.60986 0.60868 1.4509 0.98489 2.3828 0.99023h6.627v-9.9961h-0.0039z" fill="#c4a000"/>
+ <path d="m4.002 33.99-2e-3 6.6074c-0.0026 0.941 0.3759 1.789 0.9902 2.403l0.0098-0.0098v-8h7.9999l0.99608-0.99609-6.6581-0.0039h-3.3359z" fill="#fce94f"/>
+ <g transform="translate(-31.025 7.1404)">
+  <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.6465h-9.9941z" fill="#3465a4"/>
+  <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373v-6.627z" fill="#204a87"/>
+  <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="translate(-20.999 -22.5)">
+  <path d="m54.999 26.5v9.9961l6.6641 0.0039h3.3359v-6.627c-0.0057-1.8649-1.5081-3.3624-3.373-3.373h-6.627z" fill="#73d216"/>
+  <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-3.55e-4 -3.55e-4 -0.0016 3.55e-4 -2e-3 0z" fill="#4e9a06"/>
+  <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use transform="translate(0,10)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(10,10)" width="100%" height="100%" xlink:href="#d"/>
+</svg>
diff --git a/src/icon/pentobi_icon.qrc b/src/icon/pentobi_icon.qrc
new file mode 100644 (file)
index 0000000..35af691
--- /dev/null
@@ -0,0 +1,5 @@
+<RCC>
+<qresource prefix="/pentobi_icon">
+<file>pentobi-64.svg</file>
+</qresource>
+</RCC>
diff --git a/src/icon/pentobi_icon_desktop.qrc b/src/icon/pentobi_icon_desktop.qrc
new file mode 100644 (file)
index 0000000..af173be
--- /dev/null
@@ -0,0 +1,7 @@
+<RCC>
+<qresource prefix="/pentobi_icon">
+<file>pentobi-16.svg</file>
+<file>pentobi-32.svg</file>
+<file>pentobi.svg</file>
+</qresource>
+</RCC>
diff --git a/src/learn_tool/CMakeLists.txt b/src/learn_tool/CMakeLists.txt
new file mode 100644 (file)
index 0000000..dc9455c
--- /dev/null
@@ -0,0 +1,12 @@
+find_package(Threads)
+
+add_executable(learn-tool Main.cpp)
+
+target_link_libraries(learn-tool
+  pentobi_mcts
+  pentobi_base
+  boardgame_base
+  boardgame_sgf
+  boardgame_util
+  Threads::Threads
+)
diff --git a/src/learn_tool/Main.cpp b/src/learn_tool/Main.cpp
new file mode 100644 (file)
index 0000000..9cdca87
--- /dev/null
@@ -0,0 +1,522 @@
+//-----------------------------------------------------------------------------
+/** @file learn_tool/Main.cpp
+    Learn the parameters used in libpentobi_mcts/PriorKnowledge from existing
+    games with softmax training.
+
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include <fstream>
+#include <random>
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_util/FmtSaver.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/Options.h"
+#include "libpentobi_base/Game.h"
+#include "libpentobi_base/MoveMarker.h"
+#include "libpentobi_mcts/LocalPoints.h"
+
+using namespace std;
+using libboardgame_sgf::TreeReader;
+using libboardgame_util::FmtSaver;
+using libboardgame_util::Options;
+using libboardgame_util::split;
+using libpentobi_base::Board;
+using libpentobi_base::BoardConst;
+using libpentobi_base::Color;
+using libpentobi_base::Game;
+using libpentobi_base::Geometry;
+using libpentobi_base::GridExt;
+using libpentobi_base::Move;
+using libpentobi_base::MoveList;
+using libpentobi_base::MoveMarker;
+using libpentobi_base::PointList;
+using libpentobi_base::Variant;
+using libpentobi_mcts::LocalPoints;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+/** Features, see PriorKnowledge::m_gamma_... */
+enum {
+    point_other,
+    point_opp_attach_or_nb,
+    point_second_color_attach,
+    adj_connect,
+    adj_occupied_other,
+    adj_forbidden_other,
+    adj_own_attach,
+    adj_nonforbidden,
+    attach_to_play,
+    attach_forbidden_other,
+    attach_nonforbidden_0,
+    attach_nonforbidden_1,
+    attach_nonforbidden_2,
+    attach_nonforbidden_3,
+    attach_nonforbidden_4,
+    attach_nonforbidden_5,
+    attach_nonforbidden_6,
+    attach_second_color,
+    local_move,
+    piece_score_0,
+    piece_score_1,
+    piece_score_2,
+    piece_score_3,
+    piece_score_4,
+    piece_score_5,
+    piece_score_6,
+    _nu_features
+};
+
+struct Features
+{
+    using IntType = uint_least8_t;
+
+
+    array<IntType, _nu_features> feature;
+
+
+    Features() { feature.fill(0); }
+
+    void operator+=(const Features& f)
+    {
+        for (unsigned i = 0; i < _nu_features; ++i)
+             feature[i] = static_cast<IntType>(feature[i] + f.feature[i]);
+    }
+
+    void operator|=(const Features& f)
+    {
+        for (unsigned i = 0; i < _nu_features; ++i)
+             feature[i] = feature[i] | f.feature[i];
+    }
+};
+
+struct Sample
+{
+    unsigned played_move;
+
+    vector<Features> features;
+};
+
+
+using Float = double;
+
+const Float step_size = 0.05;
+
+MoveMarker marker;
+
+MoveList moves;
+
+MoveList tmp_moves;
+
+long nu_games;
+
+long nu_positions;
+
+long nu_moves;
+
+random_device rand_dev;
+
+mt19937 rand_gen(rand_dev());
+
+vector<Float> probs;
+
+array<Float, _nu_features> weights;
+
+array<Float, _nu_features> grad_weights;
+
+GridExt<Features> feature_grid_point;
+
+GridExt<Features> feature_grid_adj;
+
+GridExt<Features> feature_grid_attach;
+
+vector<Sample> samples;
+
+LocalPoints local_points;
+
+Features feature_occured_globally;
+
+
+/** This function mirrors what is happening in PriorKnowledge::gen_children,
+    but produces feature vectors instead of a gamma value for each move. */
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+void add_sample(const Board& bd, Color to_play, Move played_mv)
+{
+    marker.clear();
+    bd.gen_moves(to_play, marker, moves);
+    nu_moves += moves.size();
+
+    local_points.init<MAX_SIZE, MAX_ADJ_ATTACH>(bd);
+    auto& geo = bd.get_geometry();
+    auto variant = bd.get_variant();
+    auto& is_forbidden = bd.is_forbidden(to_play);
+    Color second_color;
+    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;
+    }
+    for (auto p : geo)
+    {
+        feature_grid_point[p] = Features();
+        feature_grid_adj[p] = Features();
+        feature_grid_attach[p] = Features();
+    }
+    for (auto p : geo)
+    {
+        auto& feature_point = feature_grid_point[p].feature;
+        auto& feature_adj = feature_grid_adj[p].feature;
+        auto& feature_attach = feature_grid_attach[p].feature;
+        auto s = bd.get_point_state(p);
+        if (is_forbidden[p])
+        {
+            if (s != to_play)
+                feature_attach[attach_forbidden_other] = 1;
+            else
+                feature_attach[attach_to_play] = 1;
+            if (s == connect_color)
+                feature_adj[adj_connect] = 1;
+            else if (! s.is_empty())
+                feature_adj[adj_occupied_other] = 1;
+            else
+                feature_adj[adj_forbidden_other] = 1;
+        }
+        else
+        {
+            feature_point[point_other] = 1;
+            if (bd.is_attach_point(p, to_play))
+                feature_adj[adj_own_attach] = 1;
+            else
+                feature_adj[adj_nonforbidden] = 1;
+            unsigned n = 0;
+            if (MAX_SIZE == 7 || IS_CALLISTO)
+            {
+                LIBBOARDGAME_ASSERT(geo.get_adj(p).empty());
+                for (auto pa : geo.get_diag(p))
+                    n += 1u - static_cast<unsigned>(is_forbidden[pa]);
+            }
+            else
+                for (auto pa : geo.get_adj(p))
+                    n += 1u - static_cast<unsigned>(is_forbidden[pa]);
+            switch (n)
+            {
+            case 0: feature_attach[attach_nonforbidden_0] = 1; break;
+            case 1: feature_attach[attach_nonforbidden_1] = 1; break;
+            case 2: feature_attach[attach_nonforbidden_2] = 1; break;
+            case 3: feature_attach[attach_nonforbidden_3] = 1; break;
+            case 4: feature_attach[attach_nonforbidden_4] = 1; break;
+            case 5: feature_attach[attach_nonforbidden_5] = 1; break;
+            default: feature_attach[attach_nonforbidden_6] = 1; break;
+            }
+        }
+    }
+    for (Color c : bd.get_colors())
+    {
+        if (c == to_play || c == second_color)
+            continue;
+        auto& is_forbidden = bd.is_forbidden(c);
+        for (auto p : bd.get_attach_points(c))
+            if (! is_forbidden[p])
+            {
+                feature_grid_point[p].feature[point_other] = 0;
+                feature_grid_point[p].feature[point_opp_attach_or_nb] = 1;
+                for (auto j : geo.get_adj(p))
+                    if (! is_forbidden[j])
+                    {
+                        feature_grid_point[j].feature[point_other] = 0;
+                        feature_grid_point[j].feature[point_opp_attach_or_nb] = 1;
+                    }
+            }
+    }
+    if (second_color != to_play)
+    {
+        auto& is_forbidden_second_color = bd.is_forbidden(second_color);
+        for (auto p : bd.get_attach_points(second_color))
+            if (! is_forbidden_second_color[p])
+            {
+                feature_grid_point[p].feature[point_second_color_attach] = 1;
+                if (! is_forbidden[p])
+                    feature_grid_attach[p].feature[attach_second_color] = 1;
+            }
+    }
+
+    Sample sample;
+    sample.played_move = moves.size() + 1;
+    sample.features.reserve(moves.size());
+    auto& bc = bd.get_board_const();
+    auto move_info_array = bc.get_move_info_array();
+    auto move_info_ext_array = bc.get_move_info_ext_array();
+    for (unsigned i = 0; i < moves.size(); ++i)
+    {
+        auto mv = moves[i];
+        if (mv == played_mv)
+            sample.played_move = i;
+        auto& info_ext = BoardConst::get_move_info_ext<MAX_ADJ_ATTACH>(
+                    mv, move_info_ext_array);
+        auto& info = BoardConst::get_move_info<MAX_SIZE>(mv, move_info_array);
+        auto j = info.begin();
+        Features features = feature_grid_point[*j];
+        bool local = local_points.contains(*j);
+        for (unsigned k = 1; k < MAX_SIZE; ++k)
+        {
+            ++j;
+            features += feature_grid_point[*j];
+            local |= local_points.contains(*j);
+        }
+        if (local)
+            features.feature[local_move] = 1;
+        if (MAX_SIZE == 7 || IS_CALLISTO)
+        {
+            j = info_ext.begin_attach();
+            auto end = info_ext.end_attach();
+            features += feature_grid_attach[*j];
+            while (++j != end)
+            {
+                features += feature_grid_adj[*j];
+                features += feature_grid_attach[*j];
+            }
+        }
+        else
+        {
+            j = info_ext.begin_attach();
+            auto end = info_ext.end_attach();
+            features += feature_grid_attach[*j];
+            while (++j != end)
+                features += feature_grid_attach[*j];
+            j = info_ext.begin_adj();
+            end = info_ext.end_adj();
+            for ( ; j != end; ++j)
+                features += feature_grid_adj[*j];
+        }
+        switch (static_cast<unsigned>(bd.get_piece_info(info.get_piece()).get_score_points()))
+        {
+        case 0: features.feature[piece_score_0] = 1; break;
+        case 1: features.feature[piece_score_1] = 1; break;
+        case 2: features.feature[piece_score_2] = 1; break;
+        case 3: features.feature[piece_score_3] = 1; break;
+        case 4: features.feature[piece_score_4] = 1; break;
+        case 5: features.feature[piece_score_5] = 1; break;
+        default: features.feature[piece_score_6] = 1; break;
+        }
+        sample.features.push_back(features);
+        feature_occured_globally |= features;
+    }
+    if (sample.played_move == moves.size() + 1)
+        throw runtime_error("game contains illegal move");
+    samples.push_back(sample);
+}
+
+void gen_train_data(const string& file, Variant& variant)
+{
+    ifstream in(file);
+    if (! in)
+        throw runtime_error("could not open " + file);
+    Game game(variant);
+    auto& bd = game.get_board();
+    TreeReader reader;
+    bool has_more;
+    do
+    {
+        has_more = reader.read(in, false);
+        auto tree = reader.get_tree_transfer_ownership();
+        game.init(tree);
+        if (nu_games > 0 && game.get_variant() != variant)
+            throw runtime_error("Files have inconsistent game variants");
+        ++nu_games;
+        variant = game.get_variant();
+        auto max_piece_size = bd.get_board_const().get_max_piece_size();
+        auto node = &game.get_root();
+        do
+        {
+            auto mv = game.get_tree().get_move(*node);
+            if (! mv.is_null() && node->has_parent())
+            {
+                ++nu_positions;
+                game.goto_node(node->get_parent());
+                game.set_to_play(mv.color);
+                if (max_piece_size == 5 && bd.is_callisto())
+                    add_sample<5, 16, true>(bd, mv.color, mv.move);
+                else if (max_piece_size == 5)
+                    add_sample<5, 16, false>(bd, mv.color, mv.move);
+                else if (max_piece_size == 6)
+                    add_sample<6, 22, false>(bd, mv.color, mv.move);
+                else if (max_piece_size == 7)
+                    add_sample<7, 12, false>(bd, mv.color, mv.move);
+                else
+                    add_sample<22, 44, false>(bd, mv.color, mv.move);
+            }
+            node = node->get_first_child_or_null();
+        }
+        while (node != nullptr);
+        cerr << '.';
+        if (nu_games % 79 == 0)
+            cerr << '\n';
+    }
+    while (has_more);
+}
+
+void print_weight(unsigned i, const char* name, bool is_member = true)
+{
+    if (is_member)
+        cout << "m_";
+    cout << "gamma_" << name << " = ";
+    if (feature_occured_globally.feature[i] == 0u)
+        cout << "1; // unused\n";
+    else
+        cout << "exp(" << weights[i] << "f / temperature);\n";
+}
+
+void print_weights()
+{
+    FmtSaver saver(cout);
+    cout << std::fixed << setprecision(3);
+    print_weight(point_other, "point_other");
+    print_weight(point_opp_attach_or_nb, "point_opp_attach_or_nb");
+    print_weight(point_second_color_attach, "point_second_color_attach");
+    print_weight(adj_connect, "adj_connect");
+    print_weight(adj_occupied_other, "adj_occupied_other");
+    print_weight(adj_forbidden_other, "adj_forbidden_other");
+    print_weight(adj_own_attach, "adj_own_attach");
+    print_weight(adj_nonforbidden, "adj_nonforbidden");
+    print_weight(attach_to_play, "attach_to_play");
+    print_weight(attach_forbidden_other, "attach_forbidden_other");
+    print_weight(attach_nonforbidden_0, "attach_nonforbidden[0]");
+    print_weight(attach_nonforbidden_1, "attach_nonforbidden[1]");
+    print_weight(attach_nonforbidden_2, "attach_nonforbidden[2]");
+    print_weight(attach_nonforbidden_3, "attach_nonforbidden[3]");
+    print_weight(attach_nonforbidden_4, "attach_nonforbidden[4]");
+    print_weight(attach_nonforbidden_5, "attach_nonforbidden[5]");
+    print_weight(attach_nonforbidden_6, "attach_nonforbidden[6]");
+    print_weight(attach_second_color, "attach_second_color");
+    print_weight(local_move, "local");
+    print_weight(piece_score_0, "piece_score_0", false);
+    print_weight(piece_score_1, "piece_score_1", false);
+    print_weight(piece_score_2, "piece_score_2", false);
+    print_weight(piece_score_3, "piece_score_3", false);
+    print_weight(piece_score_4, "piece_score_4", false);
+    print_weight(piece_score_5, "piece_score_5", false);
+    print_weight(piece_score_6, "piece_score_6", false);
+}
+
+void init_weights()
+{
+    normal_distribution<Float> distribution(0, 0.01);
+    for (auto& w : weights)
+        w = distribution(rand_gen);
+}
+
+/** Gradient descent step using softmax training. */
+void train_step(unsigned step, bool print)
+{
+    for (auto& w : grad_weights)
+        w = 0;
+
+    Float cost = 0;
+    for (auto& sample : samples)
+    {
+        auto nu_moves = sample.features.size();
+        probs.resize(nu_moves);
+        Float sum = 0;
+        for (size_t i = 0; i < nu_moves; ++i)
+        {
+            auto& feature = sample.features[i].feature;
+            probs[i] = 0;
+            for (unsigned j = 0; j < _nu_features; ++j)
+                probs[i] += weights[j] * feature[j];
+            probs[i] = exp(probs[i]);
+            sum += probs[i];
+        }
+        for (size_t i = 0; i < nu_moves; ++i)
+            probs[i] /= sum;
+        for (size_t i = 0; i < nu_moves; ++i)
+        {
+            auto p = probs[i];
+            auto& feature = sample.features[i].feature;
+            if (i == sample.played_move)
+            {
+                for (unsigned j = 0; j < _nu_features; ++j)
+                    grad_weights[j] -= (1 - p) * feature[j];
+            }
+            else
+            {
+                for (unsigned j = 0; j < _nu_features; ++j)
+                    grad_weights[j] -= (-p) * feature[j];
+            }
+        }
+        cost += -log(probs[sample.played_move]);
+    }
+
+    auto nu_samples = static_cast<Float>(samples.size());
+    Float decay = 1e-3;
+    for (unsigned i = 0; i < _nu_features; ++i)
+    {
+        auto& w = weights[i];
+        auto dw = grad_weights[i] / nu_samples + decay * w;
+        w += -step_size * dw;
+    }
+
+    cost /= nu_samples;
+
+    if (print)
+    {
+        LIBBOARDGAME_LOG("Step ", step);
+        LIBBOARDGAME_LOG("Cost ", cost);
+        print_weights();
+    }
+}
+
+void train(const string& file_list, unsigned steps)
+{
+    nu_games = 0;
+    nu_positions = 0;
+    nu_moves = 0;
+        auto files = split(file_list, ',');
+    Variant variant = Variant::classic_2;
+    for (auto& file : files)
+        gen_train_data(file, variant);
+    cerr << '\n';
+    LIBBOARDGAME_LOG("Files: ", file_list);
+    LIBBOARDGAME_LOG(nu_games, " games");
+    LIBBOARDGAME_LOG(nu_positions, " positions");
+    if (nu_positions == 0)
+        return;
+    LIBBOARDGAME_LOG(double(nu_moves) / double(nu_positions), " moves/pos");
+    init_weights();
+    for (unsigned i = 1; i <= steps; ++i)
+        train_step(i, i % 100 == 0 || i == steps);
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char** argv)
+{
+    libboardgame_util::LogInitializer log_initializer;
+    try
+    {
+        vector<string> specs = {
+            "sgffiles:",
+            "steps:"
+        };
+        Options opt(argc, argv, specs);
+        train(opt.get("sgffiles"), opt.get<unsigned>("steps", 3000));
+    }
+    catch (const exception& e)
+    {
+        LIBBOARDGAME_LOG("Error: ", e.what());
+        return 1;
+    }
+    return 0;
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/libboardgame_base/CMakeLists.txt b/src/libboardgame_base/CMakeLists.txt
new file mode 100644 (file)
index 0000000..1b0d94e
--- /dev/null
@@ -0,0 +1,34 @@
+set(boardgame_base_SRCS
+    CoordPoint.h
+    CoordPoint.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
+    )
+if (PENTOBI_BUILD_GTP)
+    set(boardgame_base_SRCS ${boardgame_base_SRCS}
+        Engine.cpp
+        Engine.h
+        )
+endif()
+
+add_library(boardgame_base STATIC ${boardgame_base_SRCS})
+
+target_link_libraries(boardgame_base boardgame_util)
+if (PENTOBI_BUILD_GTP)
+    target_link_libraries(boardgame_base boardgame_gtp)
+endif()
+
+target_include_directories(boardgame_base PUBLIC ..)
diff --git a/src/libboardgame_base/CoordPoint.cpp b/src/libboardgame_base/CoordPoint.cpp
new file mode 100644 (file)
index 0000000..d9bb537
--- /dev/null
@@ -0,0 +1,26 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/CoordPoint.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CoordPoint.h"
+
+#include <iostream>
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+ostream& operator<<(ostream& out, 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..5c1148e
--- /dev/null
@@ -0,0 +1,139 @@
+//-----------------------------------------------------------------------------
+/** @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>
+#include "libboardgame_util/Assert.h"
+
+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);
+
+    CoordPoint(unsigned x, unsigned y);
+
+    bool operator==(CoordPoint p) const;
+
+    bool operator!=(CoordPoint p) const;
+
+    bool operator<(CoordPoint p) const;
+
+    CoordPoint operator+(CoordPoint p) const;
+
+    CoordPoint operator-(CoordPoint p) const;
+
+    CoordPoint& operator+=(CoordPoint p);
+
+    CoordPoint& operator-=(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 CoordPoint::CoordPoint(unsigned x, unsigned y)
+{
+    LIBBOARDGAME_ASSERT(x < numeric_limits<int>::max());
+    LIBBOARDGAME_ASSERT(y < numeric_limits<int>::max());
+    this->x = static_cast<int>(x);
+    this->y = static_cast<int>(y);
+}
+
+inline bool CoordPoint::operator==(CoordPoint p) const
+{
+    return x == p.x && y == p.y;
+}
+
+inline bool CoordPoint::operator<(CoordPoint p) const
+{
+    if (y != p.y)
+        return y < p.y;
+    return x < p.x;
+}
+
+inline bool CoordPoint::operator!=(CoordPoint p) const
+{
+    return ! operator==(p);
+}
+
+inline CoordPoint CoordPoint::operator+(CoordPoint p) const
+{
+    return {x + p.x, y + p.y};
+}
+
+inline CoordPoint& CoordPoint::operator+=(CoordPoint p)
+{
+    *this = *this + p;
+    return *this;
+}
+
+inline CoordPoint CoordPoint::operator-(CoordPoint p) const
+{
+    return {x - p.x, y - p.y};
+}
+
+inline CoordPoint& CoordPoint::operator-=(CoordPoint p)
+{
+    *this = *this - p;
+    return *this;
+}
+
+inline CoordPoint CoordPoint::null()
+{
+    return {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, 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..fcd9ef1
--- /dev/null
@@ -0,0 +1,51 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Engine.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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);
+}
+
+void Engine::cmd_cputime(Response& response)
+{
+    double time = libboardgame_sys::cpu_time();
+    if (time < 0)
+        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(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..fa2aef5
--- /dev/null
@@ -0,0 +1,36 @@
+//-----------------------------------------------------------------------------
+/** @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& response);
+    void cmd_set_random_seed(Arguments args);
+
+    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..b92a347
--- /dev/null
@@ -0,0 +1,347 @@
+//-----------------------------------------------------------------------------
+/** @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 can 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 rectangular grids.
+    @tparam P An instantiation of libboardgame_base::Point (or compatible
+    class) */
+template<class P>
+class Geometry
+{
+public:
+    using Point = P;
+
+    using IntType = typename Point::IntType;
+
+    static const unsigned max_adj = 4;
+
+    static const unsigned max_diag = 11;
+
+    /** On-board adjacent neighbors of a point. */
+    using AdjList = ArrayList<Point, max_adj, unsigned short>;
+
+    /** On-board diagonal neighbors of a point
+        Currently supports up to 11 diagonal points as used on boards
+        for GembloQ. */
+    using DiagList = ArrayList<Point, max_diag, unsigned short>;
+
+    /** Adjacent neighbors of a coordinate. */
+    using AdjCoordList = ArrayList<CoordPoint, max_adj>;
+
+    /** Diagonal neighbors of a coordinate. */
+    using DiagCoordList = ArrayList<CoordPoint, max_diag>;
+
+    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();
+
+    /** Get points that share an edge with this point. */
+    virtual AdjCoordList get_adj_coord(int x, int y) const = 0;
+
+    /** Get points that share a corner but not an edge with this point.
+        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. */
+    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(m_range); }
+
+    unsigned get_point_type(CoordPoint p) const;
+
+    unsigned get_point_type(Point p) const;
+
+    bool is_onboard(CoordPoint p) const;
+
+    bool is_onboard(unsigned x, unsigned y) const;
+
+    bool is_onboard(int x, int y) const { return is_onboard(CoordPoint(x, y)); }
+
+    /** 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;
+
+    /** Return the point at a given coordinate.
+        @return The point or Point::null() if this coordinates are
+        off-board. */
+    Point get_point(int x, int y) const;
+
+    unsigned get_width() const { return m_width; }
+
+    unsigned get_height() const { return m_height; }
+
+    /** Get range used for onboard points. */
+    IntType get_range() const { return m_range; }
+
+    unsigned get_x(Point p) const;
+
+    unsigned get_y(Point p) const;
+
+    bool from_string(string::const_iterator begin, string::const_iterator end,
+                     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 = make_unique<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:
+    IntType m_range;
+
+    AdjList m_adj[Point::range_onboard];
+
+    DiagList m_diag[Point::range_onboard];
+
+    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];
+
+#ifdef 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; // Non-inline to avoid GCC -Winline warning
+
+template<class P>
+bool Geometry<P>::from_string(string::const_iterator begin,
+                              string::const_iterator end, Point& p) const
+{
+    unsigned x;
+    unsigned y;
+    if (! m_string_rep->read(begin, end, m_width, m_height, x, y)
+            || ! is_onboard(x, y))
+        return false;
+    p = get_point(x, y);
+    return true;
+}
+
+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 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 auto Geometry<P>::get_point(int x, int y) const -> Point
+{
+    if (x < 0 || static_cast<unsigned>(x) >= m_width
+            || y < 0 || static_cast<unsigned>(y) >= m_height)
+        return Point::null();
+    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 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 < n; ++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>
+bool Geometry<P>::is_onboard(unsigned x, unsigned y) const
+{
+    return x < m_width && y < m_height && ! 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) && ! get_point(p.x, p.y).is_null();
+}
+
+#ifdef LIBBOARDGAME_DEBUG
+
+template<class P>
+inline bool Geometry<P>::is_valid(Point p) const
+{
+    return p.to_int() < m_range;
+}
+
+#endif
+
+template<class P>
+inline const string& Geometry<P>::to_string(Point p) const
+{
+    LIBBOARDGAME_ASSERT(p.to_int() < m_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..6c5c586
--- /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 = static_cast<unsigned>(max_x - min_x + 1);
+    height = static_cast<unsigned>(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 {x, y};
+    LIBBOARDGAME_ASSERT(false);
+    return {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..683b8ff
--- /dev/null
@@ -0,0 +1,229 @@
+//-----------------------------------------------------------------------------
+/** @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:
+    using Point = P;
+
+    using Geometry = libboardgame_base::Geometry<P>;
+
+
+    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)
+{
+#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:
+    using Point = P;
+
+    using Geometry = libboardgame_base::Geometry<P>;
+
+
+    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..4ec2f40
--- /dev/null
@@ -0,0 +1,103 @@
+//-----------------------------------------------------------------------------
+/** @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:
+    using Point = P;
+
+
+    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..e7f4108
--- /dev/null
@@ -0,0 +1,152 @@
+//-----------------------------------------------------------------------------
+/** @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). This may be smaller than
+    W*H if the geomtries are not rectangular.
+    @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:
+    using IntType = I;
+
+    static const unsigned range_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(range_onboard <= max_width * max_height, "");
+
+    static const unsigned range = range_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 */
+    IntType 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()
+{
+#ifdef 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 auto Point<M, W, H, I>::to_int() const -> IntType
+{
+    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..53e4ee1
--- /dev/null
@@ -0,0 +1,410 @@
+//-----------------------------------------------------------------------------
+/** @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:
+    using Point = P;
+
+    virtual ~PointTransform() = default;
+
+    virtual Point get_transformed(Point p, const Geometry<P>& geo) const = 0;
+};
+
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfIdent
+    : public PointTransform<P>
+{
+public:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfIdent<P>::get_transformed(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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfRot90<P>::get_transformed(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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfRot180<P>::get_transformed(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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfRot270<P>::get_transformed(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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfRot270Refl<P>::get_transformed(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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfRot90Refl<P>::get_transformed(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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfRefl<P>::get_transformed(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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfReflRot180<P>::get_transformed(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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonRot60<P>::get_transformed(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;
+    auto x = static_cast<unsigned>(round(cx + 0.5f * px + 1.5f * py));
+    auto 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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonRot120<P>::get_transformed(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;
+    auto x = static_cast<unsigned>(round(cx - 0.5f * px + 1.5f * py));
+    auto 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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonRot240<P>::get_transformed(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;
+    auto x = static_cast<unsigned>(round(cx - 0.5f * px - 1.5f * py));
+    auto 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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonRot300<P>::get_transformed(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;
+    auto x = static_cast<unsigned>(round(cx + 0.5f * px - 1.5f * py));
+    auto 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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonReflRot60<P>::get_transformed(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;
+    auto x = static_cast<unsigned>(round(cx + 0.5f * (-px) + 1.5f * py));
+    auto 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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonReflRot120<P>::get_transformed(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;
+    auto x = static_cast<unsigned>(round(cx - 0.5f * (-px) + 1.5f * py));
+    auto 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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonReflRot240<P>::get_transformed(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;
+    auto x = static_cast<unsigned>(round(cx - 0.5f * (-px) - 1.5f * py));
+    auto 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:
+    using Point = P;
+
+    Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonReflRot300<P>::get_transformed(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;
+    auto x = static_cast<unsigned>(round(cx + 0.5f * (-px) - 1.5f * py));
+    auto 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..228e6ab
--- /dev/null
@@ -0,0 +1,46 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Rating.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Rating.h"
+
+#include <iostream>
+#include "libboardgame_util/Assert.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+ostream& operator<<(ostream& out, Rating rating)
+{
+    out << rating.m_elo;
+    return out;
+}
+
+istream& operator>>(istream& in, Rating& rating)
+{
+    in >> rating.m_elo;
+    return in;
+}
+
+double Rating::get_expected_result(Rating elo_opponent,
+                                  unsigned nu_opponents) const
+{
+    auto diff = elo_opponent.m_elo - m_elo;
+    return
+        1. / (1. + static_cast<double>(nu_opponents) * pow(10., diff / 400.));
+}
+
+void Rating::update(double game_result, Rating elo_opponent, double k_value,
+                    unsigned nu_opponents)
+{
+    LIBBOARDGAME_ASSERT(k_value > 0);
+    auto 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..3b481c8
--- /dev/null
@@ -0,0 +1,57 @@
+//-----------------------------------------------------------------------------
+/** @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, Rating rating);
+    friend istream& operator>>(istream& in, Rating& rating);
+
+    explicit Rating(double elo = 0) : m_elo(elo) { }
+
+    /** 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) */
+    double 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(double game_result, Rating elo_opponent, double k_value = 32,
+                unsigned nu_opponents = 1);
+
+    double get() const { return m_elo; }
+
+    /** Get rating rounded to an integer. */
+    int to_int() const { return static_cast<int>(round(m_elo)); }
+
+private:
+    double 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..40eac8e
--- /dev/null
@@ -0,0 +1,128 @@
+//-----------------------------------------------------------------------------
+/** @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:
+    using Point = P;
+
+    using AdjCoordList = typename Geometry<P>::AdjCoordList;
+
+    using DiagCoordList = typename Geometry<P>::DiagCoordList;
+
+
+    RectGeometry(unsigned width, unsigned height);
+
+    /** Create or reuse an already created geometry with a given size. */
+    static const RectGeometry& get(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;
+};
+
+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)
+{
+    static map<pair<unsigned, unsigned>, shared_ptr<RectGeometry>> s_geometry;
+
+    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
+{
+    // See Geometry::get_diag_coord() about advantageous ordering of the list
+    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..83b0c8b
--- /dev/null
@@ -0,0 +1,69 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/RectTransform.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "RectTransform.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfIdentity::get_transformed(CoordPoint p) const
+{
+    return p;
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot90::get_transformed(CoordPoint p) const
+{
+    return {-p.y, p.x};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot180::get_transformed(CoordPoint p) const
+{
+    return {-p.x, -p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot270::get_transformed(CoordPoint p) const
+{
+    return {p.y, -p.x};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRefl::get_transformed(CoordPoint p) const
+{
+    return {-p.x, p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot90Refl::get_transformed(CoordPoint p) const
+{
+    return {-p.y, -p.x};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot180Refl::get_transformed(CoordPoint p) const
+{
+    return {p.x, -p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot270Refl::get_transformed(CoordPoint p) const
+{
+    return {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..bb32ad1
--- /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(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot90
+    : public Transform
+{
+public:
+    TransfRectRot90() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot180
+    : public Transform
+{
+public:
+    TransfRectRot180() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot270
+    : public Transform
+{
+public:
+    TransfRectRot270() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRefl
+    : public Transform
+{
+public:
+    TransfRectRefl() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot90Refl
+    : public Transform
+{
+public:
+    TransfRectRot90Refl() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot180Refl
+    : public Transform
+{
+public:
+    TransfRectRot180Refl() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot270Refl
+    : public Transform
+{
+public:
+    TransfRectRot270Refl() : Transform(0) {}
+
+    CoordPoint get_transformed(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..b971e49
--- /dev/null
@@ -0,0 +1,71 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/StringRep.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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;
+
+//-----------------------------------------------------------------------------
+
+bool StdStringRep::read(string::const_iterator begin,
+                        string::const_iterator end, unsigned width,
+                        unsigned height, unsigned& x, unsigned& y) const
+{
+    auto p = begin;
+    while (p != end && isspace(*p) != 0)
+        ++p;
+    bool read_x = false;
+    x = 0;
+    int c;
+    while (p != end && isalpha(*p) != 0)
+    {
+        c = tolower(*(p++));
+        if (c < 'a' || c > 'z')
+            return false;
+        x = 26 * x + static_cast<unsigned>(c - 'a' + 1);
+        if (x > width)
+            return false;
+        read_x = true;
+    }
+    if (! read_x)
+        return false;
+    --x;
+    bool read_y = false;
+    y = 0;
+    while (p != end && isdigit(*p) != 0)
+    {
+        c = *(p++);
+        y = 10 * y + static_cast<unsigned>((c - '0'));
+        if (y > height)
+            return false;
+        read_y = true;
+    }
+    if (! read_y)
+        return false;
+    y = height - y;
+    while (p != end)
+        if (isspace(*(p++)) == 0)
+            return false;
+    return true;
+}
+
+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..d20341b
--- /dev/null
@@ -0,0 +1,56 @@
+//-----------------------------------------------------------------------------
+/** @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>
+#include <string>
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** String representation of points. */
+struct StringRep
+{
+    virtual ~StringRep() = default;
+
+    virtual bool read(string::const_iterator begin, string::const_iterator end,
+                      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
+{
+    bool read(string::const_iterator begin, string::const_iterator end,
+              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..0569276
--- /dev/null
@@ -0,0 +1,17 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Transform.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Transform.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+Transform::~Transform() = default;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
diff --git a/src/libboardgame_base/Transform.h b/src/libboardgame_base/Transform.h
new file mode 100644 (file)
index 0000000..48c5bb1
--- /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(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_point_type() const { return m_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 point_type)
+        : m_point_type(point_type)
+    {}
+
+private:
+    unsigned m_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..2b34e4d
--- /dev/null
@@ -0,0 +1,74 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/Arguments.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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");
+    if (n == 1)
+        throw Failure("command needs one argument");
+    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");
+    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..dce8f75
--- /dev/null
@@ -0,0 +1,237 @@
+//-----------------------------------------------------------------------------
+/** @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 minimum 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 minimum 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;
+    }
+#endif
+    return typeid(T).name();
+}
+
+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
+{
+    auto 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..c4342ff
--- /dev/null
@@ -0,0 +1,14 @@
+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
+)
+
+target_include_directories(boardgame_gtp PUBLIC ..)
diff --git a/src/libboardgame_gtp/CmdLine.cpp b/src/libboardgame_gtp/CmdLine.cpp
new file mode 100644 (file)
index 0000000..b63d4fe
--- /dev/null
@@ -0,0 +1,117 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/CmdLine.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CmdLine.h"
+
+#include <limits>
+#include <sstream>
+
+namespace libboardgame_gtp {
+
+//-----------------------------------------------------------------------------
+
+CmdLine::CmdLine(const string& line)
+{
+    init(line);
+}
+
+CmdLine::~CmdLine() = default; // Non-inline to avoid GCC -Winline warning
+
+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)) != 0 && ! 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)) != 0)
+        ++begin;
+    auto end = m_line.end();
+    while (end > begin && isspace(static_cast<unsigned char>(*(end - 1))) != 0)
+        --end;
+    return {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..c6183ab
--- /dev/null
@@ -0,0 +1,95 @@
+//-----------------------------------------------------------------------------
+/** @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 { return m_line; }
+
+    /** Get command name. */
+    CmdLineRange get_name() const { return m_elem[m_idx_name]; }
+
+
+    void write_id(ostream& out) const;
+
+    CmdLineRange get_trimmed_line_after_elem(unsigned i) const;
+
+    const vector<CmdLineRange>& get_elements() const { return m_elem; }
+
+
+    const CmdLineRange& get_element(unsigned i) const;
+
+    unsigned get_idx_name() const { return m_idx_name; }
+
+private:
+    unsigned 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 const CmdLineRange& CmdLine::get_element(unsigned i) const
+{
+    assert(i < m_elem.size());
+    return m_elem[i];
+}
+
+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..acf0692
--- /dev/null
@@ -0,0 +1,71 @@
+//-----------------------------------------------------------------------------
+/** @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 equal(m_begin, m_end, s.begin(), s.end());
+    }
+
+    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 static_cast<string::size_type>(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..8326448
--- /dev/null
@@ -0,0 +1,201 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/Engine.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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)) == 0)
+            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;
+    }
+    return false;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+Engine::Engine()
+{
+    add("known_command", &Engine::cmd_known_command);
+    add("list_commands", &Engine::cmd_list_commands);
+    add("quit", &Engine::cmd_quit);
+}
+
+Engine::~Engine() = default; // Non-inline to avoid GCC -Winline warning
+
+void Engine::add(const string& name, const Handler& f)
+{
+    m_handlers[name] = f;
+}
+
+void Engine::add(const string& name, const HandlerNoArgs& f)
+{
+    add(name, [f](Arguments args, Response& response) {
+        args.check_empty();
+        f(response);
+    });
+}
+
+void Engine::add(const string& name, const HandlerNoResponse& f)
+{
+    add(name, [f](Arguments args, Response&) {
+        f(args);
+    });
+}
+
+void Engine::add(const string& name, const HandlerNoArgsNoResponse& f)
+{
+    add(name, [f](Arguments args, Response&) {
+        args.check_empty();
+        f();
+    });
+}
+
+/** Return @c true if command is known, @c false otherwise. */
+void Engine::cmd_known_command(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';
+}
+
+/** Quit command loop. */
+void Engine::cmd_quit()
+{
+    m_quit = true;
+}
+
+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 != nullptr)
+            *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 != nullptr)
+    {
+        *out << (status ? '=' : '?');
+        line.write_id(*out);
+        *out << ' ';
+        response.write(*out, buffer);
+        out->flush();
+    }
+    return status;
+}
+
+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..d5f9e1d
--- /dev/null
@@ -0,0 +1,198 @@
+//-----------------------------------------------------------------------------
+/** @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:
+    using Handler = function<void(Arguments, Response&)>;
+
+    using HandlerNoArgs = function<void(Response&)>;
+
+    using HandlerNoResponse = function<void(Arguments)>;
+
+    using HandlerNoArgsNoResponse = function<void()>;
+
+
+    /** @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_quit() @c quit @endlink</dt>
+        <dd>@copydoc cmd_quit() </dd>
+        </dl> */
+    /** @name Command handlers */
+    /** @{ */
+    void cmd_known_command(Arguments args, Response& response);
+    void cmd_list_commands(Response& response);
+    void cmd_quit();
+    /** @} */ // @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 remaining 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, const Handler& f);
+
+    void add(const string& name, const HandlerNoArgs& f);
+
+    void add(const string& name, const HandlerNoResponse& f);
+
+    void add(const string& name, const 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)(Arguments, Response&), T* t);
+
+    template<class T>
+    void add(const string& name, void (T::*f)(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)(Arguments, Response&));
+
+    template<class T>
+    void add(const string& name, void (T::*f)(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:
+    /** Flag to quit main loop. */
+    bool m_quit;
+
+    map<string, Handler> m_handlers;
+
+
+    bool handle_cmd(CmdLine& line, ostream* out, Response& response,
+                    string& buffer);
+};
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(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)(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)(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)(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..dd90a06
--- /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_FAILURE_H
diff --git a/src/libboardgame_gtp/Response.cpp b/src/libboardgame_gtp/Response.cpp
new file mode 100644 (file)
index 0000000..3ab3c96
--- /dev/null
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/Response.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Response.h"
+
+namespace libboardgame_gtp {
+
+//-----------------------------------------------------------------------------
+
+void Response::clear()
+{
+    m_stream.str(string());
+    m_stream.copyfmt(m_dummy);
+}
+
+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..7988f4d
--- /dev/null
@@ -0,0 +1,52 @@
+//-----------------------------------------------------------------------------
+/** @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:
+    /** Get response.
+        @return A copy of the internal response string stream */
+    string to_string() const { return m_stream.str(); }
+
+    /** Set response. */
+    void set(const string& response) { m_stream.str(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;
+
+    template<typename TYPE>
+    Response& operator<<(const TYPE& t) { m_stream << t; return *this; }
+
+private:
+    /** Response stream */
+    ostringstream m_stream;
+
+    /** Dummy for restoring default format flags*/
+    ios m_dummy{nullptr};
+};
+
+//-----------------------------------------------------------------------------
+
+} // 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..b31f1ae
--- /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;
+
+    Atomic& operator=(T t)
+    {
+        val = t;
+        return *this;
+    }
+
+    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;
+
+    Atomic& operator=(T t)
+    {
+        val.store(t);
+        return *this;
+    }
+
+    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..7a93bfc
--- /dev/null
@@ -0,0 +1,18 @@
+option(LIBBOARDGAME_MCTS_SINGLE_THREAD
+    "Slightly faster MCTS search if only single-threaded search is used" OFF)
+
+find_package(Threads)
+
+add_library(boardgame_mcts INTERFACE)
+
+if(LIBBOARDGAME_MCTS_SINGLE_THREAD)
+  target_compile_definitions(boardgame_mcts INTERFACE
+      LIBBOARDGAME_MCTS_SINGLE_THREAD)
+endif()
+
+target_include_directories(boardgame_mcts INTERFACE ..)
+
+target_link_libraries(boardgame_mcts INTERFACE
+    boardgame_util
+    Threads::Threads
+    )
diff --git a/src/libboardgame_mcts/LastGoodReply.h b/src/libboardgame_mcts/LastGoodReply.h
new file mode 100644 (file)
index 0000000..2f49133
--- /dev/null
@@ -0,0 +1,150 @@
+//-----------------------------------------------------------------------------
+/** @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 <cstddef>
+#include <random>
+#include "Atomic.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:
+    using Move = M;
+
+
+    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_hash1[Move::range];
+
+    size_t m_hash2[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_hash1)
+        hash = generator();
+    for (auto& hash : m_hash2)
+        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_hash1[last.to_int()] ^ m_hash2[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)
+    {
+        for (typename Move::IntType j = 0; j < Move::range; ++j)
+            m_lgr1[i][j].store(Move::null().to_int(), memory_order_relaxed);
+        for (size_t j = 0; j < hash_table_size; ++j)
+            m_lgr2[i][j].store(Move::null().to_int(), memory_order_relaxed);
+    }
+}
+
+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..8c5d5df
--- /dev/null
@@ -0,0 +1,289 @@
+//-----------------------------------------------------------------------------
+/** @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;
+
+//-----------------------------------------------------------------------------
+
+using NodeIdx = uint_least32_t;
+
+//-----------------------------------------------------------------------------
+
+/** %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:
+    using Move = M;
+
+    using Float = F;
+
+
+    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. */
+    void init(const Move& mv, Float value, Float count, Float move_prior);
+
+    /** 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 { return m_move; }
+
+    /** Prior value for the move.
+        This value is used in the exploration term, see description of class
+        SearchBase. */
+    Float get_move_prior() const { return m_move_prior; }
+
+    /** 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);
+
+    /** Unlink children.
+        Only to be used in 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;
+
+    Float m_move_prior;
+
+    Atomic<unsigned short, MT> m_nu_children;
+
+    Move m_move;
+
+    Atomic<NodeIdx, MT> 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;
+        Float m_move_prior;
+        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;
+    m_move_prior = node.m_move_prior;
+    // 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.load(memory_order_acquire);
+}
+
+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,
+                          Float move_prior)
+{
+    // 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_move_prior = move_prior;
+    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()
+{
+#ifdef 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);
+    // Even if m_first_child is only used by other threads after m_nu_children
+    // was set, we need release/acquire order for both because m_first_child
+    // can be overwritten later if two threads expand a node simultaneously.
+    m_first_child.store(first_child, memory_order_release);
+    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);
+    // Store relaxed (wouldn't even need to be atomic)
+    m_first_child.store(first_child, memory_order_relaxed);
+    m_nu_children.store(nu_children, memory_order_relaxed);
+}
+
+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..12b889e
--- /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 {
+
+//-----------------------------------------------------------------------------
+
+using PlayerInt = uint_fast8_t;
+
+//-----------------------------------------------------------------------------
+
+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..b11f572
--- /dev/null
@@ -0,0 +1,1514 @@
+//-----------------------------------------------------------------------------
+/** @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/RandomGenerator.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"
+#include "libboardgame_sys/Compiler.h"
+
+namespace libboardgame_mcts {
+
+using namespace std;
+using libboardgame_mcts::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 SearchBase.
+    See description of class SearchBase 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. */
+    using 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 = 1;
+
+    /** Maximum value used for Node::get_move_prior() */
+    static constexpr Float max_move_prior = 1;
+
+    /** 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;
+
+    /** Minimum count of a node to be expanded. */
+    static constexpr Float expansion_threshold = 0;
+
+    /** Increase of the expansion threshold per in-tree move played. */
+    static constexpr Float expansion_threshold_inc = 0;
+
+    /** 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.
+
+    The exploration term is not as in original UCT but has the form
+    @f$ c * P_{move} * \sqrt{N_{parent}}/N_{child} @f$ with an exploration
+    constant c and a prior move value P (similar as used in AlphaGo
+    @ref libboardgame_doc_alphago_2016).
+
+    @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:
+    using State = S;
+
+    using Move = M;
+
+    using SearchParamConst = R;
+
+    static const bool multithread = SearchParamConst::multithread;
+
+    using Float = typename SearchParamConst::Float;
+
+    using Node = libboardgame_mcts::Node<M, Float, multithread>;
+
+    using Tree = libboardgame_mcts::Tree<Node>;
+
+    using PlayerMove = libboardgame_mcts::PlayerMove<M>;
+
+
+    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 */
+    /** @{ */
+
+    /** 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.
+        It will also reuse the tree if it is the same position but the last
+        search was aborted. The default value is true, because this is
+        the usually preferred behavior during games to save search time.
+        @see set_reuse_tree() */
+    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.
+        The default value is false, because the usually preferred behavior
+        is to see if the search generates different moves when doing subsequent
+        searches in the same position.
+        @see set_subreuse_tree() */
+    void set_reuse_tree(bool enable);
+
+    bool get_reuse_tree() const;
+
+    /** Maximum parent visit count for applying RAVE. */
+    void set_rave_parent_max(Float n);
+
+    Float get_rave_parent_max() const;
+
+    /** Maximum child value count for applying RAVE. */
+    void set_rave_child_max(Float n);
+
+    Float get_rave_child_max() const;
+
+    /** Weight used for adding RAVE values to the node value. */
+    void set_rave_weight(Float v);
+
+    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;
+
+#ifdef 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(const 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 explicitly 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);
+
+private:
+#ifdef LIBBOARDGAME_DEBUG
+    class AssertionHandler
+        : public libboardgame_util::AssertionHandler
+    {
+    public:
+        explicit AssertionHandler(const SearchBase& search);
+
+        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;
+    };
+
+    /** 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:
+        using SearchFunc = function<void(ThreadState&)>;
+
+
+        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;
+
+    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;
+
+    bool m_last_aborted = false;
+
+    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;
+
+#ifdef 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>::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();
+}
+
+
+#ifdef 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>
+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)
+#ifdef LIBBOARDGAME_DEBUG
+      , m_assertion_handler(*this)
+#endif
+{ }
+
+template<class S, class M, class R>
+SearchBase<S, M, R>::~SearchBase() = default; // Non-inline to avoid GCC -Winline warning
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::check_abort(const ThreadState& thread_state) const
+{
+#ifdef 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);
+    return check_cannot_change(thread_state, remaining_simulations);
+}
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::check_cannot_change(ThreadState& thread_state,
+                                              Float remaining) const
+{
+#ifdef 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)
+    {
+        auto t = make_unique<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));
+    }
+}
+
+#ifdef 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,
+                                         SearchParamConst::max_move_prior);
+    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 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 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 expansion_threshold = SearchParamConst::expansion_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);
+        expansion_threshold += SearchParamConst::expansion_threshold_inc;
+    }
+    state.finish_in_tree();
+    if (node->get_visit_count() > expansion_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)
+{
+#ifdef 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);
+    auto 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;
+    }
+    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_last_aborted))
+            || (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;
+    m_last_aborted = get_abort();
+    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)
+    {
+        auto 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 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::max_move_prior
+            / SearchParamConst::child_min_count;
+    auto children = m_tree.get_children_nonempty(node);
+    auto i = children.begin();
+    auto value =
+            i->get_value()
+            + i->get_move_prior() * bias_factor / i->get_value_count();
+    auto best_value = value;
+    auto limit = best_value - bias_limit;
+    auto best_child = i;
+    while (++i != children.end())
+    {
+        value = i->get_value();
+        if (value <= limit)
+            continue;
+        value += i->get_move_prior() * bias_factor / i->get_value_count();
+        if (value > best_value)
+        {
+            best_value = value;
+            limit = best_value - bias_limit;
+            best_child = i;
+        }
+    }
+    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_nonempty(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;
+    }
+    return false;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_callback(
+        const function<void(double, double)>& callback)
+{
+    m_callback = callback;
+}
+
+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;
+    auto 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_nonempty(*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;
+    auto 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..29cc290
--- /dev/null
@@ -0,0 +1,465 @@
+//-----------------------------------------------------------------------------
+/** @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"
+
+namespace libboardgame_mcts {
+
+using namespace std;
+using libboardgame_util::Range;
+
+//-----------------------------------------------------------------------------
+
+/** %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:
+    using Node = N;
+
+    using Move = typename Node::Move;
+
+    using Float = typename Node::Float;
+
+    /** Range for iterating over the children of a node. */
+    using Children = Range<const Node>;
+
+
+    /** 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 verify the arguments for
+            add_child().
+            @param max_move_prior The maximum move prior used for initializing
+            children. Used only in debug mode to verify the arguments for
+            add_child(). */
+        NodeExpander(unsigned thread_id, Tree& tree, Float child_min_count,
+                     Float max_move_prior);
+
+        /** 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,
+                       Float move_prior);
+
+        /** 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_move_prior = -numeric_limits<Float>::max();
+
+        const Node* m_first_child;
+
+        const Node* m_best_child;
+
+#ifdef LIBBOARDGAME_DEBUG
+        Float m_child_min_count;
+
+        Float m_max_move_prior;
+#endif
+    };
+
+    Tree(size_t memory, unsigned nu_threads);
+
+
+    /** Remove all nodes but the root node. */
+    void clear();
+
+    const Node& get_root() const;
+
+    Children get_children(const Node& node) const;
+
+    Children get_children_nonempty(const Node& node) const;
+
+    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,
+                                           Float max_move_prior)
+    : 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);
+#ifdef LIBBOARDGAME_DEBUG
+    m_child_min_count = child_min_count;
+    m_max_move_prior = max_move_prior;
+#else
+    LIBBOARDGAME_UNUSED(child_min_count);
+    LIBBOARDGAME_UNUSED(max_move_prior);
+#endif
+}
+
+template<typename N>
+inline void Tree<N>::NodeExpander::add_child(const Move& mv, Float value,
+                                             Float count, Float move_prior)
+{
+    // -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);
+    LIBBOARDGAME_ASSERT(move_prior <= m_max_move_prior);
+    auto& next = m_thread_storage.next;
+    LIBBOARDGAME_ASSERT(next < m_thread_storage.end);
+    next->init(mv, value, count, move_prior);
+    if (move_prior > m_best_move_prior)
+    {
+        m_best_child = next;
+        m_best_move_prior = move_prior;
+    }
+    ++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_children(const Node& node) const -> Children
+{
+    auto nu_children = node.get_nu_children();
+    auto begin = nu_children != 0 ? &get_node(node.get_first_child()) : nullptr;
+    auto end = begin + nu_children;
+    return Children(begin, end);
+}
+
+template<typename N>
+inline auto Tree<N>::get_children_nonempty(const Node& node) const -> Children
+{
+    auto begin = &get_node(node.get_first_child());
+    auto end = begin + node.get_nu_children();
+    return Children(begin, end);
+}
+
+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)
+{
+    if (nu_threads == 0)
+        nu_threads = 1;
+    auto max_nodes = memory / sizeof(Node);
+    // We need at least one node per thread
+    max_nodes = max(max_nodes, static_cast<size_t>(nu_threads));
+    // 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()));
+    m_nu_threads = nu_threads;
+    m_max_nodes = max_nodes;
+
+    // Using make_unique<Node[]>(max_nodes) slows down the array creation and
+    // thereby the startup time of Pentobi with GCC 7/8 because the compiler
+    // does not optimize away the call to the empty Move() constructor (last
+    // tested with GCC 7.2.0 and GCC 8.0.0 on Ubuntu 17.10).
+    m_nodes.reset(new Node[max_nodes]);
+
+    m_thread_storage = make_unique<ThreadStorage[]>(nu_threads);
+    m_nodes_per_thread = max_nodes / nu_threads;
+    for (unsigned i = 0; i < 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>
+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;
+    // Parenthesis around thread_storage.next are needed because of a bug
+    // with GCC 4 ("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)
+{
+    auto 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..9b2ef2f
--- /dev/null
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+
+//-----------------------------------------------------------------------------
+
+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 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..86730bd
--- /dev/null
@@ -0,0 +1,22 @@
+add_library(boardgame_sgf STATIC
+  Reader.h
+  Reader.cpp
+  SgfError.h
+  SgfError.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
+)
+
+target_include_directories(boardgame_sgf PUBLIC ..)
+
+target_link_libraries(boardgame_sgf boardgame_util)
diff --git a/src/libboardgame_sgf/Reader.cpp b/src/libboardgame_sgf/Reader.cpp
new file mode 100644 (file)
index 0000000..20d4c6d
--- /dev/null
@@ -0,0 +1,247 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/Reader.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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) != 0;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+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);
+}
+
+bool Reader::read(istream& in, bool check_single_tree)
+{
+    m_in = &in;
+    m_is_in_main_variation = true;
+    consume_whitespace();
+    read_tree(true);
+    while (true)
+    {
+        int c = m_in->peek();
+        if (c == EOF)
+            return false;
+        if (c == '(')
+        {
+            if (check_single_tree)
+                throw ReadError("Input has multiple game trees");
+            return true;
+        }
+        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);
+    }
+    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() != '[')
+        {
+            char c = read_char();
+            if (! is_ascii_space(c))
+                m_id += c;
+        }
+        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;
+        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..a847d70
--- /dev/null
@@ -0,0 +1,105 @@
+//-----------------------------------------------------------------------------
+/** @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;
+    };
+
+
+    virtual ~Reader() = default;
+
+
+    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 a stream.
+        @param in The input stream containing the SGF game tree(s).
+        @param check_single_tree If true, the caller does not want to
+        handle multi-tree SGF files and a ReadError will be thrown if
+        non-whitespace characters follow after the first tree before the end of
+        the stream.
+        @return true, if there are more trees to read in the stream.
+        @throws ReadError */
+    bool read(istream& in, bool check_single_tree = true);
+
+    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/SgfError.cpp b/src/libboardgame_sgf/SgfError.cpp
new file mode 100644 (file)
index 0000000..619bd39
--- /dev/null
@@ -0,0 +1,22 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfError.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "SgfError.h"
+
+#include <string>
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+MissingProperty::MissingProperty(const string& id)
+    : SgfError("Missing SGF property '" + id + "'")
+{
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
diff --git a/src/libboardgame_sgf/SgfError.h b/src/libboardgame_sgf/SgfError.h
new file mode 100644 (file)
index 0000000..89de646
--- /dev/null
@@ -0,0 +1,73 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfError.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SGF_SGF_ERROR_H
+#define LIBBOARDGAME_SGF_SGF_ERROR_H
+
+#include <sstream>
+#include <stdexcept>
+
+namespace libboardgame_sgf {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Exception indicating 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. */
+class SgfError
+    : public runtime_error
+{
+    using runtime_error::runtime_error;
+};
+
+//-----------------------------------------------------------------------------
+
+class MissingProperty
+    : public SgfError
+{
+public:
+    explicit MissingProperty(const string& id);
+};
+
+//-----------------------------------------------------------------------------
+
+class InvalidProperty
+    : public SgfError
+{
+public:
+    template<typename T>
+    InvalidProperty(const string& id, const T& value);
+
+private:
+    template<typename T>
+    static string get_message(const string& id, const T& value);
+};
+
+template<typename T>
+InvalidProperty::InvalidProperty(const string& id, const T& value)
+    : SgfError(get_message(id, value))
+{
+}
+
+template<typename T>
+string InvalidProperty::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_SGF_ERROR_H
diff --git a/src/libboardgame_sgf/SgfNode.cpp b/src/libboardgame_sgf/SgfNode.cpp
new file mode 100644 (file)
index 0000000..3557381
--- /dev/null
@@ -0,0 +1,287 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfNode.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "SgfNode.h"
+
+#include <algorithm>
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+Property::~Property() = default; // Non-inline to avoid GCC -Winline warning
+
+//-----------------------------------------------------------------------------
+
+SgfNode::~SgfNode() = default;  // Non-inline to avoid GCC -Winline warning
+
+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()
+{
+    auto node = make_unique<SgfNode>();
+    node->m_parent = this;
+    SgfNode& result = *(node.get());
+    auto last_child = get_last_child();
+    if (last_child == nullptr)
+        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())
+        throw MissingProperty(id);
+    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 == nullptr)
+        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 != nullptr)
+    {
+        ++n;
+        child = child->m_sibling.get();
+    }
+    return n;
+}
+
+const SgfNode* SgfNode::get_previous_sibling() const
+{
+    if (m_parent == nullptr)
+        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 != nullptr);
+    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;
+    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 == nullptr)
+            {
+                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 == nullptr)
+                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;
+    }
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
diff --git a/src/libboardgame_sgf/SgfNode.h b/src/libboardgame_sgf/SgfNode.h
new file mode 100644 (file)
index 0000000..5787d42
--- /dev/null
@@ -0,0 +1,390 @@
+//-----------------------------------------------------------------------------
+/** @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 "SgfError.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();
+
+
+    /** Append a new child. */
+    void append(unique_ptr<SgfNode> node);
+
+    bool has_property(const string& id) const;
+
+    /** Get a property.
+        @throws MissingProperty if no such property */
+    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.
+        @throws InvalidProperty
+        @throws MissingProperty */
+    template<typename T>
+    T parse_property(const string& id) const;
+
+    /** Get property parsed as a type with default value.
+        @throws InvalidProperty */
+    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. */
+    void 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;
+}
+
+inline const SgfNode& SgfNode::get_first_child() const
+{
+    LIBBOARDGAME_ASSERT(has_children());
+    return *m_first_child;
+}
+
+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 InvalidProperty(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 void SgfNode::remove_children()
+{
+    m_first_child.reset();
+}
+
+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;
+    values_to_string.reserve(values.size());
+    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..66b9eda
--- /dev/null
@@ -0,0 +1,265 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfTree.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "SgfTree.h"
+
+#include <ctime>
+#include <cstdio>
+#include <cstdlib>
+#include "SgfUtil.h"
+#include "libboardgame_util/StringUtil.h"
+
+namespace libboardgame_sgf {
+
+using libboardgame_sgf::find_root;
+using libboardgame_util::trim;
+
+//-----------------------------------------------------------------------------
+
+SgfTree::SgfTree()
+{
+    SgfTree::init();
+}
+
+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()
+{
+    auto node = &get_root();
+    do
+    {
+        delete_variations(*node);
+        node = node->get_first_child_or_null();
+    }
+    while (node != nullptr);
+}
+
+void SgfTree::delete_variations(const SgfNode& node)
+{
+    if (node.get_nu_children() <= 1)
+        return;
+    non_const(node).delete_variations();
+    m_modified = true;
+}
+
+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 == nullptr)
+        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();
+    do
+    {
+        if (node->get_sibling() != nullptr)
+            return true;
+        node = node->get_first_child_or_null();
+    }
+    while (node != nullptr);
+    return false;
+}
+
+void SgfTree::init()
+{
+    auto root = make_unique<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 != nullptr && &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() != nullptr)
+    {
+        non_const(node).move_down();
+        m_modified = true;
+    }
+}
+
+void SgfTree::move_up(const SgfNode& node)
+{
+    auto parent = node.get_parent_or_null();
+    if (parent != nullptr && &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)
+{
+    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..034345e
--- /dev/null
@@ -0,0 +1,282 @@
+//-----------------------------------------------------------------------------
+/** @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 "SgfNode.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() = default;
+
+
+    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(bool is_modified = true);
+
+    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 children but the first. */
+    void delete_variations(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() */
+    void 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& round);
+
+    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 void SgfTree::remove_children(const SgfNode& node)
+{
+    if (node.has_children())
+        m_modified = true;
+    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(bool is_modified)
+{
+    m_modified = is_modified;
+}
+
+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..4775289
--- /dev/null
@@ -0,0 +1,194 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfUtil.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "SgfUtil.h"
+
+#include <algorithm>
+#include <sstream>
+#include "libboardgame_util/StringUtil.h"
+
+namespace libboardgame_sgf {
+
+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 == nullptr)
+        return node;
+    while (true)
+    {
+        auto parent = current->get_parent_or_null();
+        if (parent == nullptr || ! 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 != nullptr)
+    {
+        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 SgfNode* get_next_earlier_variation(const SgfNode& node)
+{
+    auto child = &node;
+    auto current = node.get_parent_or_null();
+    while (current != nullptr && (child->get_sibling() == nullptr))
+    {
+        child = current;
+        current = current->get_parent_or_null();
+    }
+    if (current == nullptr)
+        return nullptr;
+    return child->get_sibling();
+}
+
+const SgfNode* get_next_node(const SgfNode& node)
+{
+    auto child = node.get_first_child_or_null();
+    if (child != nullptr)
+        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 == nullptr)
+        return false;
+    while (true)
+    {
+        auto parent = current->get_parent_or_null();
+        if (parent == nullptr)
+            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 libboardgame_sgf
diff --git a/src/libboardgame_sgf/SgfUtil.h b/src/libboardgame_sgf/SgfUtil.h
new file mode 100644 (file)
index 0000000..cf7dcf0
--- /dev/null
@@ -0,0 +1,72 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+
+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 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 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..e48ad2b
--- /dev/null
@@ -0,0 +1,61 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/TreeReader.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "TreeReader.h"
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+TreeReader::TreeReader() = default; // Non-inline to avoid GCC -Winline warning
+
+TreeReader::~TreeReader() = default; // Non-inline to avoid GCC -Winline warning
+
+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 = make_unique<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..c2161f3
--- /dev/null
@@ -0,0 +1,57 @@
+//-----------------------------------------------------------------------------
+/** @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() override;
+
+    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 { return *m_root; }
+
+    /** 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;
+};
+
+//-----------------------------------------------------------------------------
+
+} // 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..0ea7773
--- /dev/null
@@ -0,0 +1,52 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/TreeWriter.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "TreeWriter.h"
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+TreeWriter::TreeWriter(ostream& out, const SgfNode& root)
+    : m_root(root),
+      m_writer(out)
+{
+}
+
+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;
+    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..3d34599
--- /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() = default;
+
+    /** 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..e1d083d
--- /dev/null
@@ -0,0 +1,80 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/Writer.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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_indent >= 0)
+        m_current_indent += static_cast<unsigned>(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_indent >= 0)
+        m_current_indent -= static_cast<unsigned>(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 (unsigned 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..e488ea2
--- /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;
+
+    unsigned 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';
+            auto indent = m_current_indent + 1 + id.size();
+            for (unsigned 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..9e44513
--- /dev/null
@@ -0,0 +1,24 @@
+include(CheckIncludeFiles)
+
+add_library(boardgame_sys STATIC
+  Compiler.h
+  CpuTime.h
+  CpuTime.cpp
+  Memory.h
+  Memory.cpp
+)
+
+check_include_files(sys/sysctl.h HAVE_SYS_SYSCTL_H)
+if(HAVE_SYS_SYSCTL_H)
+    target_compile_definitions(boardgame_sys PRIVATE HAVE_SYS_SYSCTL_H)
+endif()
+check_include_files(sys/times.h HAVE_SYS_TIMES_H)
+if(HAVE_SYS_TIMES_H)
+    target_compile_definitions(boardgame_sys PRIVATE HAVE_SYS_TIMES_H)
+endif()
+check_include_files(unistd.h HAVE_UNISTD_H)
+if(HAVE_UNISTD_H)
+    target_compile_definitions(boardgame_sys PRIVATE HAVE_UNISTD_H)
+endif()
+
+target_include_directories(boardgame_sys PUBLIC ..)
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..efdacc5
--- /dev/null
@@ -0,0 +1,56 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sys/CpuTime.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CpuTime.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+
+#ifdef 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 defined HAVE_UNISTD_H && defined HAVE_SYS_TIMES_H
+    static auto 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..cc0e91a
--- /dev/null
@@ -0,0 +1,66 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sys/Memory.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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 defined 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..0ecfcea
--- /dev/null
@@ -0,0 +1,12 @@
+add_library(boardgame_test STATIC
+  Test.h
+  Test.cpp
+)
+
+target_include_directories(boardgame_test PUBLIC ..)
+
+target_link_libraries(boardgame_test boardgame_util)
+
+add_library(boardgame_test_main STATIC Main.cpp)
+
+target_link_libraries(boardgame_test_main boardgame_test)
diff --git a/src/libboardgame_test/Main.cpp b/src/libboardgame_test/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_test/Test.cpp b/src/libboardgame_test/Test.cpp
new file mode 100644 (file)
index 0000000..7077bae
--- /dev/null
@@ -0,0 +1,111 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_test/Test.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Test.h"
+
+#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, const 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;
+    }
+    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[])
+{
+    libboardgame_util::LogInitializer log_initializer;
+    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..c541941
--- /dev/null
@@ -0,0 +1,154 @@
+//-----------------------------------------------------------------------------
+/** @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 <functional>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+
+namespace libboardgame_test {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+using TestFunction = function<void()>;
+
+//-----------------------------------------------------------------------------
+
+class TestFail
+    : public logic_error
+{
+public:
+    TestFail(const char* file, int line, const string& s);
+};
+
+//-----------------------------------------------------------------------------
+
+void add_test(const string& name, const 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, const TestFunction& function)
+    {
+        add_test(name, function);
+    }
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_test
+
+//-----------------------------------------------------------------------------
+
+#define LIBBOARDGAME_TEST_CASE(name)                                       \
+    static void name();                                                    \
+    static 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;                              \
+        const auto& result1 = (expr1);                                         \
+        const 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__,                          \
+                           "Unexpected 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) > (tolerance) * result1 / 100)      \
+        {                                                               \
+            ostringstream msg;                                          \
+            msg << "Difference between " << result1 << " and "          \
+                << result2 << " exceeds " << ((tolerance) / 100 )       \
+                << " 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_util/Abort.cpp b/src/libboardgame_util/Abort.cpp
new file mode 100644 (file)
index 0000000..d447ff1
--- /dev/null
@@ -0,0 +1,17 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Abort.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Abort.h"
+
+//----------------------------------------------------------------------------
+
+namespace libboardgame_util {
+
+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..fdd8a40
--- /dev/null
@@ -0,0 +1,31 @@
+//-----------------------------------------------------------------------------
+/** @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 = false; }
+
+inline bool get_abort() { return abort; }
+
+inline void set_abort() { abort = true; }
+
+//-----------------------------------------------------------------------------
+
+} // 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..7822e60
--- /dev/null
@@ -0,0 +1,350 @@
+//-----------------------------------------------------------------------------
+/** @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:
+    using IntType = I;
+
+    static_assert(numeric_limits<IntType>::is_integer, "");
+
+    static const IntType max_size = M;
+
+    using iterator = typename array<T, max_size>::iterator;
+
+    using const_iterator = typename array<T, max_size>::const_iterator;
+
+    using value_type = T;
+
+
+    ArrayList() = default;
+
+    ArrayList(const ArrayList& l);
+
+    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>
+ArrayList<T, M, I>::ArrayList(const ArrayList& l)
+{
+    *this = l;
+}
+
+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
+{
+    return equal(begin(), end(), array_list.begin(), array_list.end());
+}
+
+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..bbd9c3d
--- /dev/null
@@ -0,0 +1,80 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Assert.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Assert.h"
+
+#include <list>
+
+#ifdef LIBBOARDGAME_DEBUG
+#include <algorithm>
+#include <functional>
+#include <sstream>
+#include <string>
+#include <vector>
+#include "Log.h"
+#endif
+
+#ifdef LIBBOARDGAME_DISABLE_LOG
+#include "Unused.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);
+}
+
+//----------------------------------------------------------------------------
+
+#ifdef LIBBOARDGAME_DEBUG
+
+void handle_assertion(const char* expression, const char* file, int line)
+{
+    static bool is_during_handle_assertion = false;
+#ifdef LIBBOARDGAME_DISABLE_LOG
+    LIBBOARDGAME_UNUSED(expression);
+    LIBBOARDGAME_UNUSED(file);
+    LIBBOARDGAME_UNUSED(line);
+#else
+    LIBBOARDGAME_LOG(file, ":", line, ": Assertion '", expression, "' failed");
+#endif
+    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..36671dd
--- /dev/null
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------------
+/** @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();
+
+    AssertionHandler(const AssertionHandler&) = delete;
+    AssertionHandler& operator=(const AssertionHandler&) = delete;
+
+    virtual void run() = 0;
+};
+
+#ifdef 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. */
+#ifdef 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..30a69b4
--- /dev/null
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Barrier.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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..d6c7318
--- /dev/null
@@ -0,0 +1,41 @@
+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
+)
+
+target_compile_options(boardgame_util PUBLIC
+    "$<$<CONFIG:DEBUG>:-DLIBBOARDGAME_DEBUG>")
+
+target_include_directories(boardgame_util PUBLIC ..)
+
+target_link_libraries(boardgame_util boardgame_sys)
diff --git a/src/libboardgame_util/CpuTimeSource.cpp b/src/libboardgame_util/CpuTimeSource.cpp
new file mode 100644 (file)
index 0000000..5b1df44
--- /dev/null
@@ -0,0 +1,22 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/CpuTimeSource.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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..9573023
--- /dev/null
@@ -0,0 +1,79 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/IntervalChecker.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "IntervalChecker.h"
+
+#include <limits>
+#include "Assert.h"
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+IntervalChecker::IntervalChecker(TimeSource& time_source, double time_interval,
+                                 const function<bool()>& f)
+    : m_time_source(time_source),
+      m_time_interval(time_interval),
+      m_function(f)
+{
+    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 = static_cast<unsigned>(new_count_interval);
+        m_result = m_function();
+    }
+    else
+    {
+        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..6522499
--- /dev/null
@@ -0,0 +1,78 @@
+//-----------------------------------------------------------------------------
+/** @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 "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,
+                    const 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();
+    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..28d040a
--- /dev/null
@@ -0,0 +1,121 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Log.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_DISABLE_LOG
+
+//-----------------------------------------------------------------------------
+
+#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 = nullptr;
+
+//-----------------------------------------------------------------------------
+
+void _log(const string& s)
+{
+    if (_log_stream == nullptr)
+        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
+    _log_stream = &cerr;
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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..796b80a
--- /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;
+
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_DISABLE_LOG
+extern ostream* _log_stream;
+#endif
+
+inline void disable_logging()
+{
+#ifndef LIBBOARDGAME_DISABLE_LOG
+    _log_stream = nullptr;
+#endif
+}
+
+inline ostream* get_log_stream()
+{
+#ifndef LIBBOARDGAME_DISABLE_LOG
+    return _log_stream;
+#else
+    return nullptr;
+#endif
+}
+
+inline void flush_log()
+{
+#ifndef LIBBOARDGAME_DISABLE_LOG
+    if (_log_stream != nullptr)
+        _log_stream->flush();
+#endif
+}
+
+//-----------------------------------------------------------------------------
+
+#ifndef 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()
+    {
+#ifndef LIBBOARDGAME_DISABLE_LOG
+        _log_init();
+#endif
+    }
+
+    ~LogInitializer()
+    {
+#ifndef LIBBOARDGAME_DISABLE_LOG
+        _log_close();
+#endif
+    }
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+//-----------------------------------------------------------------------------
+
+#ifndef 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..f58a9a7
--- /dev/null
@@ -0,0 +1,41 @@
+//----------------------------------------------------------------------------
+/** @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 {
+
+//-----------------------------------------------------------------------------
+
+/** 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;
+}
+
+/** Modulus operation with always positive result. */
+inline int mod(int a, int b)
+{
+    return ((a % b) + b) % b;
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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..46c9413
--- /dev/null
@@ -0,0 +1,155 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Options.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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.compare(0, 1, "-") == 0 && arg != "-")
+        {
+            if (arg == "--")
+            {
+                end_of_options = true;
+                continue;
+            }
+            string name;
+            string value;
+            bool needs_arg = false;
+            if (arg.compare(0, 2, "--") == 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 (string::size_type 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() = default; // Non-inline to avoid GCC -Winline warning
+
+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..6a1d29b
--- /dev/null
@@ -0,0 +1,70 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/RandomGenerator.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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..a443892
--- /dev/null
@@ -0,0 +1,102 @@
+//-----------------------------------------------------------------------------
+/** @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:
+    using Generator = minstd_rand;
+
+    using ResultType = Generator::result_type;
+
+
+    /** 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 explicitly 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();
+
+    RandomGenerator(const RandomGenerator&) = delete;
+    RandomGenerator& operator=(const RandomGenerator&) = delete;
+
+    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..601ebf8
--- /dev/null
@@ -0,0 +1,54 @@
+//----------------------------------------------------------------------------
+/** @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 empty() const { return m_begin == m_end; }
+
+    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..8659f8a
--- /dev/null
@@ -0,0 +1,102 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/StringUtil.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "StringUtil.h"
+
+#include <cctype>
+#include <cmath>
+#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)
+{
+    auto int_seconds = static_cast<int>(round(seconds));
+    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]) != 0)
+        ++begin;
+    while (end > begin && isspace(s[end - 1]) != 0)
+        --end;
+    return s.substr(begin, end - begin);
+}
+
+string trim_right(const string& s)
+{
+    auto end = s.size();
+    while (end > 0 && isspace(s[end - 1]) != 0)
+        --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..f3dfed8
--- /dev/null
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+/** @file TimeIntervalChecker.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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..6976be2
--- /dev/null
@@ -0,0 +1,17 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/TimeSource.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "TimeSource.h"
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+TimeSource::~TimeSource() = default;
+
+//----------------------------------------------------------------------------
+
+} // 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..e6b72f5
--- /dev/null
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Timer.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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..4f50ba3
--- /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&) { }
+
+#ifdef 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..89ef238
--- /dev/null
@@ -0,0 +1,25 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/WallTimeSource.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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..88fa85a
--- /dev/null
@@ -0,0 +1,872 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Board.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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,
+                   bool is_gembloq)
+{
+    for (unsigned i = 0; i < offset; ++i)
+        out << ' ';
+    if (is_gembloq)
+    {
+        char c = 'A';
+        char c1 = ' ';
+        out << ' ';
+        for (unsigned x = 0; x < width; ++x, ++c)
+        {
+            if (x % 2 != 0)
+                out << c1;
+            if (x > 0 && x % 26 == 0)
+            {
+                c = 'A';
+                if (c1 == ' ')
+                    c1 = 'A';
+                else
+                    ++c1;
+            }
+            out << c;
+        }
+    }
+    else
+    {
+        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();
+#ifdef 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();
+    if (! m_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 (m_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 (! m_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).empty())
+        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 = get_break_ties();
+    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<>());
+    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
+{
+    if (m_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 (! m_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;
+    if (m_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 && m_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 == nullptr)
+    {
+        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 (variant == Variant::duo)
+    {
+        m_color_name[Color(0)] = "Purple";
+        m_color_name[Color(1)] = "Orange";
+        m_color_esc_sequence[Color(0)] = "\x1B[1;35;47m";
+        m_color_esc_sequence[Color(1)] = "\x1B[1;33;47m";
+        m_color_esc_sequence_text[Color(0)] = "\x1B[1;35m";
+        m_color_esc_sequence_text[Color(1)] = "\x1B[1;33m";
+    }
+    else if (variant == Variant::junior)
+    {
+        m_color_name[Color(0)] = "Green";
+        m_color_name[Color(1)] = "Orange";
+        m_color_esc_sequence[Color(0)] = "\x1B[1;32;47m";
+        m_color_esc_sequence[Color(1)] = "\x1B[1;33;47m";
+        m_color_esc_sequence_text[Color(0)] = "\x1B[1;32m";
+        m_color_esc_sequence_text[Color(1)] = "\x1B[1;33m";
+    }
+    else 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_geometry_type = libpentobi_base::get_geometry_type(variant);
+    m_is_callisto = (m_geometry_type == GeometryType::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::gembloq)
+        m_needed_starting_points = 4;
+    else
+        m_needed_starting_points = 1;
+    if (m_is_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_colors);
+    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_name() == "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;
+        has_attach_point |= static_cast<int>(is_attach_point(*i, c));
+    }
+    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();
+    unsigned n = 0;
+    do
+        if (is_colorless_starting_point(*i)
+                || (is_colored_starting_point(*i)
+                    && get_starting_point_color(*i) == c))
+            if (++n >= m_needed_starting_points)
+                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 if (m_max_piece_size == 7)
+        for (Color c : get_colors())
+            for (Move mv : setup.placements[c])
+                place<7, 12>(c, mv);
+    else
+        for (Color c : get_colors())
+            for (Move mv : setup.placements[c])
+                place<22, 44>(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 if (m_max_piece_size == 7)
+        play<7, 12>(c, mv);
+    else
+        play<22, 44>(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);
+    }
+    auto width = m_geo->get_width();
+    auto 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_gembloq = (m_piece_set == PieceSet::gembloq);
+    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(static_cast<int>(x),
+                                                    static_cast<int>(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)
+                        && (! 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()
+                         && 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 if (is_gembloq)
+                {
+                    set_color(out, "\x1B[1;30;47m");
+                    if (point_type == 1)
+                        out << '/';
+                    else if (point_type == 3)
+                        out << '\\';
+                }
+                else
+                {
+                    set_color(out, "\x1B[1;30;47m");
+                    out << ' ';
+                }
+            }
+            if (is_offboard)
+            {
+                if (is_trigon && m_geo->is_onboard(x - 1, y))
+                {
+                    set_color(out, "\x1B[1;30;47m");
+                    out << (point_type == 1 ? '\\' : '/');
+                }
+                else if (is_gembloq && m_geo->is_onboard(x - 1, y))
+                {
+                    set_color(out, "\x1B[1;30;47m");
+                    if (point_type == 1)
+                        out << '/';
+                    else if (point_type == 3)
+                        out << '\\';
+                }
+                else if (m_is_callisto && x == 0)
+                {
+                    set_color(out, "\x1B[0m");
+                    out << ' ';
+                }
+                else if (is_gembloq)
+                {
+                    set_color(out, "\x1B[0m");
+                    if (point_type == 1 || point_type == 3)
+                        out << "  ";
+                    else
+                        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 || is_gembloq)
+                            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 (m_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(static_cast<int>(width - 1),
+                                              static_cast<int>(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, is_gembloq);
+    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..273d681
--- /dev/null
@@ -0,0 +1,919 @@
+//-----------------------------------------------------------------------------
+/** @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 "Geometry.h"
+#include "MoveList.h"
+#include "PointList.h"
+#include "PointState.h"
+#include "Setup.h"
+#include "StartingPoints.h"
+#include "Variant.h"
+
+namespace libpentobi_base {
+
+class MoveMarker;
+
+//-----------------------------------------------------------------------------
+
+/** Blokus board.
+    The implementation is speed-optimized for Monte-Carlo tree search. Only
+    data that is needed during the MCTS search is computed incrementally.
+    For the same reason, it does not provide an undo function, but instead
+    a snapshot state that can can be restored quickly at the start of each
+    MCTS simulation.
+    @note @ref libboardgame_avoid_stack_allocation */
+class Board
+{
+public:
+    using PointStateGrid = Grid<PointState>;
+
+    /** Maximum number of pieces per player in any game variant. */
+    static const unsigned max_pieces = Setup::max_pieces;
+
+    using PiecesLeftList = ArrayList<Piece, Piece::max_pieces>;
+
+    static const unsigned max_player_moves = max_pieces;
+
+    /** Maximum number of moves in any game variant. */
+    static const unsigned max_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_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; }
+
+    GeometryType get_geometry_type() const { return m_geometry_type; }
+
+    bool is_callisto() const { return m_is_callisto; }
+
+    /** Whether ties are broken in the current game variant. */
+    bool get_break_ties() const { return m_is_callisto; }
+
+    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() */
+    bool from_string(Move& mv, const string& s) const {
+        return m_bc->from_string(mv, s); }
+
+    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;
+
+    /** Number of starting points the first move needs to cover.
+        This is needed for GembloQ Three-Player to ensure that the first
+        player covers all four triangles of the starting square. */
+    unsigned get_needed_starting_points() const { return m_needed_starting_points; }
+
+    /** 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;
+
+    GeometryType m_geometry_type;
+
+    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;
+
+    /** See get_needed_starting_points() */
+    unsigned m_needed_starting_points;
+
+    /** 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_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 unsigned Board::get_adj_status(Point p, Color c) const
+{
+    LIBBOARDGAME_ASSERT(m_bc->has_adj_status_points(p));
+    auto i = m_bc->get_adj_status_points(p).begin();
+    auto result = static_cast<unsigned>(is_forbidden(*i, c));
+    for (unsigned j = 1; j < PrecompMoves::adj_status_nu_adj; ++j)
+        result |= (static_cast<unsigned>(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_range());
+    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_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);
+    if (m_nu_players == 2)
+        return get_score_multicolor(c);
+    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;
+    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);
+    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;
+    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 (MAX_SIZE == 22) // GembloQ
+        {
+            LIBBOARDGAME_ASSERT(m_bonus_all_pieces == 0);
+            LIBBOARDGAME_ASSERT(m_bonus_one_piece == 0);
+        }
+        else 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..a6bbb1d
--- /dev/null
@@ -0,0 +1,1425 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/BoardConst.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "BoardConst.h"
+
+#include <algorithm>
+#include "Marker.h"
+#include "PieceTransformsClassic.h"
+#include "PieceTransformsGembloQ.h"
+#include "PieceTransformsTrigon.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_sys/Compiler.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sys::get_type_name;
+
+//-----------------------------------------------------------------------------
+
+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, 44>, 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 less = [](CoordPoint a, CoordPoint b)
+    {
+        return ((a.y == b.y && a.x < b.x) || a.y > b.y);
+    };
+    auto check = [&](unsigned short a, unsigned short b)
+    {
+        if (! less(points[a], 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;
+    case 1:
+        break;
+    default:
+        sort(points.begin(), points.end(), less);
+    }
+}
+
+vector<PieceInfo> create_pieces_callisto(const Geometry& geo,
+                                         const PieceTransforms& transforms)
+{
+    auto geometry_type = GeometryType::callisto;
+    vector<PieceInfo> pieces;
+    pieces.reserve(19);
+    pieces.emplace_back("1",
+                        PiecePoints{ CoordPoint(0, 0) },
+                        geo, transforms, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("L",
+                        PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+                                     CoordPoint(0, -1), CoordPoint(1, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("T4",
+                        PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+                                     CoordPoint(1, 0), CoordPoint(0, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("Z",
+                        PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+                                     CoordPoint(0, 1), CoordPoint(1, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("O",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+                                     CoordPoint(1, 0), CoordPoint(1, -1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("V",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+                                     CoordPoint(1, 0) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("I",
+                        PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+                                     CoordPoint(0, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("2",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    return pieces;
+}
+
+vector<PieceInfo> create_pieces_classic(const Geometry& geo,
+                                        const PieceTransforms& transforms)
+{
+    auto geometry_type = GeometryType::classic;
+    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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("L4",
+                        PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+                                     CoordPoint(0, -1), CoordPoint(1, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("I4",
+                        PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+                                     CoordPoint(0, 1), CoordPoint(0, 2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("T4",
+                        PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+                                     CoordPoint(1, 0), CoordPoint(0, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("Z4",
+                        PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+                                     CoordPoint(0, 1), CoordPoint(1, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("O",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+                                     CoordPoint(1, 0), CoordPoint(1, -1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("V3",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+                                     CoordPoint(1, 0) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("I3",
+                        PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+                                     CoordPoint(0, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("2",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("1",
+                        PiecePoints{ CoordPoint(0, 0) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    return pieces;
+}
+
+vector<PieceInfo> create_pieces_gembloq(const Geometry& geo,
+                                        const PieceTransforms& transforms)
+{
+    auto geometry_type = GeometryType::gembloq;
+    vector<PieceInfo> pieces;
+    pieces.reserve(21);
+    pieces.emplace_back("P",
+                        PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(1, 1), CoordPoint(2, 1),
+                                     CoordPoint(3, 1), CoordPoint(3, 2),
+                                     CoordPoint(-3, -1), CoordPoint(-2, -1),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(1, -1), CoordPoint(2, -1),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2),
+                                     CoordPoint(-1, -2), CoordPoint(0, -2),
+                                     CoordPoint(1, -2), CoordPoint(2, -2),
+                                     CoordPoint(-1, -3), CoordPoint(0, -3) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("I5",
+                        PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(-3, -1), CoordPoint(-2, -1),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(-5, -2), CoordPoint(-4, -2),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2),
+                                     CoordPoint(-5, -3), CoordPoint(-4, -3),
+                                     CoordPoint(1, 1), CoordPoint(2, 1),
+                                     CoordPoint(3, 1), CoordPoint(4, 1),
+                                     CoordPoint(3, 2), CoordPoint(4, 2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("X",
+                        PiecePoints{ CoordPoint(-3, 0), CoordPoint(-2, 0),
+                                     CoordPoint(-1, 0), CoordPoint(0, 0),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(-3, -1), CoordPoint(-2, -1),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(1, -1), CoordPoint(2, -1),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2),
+                                     CoordPoint(1, -2), CoordPoint(2, -2),
+                                     CoordPoint(-3, 1), CoordPoint(-2, 1),
+                                     CoordPoint(1, 1), CoordPoint(2, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("W",
+                        PiecePoints{ CoordPoint(-5, 0), CoordPoint(-4, 0),
+                                     CoordPoint(-3, 0), CoordPoint(-2, 0),
+                                     CoordPoint(-1, 0), CoordPoint(0, 0),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(3, 0), CoordPoint(4, 0),
+                                     CoordPoint(-5, -1), CoordPoint(-4, -1),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(3, -1), CoordPoint(4, -1),
+                                     CoordPoint(-3, 1), CoordPoint(-2, 1),
+                                     CoordPoint(1, 1), CoordPoint(2, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("Z",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(3, 0), CoordPoint(4, 0),
+                                     CoordPoint(-5, 0), CoordPoint(-4, 0),
+                                     CoordPoint(-5, -1), CoordPoint(-4, -1),
+                                     CoordPoint(1, 1), CoordPoint(2, 1),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(3, -1), CoordPoint(4, -1),
+                                     CoordPoint(-3, -1), CoordPoint(-2, -1),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("Y",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-3, 0),
+                                     CoordPoint(-2, 0), CoordPoint(-1, 0),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(-2, -1), CoordPoint(-1, -1),
+                                     CoordPoint(0, -1), CoordPoint(-3, -1),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2),
+                                     CoordPoint(-3, 1), CoordPoint(-2, 1),
+                                     CoordPoint(1, 1), CoordPoint(2, 1),
+                                     CoordPoint(3, 1), CoordPoint(4, 1),
+                                     CoordPoint(3, 2), CoordPoint(4, 2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("N5",
+                        PiecePoints{ CoordPoint(0, -1), CoordPoint(-3, -1),
+                                     CoordPoint(-2, -1), CoordPoint(-1, -1),
+                                     CoordPoint(-4, -2), CoordPoint(-3, -2),
+                                     CoordPoint(-5, -2), CoordPoint(-5, -3),
+                                     CoordPoint(-2, -2), CoordPoint(-4, -3),
+                                     CoordPoint(-3, 0), CoordPoint(-2, 0),
+                                     CoordPoint(-1, 0), CoordPoint(0, 0),
+                                     CoordPoint(-3, 1), CoordPoint(-2, 1),
+                                     CoordPoint(-1, 1), CoordPoint(0, 1),
+                                     CoordPoint(-1, 2), CoordPoint(0, 2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("T5",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-5, 0),
+                                     CoordPoint(-4, 0), CoordPoint(-1, 0),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(1, 1), CoordPoint(2, 1),
+                                     CoordPoint(-5, -1), CoordPoint(-4, -1),
+                                     CoordPoint(-3, -1), CoordPoint(-2, -1),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2),
+                                     CoordPoint(-1, -2), CoordPoint(0, -2),
+                                     CoordPoint(-1, -3), CoordPoint(0, -3) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("L5",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(-5, -2), CoordPoint(-4, -2),
+                                     CoordPoint(-5, -3), CoordPoint(-4, -3),
+                                     CoordPoint(3, 0), CoordPoint(4, 0),
+                                     CoordPoint(1, 1), CoordPoint(2, 1),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(3, -1), CoordPoint(4, -1),
+                                     CoordPoint(-3, -1), CoordPoint(-2, -1),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("N4.5",
+                        PiecePoints{ CoordPoint(0, -1), CoordPoint(-3, -1),
+                                     CoordPoint(-2, -1), CoordPoint(-1, -1),
+                                     CoordPoint(-4, -2), CoordPoint(-3, -2),
+                                     CoordPoint(-2, -2), CoordPoint(-4, -3),
+                                     CoordPoint(-3, 0), CoordPoint(-2, 0),
+                                     CoordPoint(-1, 0), CoordPoint(0, 0),
+                                     CoordPoint(-3, 1), CoordPoint(-2, 1),
+                                     CoordPoint(-1, 1), CoordPoint(0, 1),
+                                     CoordPoint(-1, 2), CoordPoint(0, 2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("O",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-3, 0),
+                                     CoordPoint(-2, 0), CoordPoint(-1, 0),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(-3, 1), CoordPoint(-2, 1),
+                                     CoordPoint(-1, 1), CoordPoint(0, 1),
+                                     CoordPoint(1, 1), CoordPoint(2, 1),
+                                     CoordPoint(-1, 2), CoordPoint(0, 2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("T4",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-3, 0),
+                                     CoordPoint(-2, 0), CoordPoint(-1, 0),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(1, -1), CoordPoint(2, -1),
+                                     CoordPoint(1, -2), CoordPoint(2, -2),
+                                     CoordPoint(-3, 1), CoordPoint(-2, 1),
+                                     CoordPoint(1, 1), CoordPoint(2, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("L4",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(3, 0), CoordPoint(4, 0),
+                                     CoordPoint(1, 1), CoordPoint(2, 1),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(3, -1), CoordPoint(4, -1),
+                                     CoordPoint(-3, -1), CoordPoint(-2, -1),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("I4",
+                        PiecePoints{ CoordPoint(-2, -1), CoordPoint(-3, -1),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2),
+                                     CoordPoint(0, 0), CoordPoint(-1, 0),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(2, 1), CoordPoint(1, 1),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(4, 2), CoordPoint(3, 2),
+                                     CoordPoint(3, 1), CoordPoint(4, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("L3.5",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+                                     CoordPoint(-3, -1), CoordPoint(-2, -1),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(1, -1), CoordPoint(2, -1),
+                                     CoordPoint(1, -2), CoordPoint(2, -2),
+                                     CoordPoint(-5, -2), CoordPoint(-4, -2),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("V",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+                                     CoordPoint(-3, -1), CoordPoint(-2, -1),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(1, -1), CoordPoint(2, -1),
+                                     CoordPoint(1, -2), CoordPoint(2, -2),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("I3",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+                                     CoordPoint(-3, -1), CoordPoint(-2, -1),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(1, 1), CoordPoint(2, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("L2.5",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+                                     CoordPoint(-3, -1), CoordPoint(-2, -1),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(1, -1), CoordPoint(2, -1),
+                                     CoordPoint(-3, -2), CoordPoint(-2, -2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("2",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(1, 0), CoordPoint(2, 0),
+                                     CoordPoint(1, 1), CoordPoint(2, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("1.5",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1),
+                                     CoordPoint(1, 0), CoordPoint(2, 0)},
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("1",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+                                     CoordPoint(-1, -1), CoordPoint(0, -1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    return pieces;
+}
+
+vector<PieceInfo> create_pieces_junior(const Geometry& geo,
+                                       const PieceTransforms& transforms)
+{
+    auto geometry_type = GeometryType::classic;
+    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, geometry_type, 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, geometry_type, 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, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("O",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+                                     CoordPoint(1, 0), CoordPoint(1, -1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("T4",
+                        PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+                                     CoordPoint(1, 0), CoordPoint(0, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("Z4",
+                        PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+                                     CoordPoint(0, 1), CoordPoint(1, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("L4",
+                        PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+                                     CoordPoint(0, -1), CoordPoint(1, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("I4",
+                        PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+                                     CoordPoint(0, -1), CoordPoint(0, -2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("V3",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+                                     CoordPoint(1, 0) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("I3",
+                        PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+                                     CoordPoint(0, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("2",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    pieces.emplace_back("1",
+                        PiecePoints{ CoordPoint(0, 0) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+    return pieces;
+}
+
+// Note that the pieces for Trigon are currently used for both trigon_3 and
+// the other Trigon variants even if the point types of their geometries are
+// not compatible (e.g. whether the point with coordinates 0,0 is an upward or
+// downward triangle). This requires special handling of Trigon at several
+// places. In the future, we should probably use a separate set of Trigon
+// pieces for even-sized and odd-sized boards instead.
+vector<PieceInfo> create_pieces_trigon(const Geometry& geo,
+                                       const PieceTransforms& transforms)
+{
+    auto geometry_type = GeometryType::trigon;
+    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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, CoordPoint(1, 0));
+    pieces.emplace_back("I4",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0),
+                                     CoordPoint(-1, 1), CoordPoint(0, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("C4",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0),
+                                     CoordPoint(0, 1), CoordPoint(1, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("A4",
+                        PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0),
+                                     CoordPoint(1, 0), CoordPoint(2, 0) },
+                        geo, transforms, geometry_type, CoordPoint(1, 0));
+    pieces.emplace_back("I3",
+                        PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0),
+                                     CoordPoint(1, 0) },
+                        geo, transforms, geometry_type, CoordPoint(1, 0));
+    pieces.emplace_back("2",
+                        PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    pieces.emplace_back("1",
+                        PiecePoints{ CoordPoint(0, 0) },
+                        geo, transforms, geometry_type, CoordPoint(0, 0));
+    return pieces;
+}
+
+vector<PieceInfo> create_pieces_nexos(const Geometry& geo,
+                                      const PieceTransforms& transforms)
+{
+    auto geometry_type = GeometryType::nexos;
+    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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, CoordPoint(-1, 0));
+    pieces.emplace_back("W",
+                        PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, 0),
+                                     CoordPoint(0, 1), CoordPoint(1, 2)},
+                        geo, transforms, geometry_type, 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, geometry_type, 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, geometry_type, CoordPoint(0, 1));
+    pieces.emplace_back("E",
+                        PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0),
+                                     CoordPoint(0, 1), CoordPoint(-1, 2)},
+                        geo, transforms, geometry_type, 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, geometry_type, CoordPoint(-1, 0));
+    pieces.emplace_back("X",
+                        PiecePoints{ CoordPoint(0, -1), CoordPoint(-1, 0),
+                                     CoordPoint(1, 0), CoordPoint(0, 1)},
+                        geo, transforms, geometry_type, CoordPoint(0, -1));
+    pieces.emplace_back("F",
+                        PiecePoints{ CoordPoint(1, -2), CoordPoint(0, -1),
+                                     CoordPoint(1, 0), CoordPoint(0, 1)},
+                        geo, transforms, geometry_type, CoordPoint(0, -1));
+    pieces.emplace_back("H",
+                        PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0),
+                                     CoordPoint(0, 1), CoordPoint(2, 1)},
+                        geo, transforms, geometry_type, 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, geometry_type, CoordPoint(-1, 0));
+    pieces.emplace_back("G",
+                        PiecePoints{ CoordPoint(2, -1), CoordPoint(1, 0),
+                                     CoordPoint(0, 1), CoordPoint(1, 2)},
+                        geo, transforms, geometry_type, CoordPoint(1, 0));
+    pieces.emplace_back("O",
+                        PiecePoints{ CoordPoint(1, 0), CoordPoint(2, 1),
+                                     CoordPoint(0, 1), CoordPoint(1, 2)},
+                        geo, transforms, geometry_type, 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, geometry_type, CoordPoint(0, 1));
+    pieces.emplace_back("L3",
+                        PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+                                     CoordPoint(0, 1), CoordPoint(1, 2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 1));
+    pieces.emplace_back("T3",
+                        PiecePoints{ CoordPoint(-1, 0), CoordPoint(1, 0),
+                                     CoordPoint(0, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 1));
+    pieces.emplace_back("Z3",
+                        PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 1),
+                                     CoordPoint(1, 2) },
+                        geo, transforms, geometry_type, CoordPoint(0, 1));
+    pieces.emplace_back("U3",
+                        PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0),
+                                     CoordPoint(2, -1) },
+                        geo, transforms, geometry_type, CoordPoint(1, 0));
+    pieces.emplace_back("V2",
+                        PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, -1) },
+                        geo, transforms, geometry_type, CoordPoint(-1, 0));
+    pieces.emplace_back("I2",
+                        PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+                                     CoordPoint(0, 1) },
+                        geo, transforms, geometry_type, CoordPoint(0, 1));
+    pieces.emplace_back("1",
+                        PiecePoints{ CoordPoint(1, 0) },
+                        geo, transforms, geometry_type, 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_range = Move::onboard_moves_classic;
+        break;
+    case BoardType::trigon:
+        m_range = Move::onboard_moves_trigon;
+        break;
+    case BoardType::trigon_3:
+        m_range = Move::onboard_moves_trigon_3;
+        break;
+    case BoardType::duo:
+        if (piece_set == PieceSet::classic)
+            m_range = Move::onboard_moves_duo;
+        else
+        {
+            LIBBOARDGAME_ASSERT(piece_set == PieceSet::junior);
+            m_range = Move::onboard_moves_junior;
+        }
+        break;
+    case BoardType::nexos:
+        m_range = Move::onboard_moves_nexos;
+        break;
+    case BoardType::callisto:
+        m_range = Move::onboard_moves_callisto;
+        break;
+    case BoardType::callisto_2:
+        m_range = Move::onboard_moves_callisto_2;
+        break;
+    case BoardType::callisto_3:
+        m_range = Move::onboard_moves_callisto_3;
+        break;
+    case BoardType::gembloq:
+        m_range = Move::onboard_moves_gembloq;
+        break;
+    case BoardType::gembloq_2:
+        m_range = Move::onboard_moves_gembloq_2;
+        break;
+    case BoardType::gembloq_3:
+        m_range = Move::onboard_moves_gembloq_3;
+        break;
+    }
+    ++m_range; // Move::null()
+    switch (piece_set)
+    {
+    case PieceSet::classic:
+        m_transforms = make_unique<PieceTransformsClassic>();
+        m_pieces = create_pieces_classic(m_geo, *m_transforms);
+        m_max_piece_size = 5;
+        m_max_adj_attach = 16;
+        m_move_info.reset(calloc(m_range, sizeof(MoveInfo<5>)));
+        m_move_info_ext.reset(calloc(m_range, sizeof(MoveInfoExt<16>)));
+        break;
+    case PieceSet::junior:
+        m_transforms = make_unique<PieceTransformsClassic>();
+        m_pieces = create_pieces_junior(m_geo, *m_transforms);
+        m_max_piece_size = 5;
+        m_max_adj_attach = 16;
+        m_move_info.reset(calloc(m_range, sizeof(MoveInfo<5>)));
+        m_move_info_ext.reset(calloc(m_range, sizeof(MoveInfoExt<16>)));
+        break;
+    case PieceSet::trigon:
+        m_transforms = make_unique<PieceTransformsTrigon>();
+        m_pieces = create_pieces_trigon(m_geo, *m_transforms);
+        m_max_piece_size = 6;
+        m_max_adj_attach = 22;
+        m_move_info.reset(calloc(m_range, sizeof(MoveInfo<6>)));
+        m_move_info_ext.reset(calloc(m_range, sizeof(MoveInfoExt<22>)));
+        break;
+    case PieceSet::nexos:
+        m_transforms = make_unique<PieceTransformsClassic>();
+        m_pieces = create_pieces_nexos(m_geo, *m_transforms);
+        m_max_piece_size = 7;
+        m_max_adj_attach = 12;
+        m_move_info.reset(calloc(m_range, sizeof(MoveInfo<7>)));
+        m_move_info_ext.reset(calloc(m_range, sizeof(MoveInfoExt<12>)));
+        break;
+    case PieceSet::callisto:
+        m_transforms = make_unique<PieceTransformsClassic>();
+        m_pieces = create_pieces_callisto(m_geo, *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_range, sizeof(MoveInfo<5>)));
+        m_move_info_ext.reset(calloc(m_range, sizeof(MoveInfoExt<16>)));
+        break;
+    case PieceSet::gembloq:
+        m_transforms = make_unique<PieceTransformsGembloQ>();
+        m_pieces = create_pieces_gembloq(m_geo, *m_transforms);
+        m_max_piece_size = 22;
+        m_max_adj_attach = 44;
+        m_move_info.reset(calloc(m_range, sizeof(MoveInfo<22>)));
+        m_move_info_ext.reset(calloc(m_range, sizeof(MoveInfoExt<44>)));
+        break;
+    }
+    m_move_info_ext_2 = make_unique<MoveInfoExt2[]>(m_range);
+    m_nu_pieces = static_cast<Piece::IntType>(m_pieces.size());
+    for (Point p : m_geo)
+        if (has_adj_status_points(p))
+            init_adj_status_points(p);
+    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;
+    case PieceSet::gembloq:
+        LIBBOARDGAME_ASSERT(m_nu_pieces == 21);
+        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>();
+    else if (board_type == BoardType::gembloq_2)
+        init_symmetry_info<22>();
+}
+
+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_range);
+    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)
+    {
+        LIBBOARDGAME_ASSERT(has_adj_status_points(*i));
+        auto j = m_adj_status_points[*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 if (m_max_piece_size == 7)
+            create_moves<7, 12>(moves_created, piece);
+        else
+            create_moves<22, 44>(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_range);
+    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 = static_cast<int>(m_geo.get_x(p));
+        auto y = static_cast<int>(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)
+            {
+#ifndef LIBBOARDGAME_DISABLE_LOG
+                auto& transform = *transforms[i];
+                LIBBOARDGAME_LOG("Transformation ", get_type_name(transform));
+#endif
+            }
+            if (transforms[i]->get_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(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));
+        }
+    }
+}
+
+bool BoardConst::from_string(Move& mv, const string& s) const
+{
+    if (s == "null")
+    {
+        mv = Move::null();
+        return true;
+    }
+    MovePoints points;
+    auto begin = s.begin();
+    auto end = begin;
+    while (true)
+    {
+        while (end != s.end() && *end != ',')
+            ++end;
+        Point p;
+        if (! m_geo.from_string(begin, end, p))
+            return false;
+        if (points.size() == MovePoints::max_size)
+            return false;
+        points.push_back(p);
+        if (end == s.end())
+            break;
+        ++end;
+        begin = end;
+    }
+    return find_move(points, 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;
+}
+
+Piece BoardConst::get_move_piece(Move mv) const
+{
+    if (m_max_piece_size == 5)
+        return get_move_piece<5>(mv);
+    if (m_max_piece_size == 6)
+        return get_move_piece<6>(mv);
+    if (m_max_piece_size == 7)
+        return get_move_piece<7>(mv);
+    LIBBOARDGAME_ASSERT(m_max_piece_size == 22);
+    return get_move_piece<22>(mv);
+}
+
+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
+{
+    if (points.empty())
+        return false;
+    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 (equal(sorted_points.begin(), sorted_points.end(),
+                      info_ext_2.begin_scored_points(),
+                      info_ext_2.end_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;
+}
+
+/** Builds the list of neighboring points that is used for the adjacent
+    status for matching precompted move lists. */
+void BoardConst::init_adj_status_points(Point p)
+{
+    // The order of points affects the size of the precomputed lists. The
+    // following algorithm does well but is not optimal for all geometries.
+    auto& points = m_adj_status_points[p];
+    const auto max_size = PrecompMoves::adj_status_nu_adj;
+    unsigned n = 0;
+    auto add_adj = [&](Point p)
+    {
+        for (Point pp : m_geo.get_adj(p))
+        {
+            if (n == max_size)
+                return;
+            auto end = points.begin() + n;
+            if (find(points.begin(), end, pp) == end)
+                points[n++] = pp;
+        }
+    };
+    auto add_diag = [&](Point p)
+    {
+        for (Point pp : m_geo.get_diag(p))
+        {
+            if (n == max_size)
+                return;
+            auto end = points.begin() + n;
+            if (find(points.begin(), end, pp) == end)
+                points[n++] = pp;
+        }
+    };
+    add_adj(p);
+    add_diag(p);
+    auto old_n = n;
+    if (n < max_size)
+    {
+        for (unsigned i = 0; i < old_n; ++i)
+        {
+            add_adj(points[i]);
+            if (n == max_size)
+                break;
+        }
+    }
+    if (n < max_size)
+    {
+        for (unsigned i = 0; i < old_n; ++i)
+        {
+            add_diag(points[i]);
+            if (n == max_size)
+                break;
+        }
+    }
+
+    LIBBOARDGAME_ASSERT(n == max_size);
+}
+
+template<unsigned MAX_SIZE>
+void BoardConst::init_symmetry_info()
+{
+    m_symmetric_points.init(m_geo);
+    for (Move::IntType i = 1; i < m_range; ++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;
+            }
+    }
+}
+
+void BoardConst::sort(MovePoints& points) const
+{
+    auto less = [this](Point a, Point b)
+    {
+        return this->m_compare_val[a] < this->m_compare_val[b];
+    };
+    auto check = [&](unsigned short a, unsigned short b)
+    {
+        if (! less(points[a], 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;
+    case 1:
+        break;
+    default:
+        std::sort(points.begin(), points.end(), less);
+    }
+}
+
+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..efd2ae1
--- /dev/null
@@ -0,0 +1,347 @@
+//-----------------------------------------------------------------------------
+/** @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 "MoveInfo.h"
+#include "PieceInfo.h"
+#include "PrecompMoves.h"
+#include "SymmetricPoints.h"
+#include "Variant.h"
+#include "libboardgame_util/Range.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+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_points() */
+    using AdjStatusPoints = array<Point, PrecompMoves::adj_status_nu_adj>;
+
+    /** 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(). */
+    using MoveInfoArray = const void*;
+
+    /** 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(). */
+    using MoveInfoExtArray = const void*;
+
+
+    /** 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;
+
+    Move::IntType get_range() const { return m_range; }
+
+    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;
+
+    /** Array containing the points used for the adjacent status.
+        Contains a selection of first-order or second-order adjacent and
+        diagonal neighbor points.
+        @pre has_adj_status_points(p) */
+    const AdjStatusPoints& get_adj_status_points(Point p) const
+    {
+        return m_adj_status_points[p];
+    }
+
+    /** Adjacent status arrays are not initialized for junction points in
+        Nexos. */
+    bool has_adj_status_points(Point p) const
+    {
+        return m_board_type != BoardType::nexos || m_geo.get_point_type(p) != 0;
+    }
+
+    /** Only initialized in game variants with central symmetry of board
+        including starting 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;
+
+    bool from_string(Move& mv, 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;
+
+    Move::IntType m_range;
+
+    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<AdjStatusPoints> m_adj_status_points;
+
+    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_points(Point p);
+
+    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_range);
+    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 Range<const Point> BoardConst::get_move_points(Move mv) const
+{
+    if (m_max_piece_size == 5)
+    {
+        auto& info = get_move_info<5>(mv);
+        return {info.begin(), info.end()};
+    }
+    if (m_max_piece_size == 6)
+    {
+        auto& info = get_move_info<6>(mv);
+        return {info.begin(), info.end()};
+    }
+    if (m_max_piece_size == 7)
+    {
+        auto& info = get_move_info<7>(mv);
+        return {info.begin(), info.end()};
+    }
+    LIBBOARDGAME_ASSERT(m_max_piece_size == 22);
+    auto& info = get_move_info<22>(mv);
+    return {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);
+    if (m_max_piece_size == 6)
+        return get_move_points_begin<6>(mv);
+    if (m_max_piece_size == 7)
+        return get_move_points_begin<7>(mv);
+    LIBBOARDGAME_ASSERT(m_max_piece_size == 22);
+    return get_move_points_begin<22>(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_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..9a06e76
--- /dev/null
@@ -0,0 +1,132 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/BoardUpdater.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "BoardUpdater.h"
+
+#include "BoardUtil.h"
+#include "NodeUtil.h"
+#include "libboardgame_sgf/SgfUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::SgfError;
+using libboardgame_sgf::get_path_from_root;
+using libpentobi_base::get_current_position_as_setup;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+/** List to hold remaining pieces of a color with one entry for each instance
+    of the same piece. */
+using AllPiecesLeftList =
+    ArrayList<Piece, PieceInfo::max_instances * Piece::max_pieces>;
+
+/** 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;
+    for (auto& s : node.get_multi_property(id))
+    {
+        Move mv;
+        if (! bd.from_string(mv, s))
+            throw SgfError("invalid move " + s);
+        Piece piece = bd.get_move_piece(mv);
+        if (! pieces_left[c].remove(piece))
+            throw SgfError("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;
+    for (auto& s : node.get_multi_property("AE"))
+    {
+        Move mv;
+        if (! bd.from_string(mv, s))
+            throw SgfError("invalid move " + s);
+        for (Color c : bd.get_colors())
+            if (setup.placements[c].remove(mv))
+            {
+                Piece piece = bd.get_move_piece(mv);
+                pieces_left[c].push_back(piece);
+                break;
+            }
+    }
+}
+
+/** 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::get_player(node, bd.get_nu_colors(),
+                                                 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::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 SgfError("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..3de20eb
--- /dev/null
@@ -0,0 +1,86 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/BoardUtil.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "BoardUtil.h"
+
+#include "PentobiSgfUtil.h"
+#ifdef LIBBOARDGAME_DEBUG
+#include <sstream>
+#endif
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+#ifdef 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))
+    {
+        auto& placements = setup.placements[c];
+        if (placements.empty())
+            continue;
+        vector<string> values;
+        values.reserve(placements.size());
+        for (Move mv : placements)
+            values.push_back(board_const.to_string(mv, false));
+        writer.write_property(get_setup_id(variant, c), values);
+    }
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
diff --git a/src/libpentobi_base/BoardUtil.h b/src/libpentobi_base/BoardUtil.h
new file mode 100644 (file)
index 0000000..049c8e1
--- /dev/null
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/BoardUtil.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_BOARD_UTIL_H
+#define LIBPENTOBI_BASE_BOARD_UTIL_H
+
+#include "Board.h"
+#include "libboardgame_sgf/Writer.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::Writer;
+
+//-----------------------------------------------------------------------------
+
+#ifdef 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 libpentobi_base
+
+#endif // LIBPENTOBI_BASE_BOARD_UTIL_H
diff --git a/src/libpentobi_base/Book.cpp b/src/libpentobi_base/Book.cpp
new file mode 100644 (file)
index 0000000..14bcc09
--- /dev/null
@@ -0,0 +1,123 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Book.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Book.h"
+
+#include "BoardUtil.h"
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_util/Log.h"
+
+//-----------------------------------------------------------------------------
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::TreeReader;
+
+//-----------------------------------------------------------------------------
+
+Book::Book(Variant variant)
+    : m_tree(variant)
+{
+    get_transforms(variant, m_transforms, m_inv_transforms);
+}
+
+Book::~Book() = default; // Non-inline to avoid GCC -Winline warning
+
+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 == nullptr)
+            return false;
+    }
+    node = select_child(bd, c, m_tree, *node, inv_transform);
+    if (node == nullptr)
+        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 (SgfTree::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());
+    auto 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..8de5385
--- /dev/null
@@ -0,0 +1,70 @@
+//-----------------------------------------------------------------------------
+/** @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:
+    using PointTransform = libboardgame_base::PointTransform<Point>;
+
+
+    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..f723765
--- /dev/null
@@ -0,0 +1,86 @@
+set(pentobi_base_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
+  ColorMap.h
+  ColorMove.h
+  Game.h
+  Game.cpp
+  GembloQGeometry.h
+  GembloQGeometry.cpp
+  GembloQTransform.h
+  GembloQTransform.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
+  PieceTransformsGembloQ.h
+  PieceTransformsGembloQ.cpp
+  PieceTransforms.h
+  PieceTransforms.cpp
+  PieceTransformsTrigon.h
+  PieceTransformsTrigon.cpp
+  PlayerBase.h
+  PlayerBase.cpp
+  Point.h
+  PointList.h
+  PointState.h
+  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_SRCS ${pentobi_base_SRCS}
+    Engine.cpp
+    Engine.h
+  )
+endif()
+
+add_library(pentobi_base STATIC ${pentobi_base_SRCS})
+
+target_include_directories(pentobi_base PUBLIC ..)
+
+target_link_libraries(pentobi_base boardgame_sgf boardgame_base)
diff --git a/src/libpentobi_base/CallistoGeometry.cpp b/src/libpentobi_base/CallistoGeometry.cpp
new file mode 100644 (file)
index 0000000..ddddcd9
--- /dev/null
@@ -0,0 +1,124 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/CallistoGeometry.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CallistoGeometry.h"
+
+#include <map>
+#include <memory>
+#include "libboardgame_util/Unused.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+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
+
+//-----------------------------------------------------------------------------
+
+CallistoGeometry::CallistoGeometry(unsigned nu_colors)
+{
+    unsigned sz = get_size_callisto(nu_colors);
+    m_edge = get_edge_callisto(nu_colors);
+    Geometry::init(sz, sz);
+}
+
+const CallistoGeometry& CallistoGeometry::get(unsigned nu_colors)
+{
+    static map<unsigned, shared_ptr<CallistoGeometry>> s_geometry;
+
+    auto pos = s_geometry.find(nu_colors);
+    if (pos != s_geometry.end())
+        return *pos->second;
+    shared_ptr<CallistoGeometry> geometry(new CallistoGeometry(nu_colors));
+    return *s_geometry.insert(make_pair(nu_colors, 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_colors)
+{
+    auto size = get_size_callisto(nu_colors);
+    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..acfa45d
--- /dev/null
@@ -0,0 +1,55 @@
+//-----------------------------------------------------------------------------
+/** @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 "Geometry.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** 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_colors The number of colors (2, 3, or 4). */
+    static const CallistoGeometry& get(unsigned nu_colors);
+
+    static bool is_center_section(unsigned x, unsigned y, unsigned nu_colors);
+
+
+    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:
+    unsigned m_edge;
+
+
+    explicit CallistoGeometry(unsigned nu_colors);
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_CALLISTO_GEOMETRY_H
diff --git a/src/libpentobi_base/Color.h b/src/libpentobi_base/Color.h
new file mode 100644 (file)
index 0000000..efa1f86
--- /dev/null
@@ -0,0 +1,168 @@
+//-----------------------------------------------------------------------------
+/** @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 "libboardgame_util/Assert.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+class Color
+{
+public:
+    using IntType = uint_fast8_t;
+
+    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);
+
+    bool operator==(Color c) const;
+
+    bool operator!=(Color c) const;
+
+    bool operator<(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()
+{
+#ifdef LIBBOARDGAME_DEBUG
+    m_i = value_uninitialized;
+#endif
+}
+
+inline Color::Color(IntType i)
+{
+    LIBBOARDGAME_ASSERT(i < range);
+    m_i = i;
+}
+
+inline bool Color::operator==(Color c) const
+{
+    LIBBOARDGAME_ASSERT(is_initialized());
+    LIBBOARDGAME_ASSERT(c.is_initialized());
+    return m_i == c.m_i;
+}
+
+inline bool Color::operator!=(Color c) const
+{
+    return ! operator==(c);
+}
+
+inline bool Color::operator<(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;
+}
+
+//-----------------------------------------------------------------------------
+
+/** 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..9974a9b
--- /dev/null
@@ -0,0 +1,78 @@
+//-----------------------------------------------------------------------------
+/** @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 "Color.h"
+#include "Move.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+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==(ColorMove mv) const;
+
+    /** Inequality operator.
+        @pre move, color, mv.move, mv.color are initialized. */
+    bool operator!=(ColorMove mv) const;
+
+    bool is_null() const;
+};
+
+inline ColorMove::ColorMove(Color c, Move mv)
+    : color(c),
+      move(mv)
+{
+}
+
+inline bool ColorMove::operator==(ColorMove mv) const
+{
+    return move == mv.move && color == mv.color;
+}
+
+inline bool ColorMove::operator!=(ColorMove mv) const
+{
+    return ! operator==(mv);
+}
+
+inline bool ColorMove::is_null() const
+{
+    return move.is_null();
+}
+
+inline ColorMove ColorMove::null()
+{
+    return {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..9480909
--- /dev/null
@@ -0,0 +1,373 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Engine.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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::TreeReader;
+using libboardgame_sgf::get_last_node;
+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(Arguments args, Response& response)
+{
+    auto& bd = get_board();
+    auto moves = make_unique<MoveList>();
+    auto marker = make_unique<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(Arguments args, Response& response)
+{
+    genmove(get_color_arg(args), response);
+}
+
+void Engine::cmd_get_place(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(Arguments args)
+{
+    args.check_size_less_equal(2);
+    string file = args.get(0);
+    unsigned move_number = 0;
+    if (args.get_size() == 2)
+        move_number = args.parse_min<unsigned>(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 > 0)
+            node = m_game.get_tree().get_node_before_move_number(move_number - 1);
+        if (node == nullptr)
+            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(Arguments args, Response& response)
+{
+    auto& bd = get_board();
+    Move mv;
+    try
+    {
+        mv = Move(args.parse<Move::IntType>());
+    }
+    catch (const Failure&)
+    {
+        if (! bd.from_string(mv, args.get()))
+        {
+            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(Arguments args)
+{
+    play(get_board().get_to_play(), args, 0);
+}
+
+void Engine::cmd_param_base(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(Arguments args)
+{
+    play(get_color_arg(args, 0), args, 1);
+}
+
+void Engine::cmd_point_integers(Response& response)
+{
+    auto& geo = get_board().get_geometry();
+    Grid<Point::IntType> grid;
+    for (Point p : geo)
+        grid[p] = p.to_int();
+    response << '\n' << grid.to_string(geo);
+}
+
+void Engine::cmd_reg_genmove(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(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(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(Arguments args) const
+{
+    if (args.get_size() > 1)
+        throw Failure("too many arguments");
+    return get_color_arg(args, 0);
+}
+
+Color Engine::get_color_arg(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 == nullptr)
+        throw Failure("no player set");
+    return *m_player;
+}
+
+void Engine::play(Color c, Arguments args, unsigned arg_move_begin)
+{
+    auto& bd = get_board();
+    if (bd.get_nu_moves() >= Board::max_moves)
+        throw Failure("too many moves");
+    Move mv;
+    if (arg_move_begin == 0)
+    {
+        if (! bd.from_string(mv, args.get_line()))
+            throw Failure("invalid move ");
+    }
+    else
+    {
+        if (! bd.from_string(mv, args.get_remaining_line(arg_move_begin - 1)))
+            throw Failure("invalid move ");
+    }
+    if (mv.is_null())
+        throw Failure("play pass not supported (anymore)");
+    // Board::play() can handle illegal moves at arbitrary positions, even
+    // overlapping, but it does not check (for performance reasons) if the
+    // piece-left count is already zero.
+    if (! bd.is_piece_left(c, bd.get_move_piece(mv)))
+        throw Failure("piece already played");
+    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..b0ec4f1
--- /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 "Game.h"
+#include "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(Arguments args, Response& response);
+    void cmd_clear_board();
+    void cmd_final_score(Response& response);
+    void cmd_g(Response& response);
+    void cmd_genmove(Arguments args, Response& response);
+    void cmd_get_place(Arguments args, Response& response);
+    void cmd_loadsgf(Arguments args);
+    void cmd_move_info(Arguments args, Response& response);
+    void cmd_p(Arguments args);
+    void cmd_param_base(Arguments args, Response& response);
+    void cmd_play(Arguments args);
+    void cmd_point_integers(Response& response);
+    void cmd_showboard(Response& response);
+    void cmd_reg_genmove(Arguments args, Response& response);
+    void cmd_savesgf(Arguments args);
+    void cmd_set_game(Arguments args);
+    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(Arguments args, unsigned i) const;
+
+    Color get_color_arg(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, 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..c755874
--- /dev/null
@@ -0,0 +1,187 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Game.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Game.h"
+
+#include "libboardgame_sgf/SgfError.h"
+#include "libboardgame_sgf/SgfUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::SgfError;
+using libboardgame_sgf::back_to_main_variation;
+using libboardgame_sgf::is_main_variation;
+
+//-----------------------------------------------------------------------------
+
+Game::Game(Variant variant)
+  : m_bd(new Board(variant)),
+    m_tree(variant)
+{
+    init(variant);
+}
+
+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();
+}
+
+string Game::get_charset() const
+{
+    return get_root().get_property("CA", "");
+}
+
+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);
+    do
+    {
+        auto mv = tree.get_move(*node);
+        if (! mv.is_null())
+        {
+            next = bd.get_next(mv.color);
+            break;
+        }
+        Color c;
+        if (libpentobi_base::get_player(*node, bd.get_nu_colors(), c))
+            return c;
+        node = node->get_parent_or_null();
+    }
+    while (node != nullptr);
+    return bd.get_effective_to_play(next);
+}
+
+void Game::goto_node(const SgfNode& node)
+{
+    auto old = m_current;
+    try
+    {
+        update(node);
+    }
+    catch (const SgfError&)
+    {
+        // Try to restore the old state.
+        if (&node != old)
+        {
+            try
+            {
+                update(*old);
+            }
+            catch (const SgfError&)
+            {
+            }
+        }
+        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 = &m_tree.get_root();
+    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 = &m_tree.get_root();
+    goto_node(m_tree.get_root());
+}
+
+void Game::keep_only_subtree()
+{
+    m_tree.keep_only_subtree(*m_current);
+    m_current = &m_tree.get_root();
+    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 != nullptr)
+        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()
+{
+    if (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.has_move(*m_current));
+    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..99e67a7
--- /dev/null
@@ -0,0 +1,420 @@
+//-----------------------------------------------------------------------------
+/** @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 explicitly 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);
+
+
+    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 SgfError 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 SgfError if the game was constructed with an
+        external SGF tree and the tree contained invalid property values
+        (syntactically or semantically, 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_tree().has_move(get_current())
+        @pre get_current()->has_parent()
+        @note Even if the implementation of this function calls goto_node(),
+        it cannot throw an InvalidProperty 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;
+
+    /** Add final score to root node if the current node is in the main
+        variation. */
+    void set_result(int score);
+
+    string get_charset() const;
+
+    void set_charset(const string& charset);
+
+    void remove_move_annotation(const SgfNode& node);
+
+    double get_bad_move(const SgfNode& node) const;
+
+    double get_good_move(const SgfNode& node) const;
+
+    bool is_doubtful_move(const SgfNode& node) const;
+
+    bool is_interesting_move(const SgfNode& node) const;
+
+    void set_bad_move(const SgfNode& node, double value = 1);
+
+    void set_good_move(const SgfNode& node, double value = 1);
+
+    void set_doubtful_move(const SgfNode& node);
+
+    void set_interesting_move(const SgfNode& node);
+
+    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::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(bool is_modified = true);
+
+    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 SgfNode& node) const
+{
+    return SgfTree::get_bad_move(node);
+}
+
+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 SgfNode& node) const
+{
+    return SgfTree::get_good_move(node);
+}
+
+inline ColorMove Game::get_move() const
+{
+    return m_tree.get_move(*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::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 SgfNode& node) const
+{
+    return SgfTree::is_doubtful_move(node);
+}
+
+inline bool Game::is_interesting_move(const SgfNode& node) const
+{
+    return SgfTree::is_interesting_move(node);
+}
+
+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(const SgfNode& node)
+{
+    m_tree.remove_move_annotation(node);
+}
+
+inline void Game::set_application(const string& name, const string& version)
+{
+    m_tree.set_application(name, version);
+}
+
+inline void Game::set_bad_move(const SgfNode& node, double value)
+{
+    m_tree.set_bad_move(node, 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(const SgfNode& node)
+{
+    m_tree.set_doubtful_move(node);
+}
+
+inline void Game::set_good_move(const SgfNode& node, double value)
+{
+    m_tree.set_good_move(node, value);
+}
+
+inline void Game::set_interesting_move(const SgfNode& node)
+{
+    m_tree.set_interesting_move(node);
+}
+
+inline void Game::set_modified(bool is_modified)
+{
+    m_tree.set_modified(is_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/GembloQGeometry.cpp b/src/libpentobi_base/GembloQGeometry.cpp
new file mode 100644 (file)
index 0000000..2cd52c4
--- /dev/null
@@ -0,0 +1,165 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/GembloQGeometry.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "GembloQGeometry.h"
+
+#include <map>
+#include <memory>
+#include "libboardgame_util/MathUtil.h"
+#include "libboardgame_util/Unused.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_base::CoordPoint;
+using libboardgame_util::mod;
+
+//-----------------------------------------------------------------------------
+
+GembloQGeometry::GembloQGeometry(unsigned nu_players)
+{
+    unsigned height;
+    if (nu_players == 2)
+    {
+        height = 22;
+        m_edge = 4;
+    }
+    else if (nu_players == 3)
+    {
+        height = 26;
+        m_edge = 6;
+    }
+    else
+    {
+        LIBBOARDGAME_ASSERT(nu_players == 4);
+        height = 28;
+        m_edge = 13;
+    }
+    Geometry::init(2 * height, height);
+}
+
+const GembloQGeometry& GembloQGeometry::get(unsigned nu_players)
+{
+    static map<unsigned, shared_ptr<GembloQGeometry>> s_geometry;
+
+    auto pos = s_geometry.find(nu_players);
+    if (pos != s_geometry.end())
+        return *pos->second;
+    shared_ptr<GembloQGeometry> geometry(new GembloQGeometry(nu_players));
+    return *s_geometry.insert(make_pair(nu_players, geometry)).first->second;
+}
+
+auto GembloQGeometry::get_adj_coord(int x, int y) const -> AdjCoordList
+{
+    AdjCoordList l;
+    l.push_back(CoordPoint(x + 1, y));
+    l.push_back(CoordPoint(x - 1, y));
+    switch (get_point_type(x, y))
+    {
+    case 0:
+    case 3:
+        l.push_back(CoordPoint(x, y - 1));
+        break;
+    case 1:
+    case 2:
+        l.push_back(CoordPoint(x, y + 1));
+        break;
+    }
+    return l;
+}
+
+auto GembloQGeometry::get_diag_coord(int x, int y) const -> DiagCoordList
+{
+    // See Geometry::get_diag_coord() about advantageous ordering of the list
+    DiagCoordList l;
+    switch (get_point_type(x, y))
+    {
+    case 0:
+        l.push_back(CoordPoint(x + 2, 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 + 3, y));
+        l.push_back(CoordPoint(x - 2, y + 1));
+        l.push_back(CoordPoint(x + 1, y + 1));
+        l.push_back(CoordPoint(x + 3, y - 1));
+        l.push_back(CoordPoint(x - 2, y));
+        l.push_back(CoordPoint(x + 2, y));
+        l.push_back(CoordPoint(x + 1, y - 1));
+        break;
+    case 1:
+        l.push_back(CoordPoint(x - 2, 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 - 3, y));
+        l.push_back(CoordPoint(x + 2, y - 1));
+        l.push_back(CoordPoint(x - 1, y - 1));
+        l.push_back(CoordPoint(x - 3, y + 1));
+        l.push_back(CoordPoint(x + 2, y));
+        l.push_back(CoordPoint(x - 2, y));
+        l.push_back(CoordPoint(x - 1, y + 1));
+        break;
+    case 2:
+        l.push_back(CoordPoint(x - 2, y - 1));
+        l.push_back(CoordPoint(x + 3, y + 1));
+        l.push_back(CoordPoint(x - 1, y + 1));
+        l.push_back(CoordPoint(x, y - 1));
+        l.push_back(CoordPoint(x + 3, y));
+        l.push_back(CoordPoint(x + 2, y + 1));
+        l.push_back(CoordPoint(x + 1, 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));
+        break;
+    case 3:
+        l.push_back(CoordPoint(x - 3, y - 1));
+        l.push_back(CoordPoint(x + 2, y + 1));
+        l.push_back(CoordPoint(x + 1, y - 1));
+        l.push_back(CoordPoint(x, y + 1));
+        l.push_back(CoordPoint(x - 3, y));
+        l.push_back(CoordPoint(x - 2, y - 1));
+        l.push_back(CoordPoint(x - 1, 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));
+        break;
+    }
+    return l;
+}
+
+unsigned GembloQGeometry::get_period_x() const
+{
+    return 4;
+}
+
+unsigned GembloQGeometry::get_period_y() const
+{
+    return 2;
+}
+
+unsigned GembloQGeometry::get_point_type(int x, int y) const
+{
+    return mod(x + 2 * static_cast<int>(y % 2 != 0), 4);
+}
+
+bool GembloQGeometry::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 = (width - 4 * m_edge) / 2 - 1 > 2 * dy ?
+                (width - 4 * m_edge) / 2 - 1 - 2 * dy : 0;
+    unsigned max_x = width - min_x - 1;
+    return x >= min_x && x <= max_x;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
diff --git a/src/libpentobi_base/GembloQGeometry.h b/src/libpentobi_base/GembloQGeometry.h
new file mode 100644 (file)
index 0000000..5db928a
--- /dev/null
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/GembloQGeometry.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_GEMBLOQ_GEOMETRY_H
+#define LIBPENTOBI_BASE_GEMBLOQ_GEOMETRY_H
+
+#include "Geometry.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** Geometry for the board game GembloQ.
+    Each square on the board consists of four triangles, each half-square of
+    two triangles. The coordinates are like this:
+    <tt>
+       0 1 2 3 4 5 6 7 8
+    0 | / | \ | / | \ | /
+    1 | \ | / | \ | / | \
+    2 | / | \ | / | \ | /
+    3 | \ | / | \ | / | \
+    </tt>
+    The point types are determined by the location of the right angle of the
+    triangle: 0: top/left, 1=down/right, 2=down/left, 3=up/right. */
+class GembloQGeometry final
+    : public Geometry
+{
+public:
+    /** Create or reuse an already created geometry.
+        @param nu_players The number of players (2, 3, or 4). */
+    static const GembloQGeometry& get(unsigned nu_players);
+
+
+    explicit GembloQGeometry(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:
+    unsigned m_edge;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_GEMBLOQ_GEOMETRY_H
diff --git a/src/libpentobi_base/GembloQTransform.cpp b/src/libpentobi_base/GembloQTransform.cpp
new file mode 100644 (file)
index 0000000..8094618
--- /dev/null
@@ -0,0 +1,138 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/GembloQTransform.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "GembloQTransform.h"
+
+#include "libboardgame_util/MathUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_util::mod;
+
+//-----------------------------------------------------------------------------
+
+namespace
+{
+
+/** Divide integer by 2 and round down. */
+int div2(int a)
+{
+    return a < 0 ? (a - 1) / 2 : a / 2;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQIdentity::get_transformed(CoordPoint p) const
+{
+    return p;
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRot90::get_transformed(CoordPoint p) const
+{
+    auto y = div2(p.x);
+    auto x = -2 * p.y;
+    switch (mod(p.x, 4))
+    {
+    case 0:
+    case 3:
+        x -= static_cast<int>(p.y % 2 != 0);
+        break;
+    case 1:
+    case 2:
+        x -= static_cast<int>(p.y % 2 == 0);
+        break;
+    }
+    return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRot180::get_transformed(CoordPoint p) const
+{
+    return {-p.x, -p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRot270::get_transformed(CoordPoint p) const
+{
+    auto y = -div2(p.x);
+    auto x = 2 * p.y;
+    switch (mod(p.x, 4))
+    {
+    case 0:
+    case 3:
+        x += static_cast<int>(p.y % 2 != 0);
+        break;
+    case 1:
+    case 2:
+        x += static_cast<int>(p.y % 2 == 0);
+        break;
+    }
+    return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRefl::get_transformed(CoordPoint p) const
+{
+    return {-p.x, p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRot90Refl::get_transformed(CoordPoint p) const
+{
+    auto y = -div2(p.x);
+    auto x = -2 * p.y;
+    switch (mod(p.x, 4))
+    {
+    case 0:
+    case 3:
+        x -= static_cast<int>(p.y % 2 != 0);
+        break;
+    case 1:
+    case 2:
+        x -= static_cast<int>(p.y % 2 == 0);
+        break;
+    }
+    return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRot180Refl::get_transformed(CoordPoint p) const
+{
+    return {p.x, -p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRot270Refl::get_transformed(CoordPoint p) const
+{
+    auto y = div2(p.x);
+    auto x = 2 * p.y;
+    switch (mod(p.x, 4))
+    {
+    case 0:
+    case 3:
+        x += static_cast<int>(p.y % 2 != 0);
+        break;
+    case 1:
+    case 2:
+        x += static_cast<int>(p.y % 2 == 0);
+        break;
+    }
+    return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
diff --git a/src/libpentobi_base/GembloQTransform.h b/src/libpentobi_base/GembloQTransform.h
new file mode 100644 (file)
index 0000000..77696d6
--- /dev/null
@@ -0,0 +1,109 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/GembloQTransform.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_GEMBLOQ_TRANSFORM_H
+#define LIBPENTOBI_BASE_GEMBLOQ_TRANSFORM_H
+
+#include "libboardgame_base/Transform.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::CoordPoint;
+using libboardgame_base::Transform;
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQIdentity
+    : public Transform
+{
+public:
+    TransfGembloQIdentity() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRot90
+    : public Transform
+{
+public:
+    TransfGembloQRot90() : Transform(3) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRot180
+    : public Transform
+{
+public:
+    TransfGembloQRot180() : Transform(1) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRot270
+    : public Transform
+{
+public:
+    TransfGembloQRot270() : Transform(2) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRefl
+    : public Transform
+{
+public:
+    TransfGembloQRefl() : Transform(3) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRot90Refl
+    : public Transform
+{
+public:
+    TransfGembloQRot90Refl() : Transform(1) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRot180Refl
+    : public Transform
+{
+public:
+    TransfGembloQRot180Refl() : Transform(2) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRot270Refl
+    : public Transform
+{
+public:
+    TransfGembloQRot270Refl() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_GEMBLOQ_TRANSFORM_H
diff --git a/src/libpentobi_base/Geometry.h b/src/libpentobi_base/Geometry.h
new file mode 100644 (file)
index 0000000..5900814
--- /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 {
+
+//-----------------------------------------------------------------------------
+
+using Geometry = libboardgame_base::Geometry<Point>;
+
+//-----------------------------------------------------------------------------
+
+} // 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..0adb60a
--- /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 {
+
+//-----------------------------------------------------------------------------
+
+using Marker = libboardgame_base::Marker<Point>;
+
+//-----------------------------------------------------------------------------
+
+} // 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..3317205
--- /dev/null
@@ -0,0 +1,142 @@
+//-----------------------------------------------------------------------------
+/** @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. */
+    using IntType = uint_least16_t;
+
+    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;
+
+    static const IntType onboard_moves_gembloq = 31254;
+
+    static const IntType onboard_moves_gembloq_2 = 15018;
+
+    static const IntType onboard_moves_gembloq_3 = 23518;
+
+    /** Integer range of moves.
+        The maximum is given by the number of on-board moves in any game
+        variant, plus a null move. */
+    static const IntType range = onboard_moves_trigon + 1;
+
+    static Move null();
+
+    Move();
+
+    explicit Move(IntType i);
+
+    bool operator==(Move mv) const;
+
+    bool operator!=(Move mv) const;
+
+    bool operator<(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()
+{
+#ifdef LIBBOARDGAME_DEBUG
+    m_i = value_uninitialized;
+#endif
+}
+
+inline Move::Move(IntType i)
+{
+    LIBBOARDGAME_ASSERT(i < range);
+    m_i = i;
+}
+
+inline bool Move::operator==(Move mv) const
+{
+    LIBBOARDGAME_ASSERT(is_initialized());
+    LIBBOARDGAME_ASSERT(mv.is_initialized());
+    return m_i == mv.m_i;
+}
+
+inline bool Move::operator!=(Move mv) const
+{
+    return ! operator==(mv);
+}
+
+inline bool Move::operator<(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..bf17f7f
--- /dev/null
@@ -0,0 +1,133 @@
+//-----------------------------------------------------------------------------
+/** @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); }
+
+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 initialized 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..a4df562
--- /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() */
+using MoveList = libboardgame_util::ArrayList<Move, Move::range - 1>;
+
+//-----------------------------------------------------------------------------
+
+} // 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..e24135b
--- /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;
+
+//-----------------------------------------------------------------------------
+
+using MovePoints = ArrayList<Point, PieceInfo::max_size, unsigned short>;
+
+//-----------------------------------------------------------------------------
+
+} // 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..fdbcf95
--- /dev/null
@@ -0,0 +1,90 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/NexosGeometry.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "NexosGeometry.h"
+
+#include <memory>
+#include "libboardgame_util/Unused.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_base::CoordPoint;
+
+//-----------------------------------------------------------------------------
+
+NexosGeometry::NexosGeometry()
+{
+    Geometry::init(25, 25);
+}
+
+const NexosGeometry& NexosGeometry::get()
+{
+    static unique_ptr<NexosGeometry> s_geometry;
+
+    if (! s_geometry)
+        s_geometry = make_unique<NexosGeometry>();
+    return *s_geometry;
+}
+
+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;
+    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(static_cast<int>(x), static_cast<int>(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..9f7f25f
--- /dev/null
@@ -0,0 +1,64 @@
+//-----------------------------------------------------------------------------
+/** @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 "Geometry.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** 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. */
+    static const NexosGeometry& get();
+
+
+    NexosGeometry();
+
+    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;
+};
+
+//-----------------------------------------------------------------------------
+
+} // 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..0ce322d
--- /dev/null
@@ -0,0 +1,198 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/NodeUtil.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "NodeUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::InvalidProperty;
+
+//-----------------------------------------------------------------------------
+
+bool has_move(const SgfNode& node, Variant variant)
+{
+    // See also comment in get_move()
+    switch (get_nu_colors(variant))
+    {
+    case 2:
+        for (auto& prop : node.get_properties())
+        {
+            auto& id = prop.id;
+            if (id == "B" || id == "W" || id == "1" || id == "2"
+                    || id == "BLUE" || id == "GREEN")
+                return true;
+        }
+        break;
+    case 3:
+        for (auto& prop : node.get_properties())
+        {
+            auto& id = prop.id;
+            if (id == "1" || id == "2" || id == "3" || id == "BLUE"
+                    || id == "YELLOW" || id == "RED")
+                return true;
+        }
+        break;
+    case 4:
+        for (auto& prop : node.get_properties())
+        {
+            auto& id = prop.id;
+            if (id == "1" || id == "2" || id == "3" || id == "4"
+                    || id == "BLUE" || id == "YELLOW" || id == "RED"
+                     || id == "GREEN")
+                return true;
+        }
+        break;
+    default:
+        LIBBOARDGAME_ASSERT(false);
+    }
+    return false;
+}
+
+bool get_move(const SgfNode& node, Variant variant, Color& c,
+              MovePoints& points)
+{
+    auto nu_colors = get_nu_colors(variant);
+    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 (nu_colors == 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() || c.to_int() >= nu_colors)
+        return false;
+    // 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);
+    for (auto& s : node.get_multi_property(id))
+    {
+        auto begin = s.begin();
+        auto end = begin;
+        while (true)
+        {
+            while (end != s.end() && *end != ',')
+                ++end;
+            Point p;
+            if (! geo.from_string(begin, end, p)
+                    || points.size() == MovePoints::max_size)
+                throw InvalidProperty(id, string(begin, end));
+            points.push_back(p);
+            if (end == s.end())
+                break;
+            ++end;
+            begin = end;
+        }
+    }
+    return true;
+}
+
+bool get_player(const SgfNode& node, Color::IntType nu_colors, 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" && nu_colors > 2)
+        c = Color(2);
+    else if (value == "4" && nu_colors > 3)
+        c = Color(3);
+    else
+        return false;
+    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 libpentobi_base
diff --git a/src/libpentobi_base/NodeUtil.h b/src/libpentobi_base/NodeUtil.h
new file mode 100644 (file)
index 0000000..0165ae2
--- /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 {
+
+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);
+
+bool has_move(const SgfNode& node, Variant variant);
+
+/** 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::IntType nu_colors, Color& c);
+
+//-----------------------------------------------------------------------------
+
+} // 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..3c294ad
--- /dev/null
@@ -0,0 +1,47 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PentobiSgfUtil.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PentobiSgfUtil.h"
+
+#include "libboardgame_util/Assert.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+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 libpentobi_base
diff --git a/src/libpentobi_base/PentobiSgfUtil.h b/src/libpentobi_base/PentobiSgfUtil.h
new file mode 100644 (file)
index 0000000..3d672a5
--- /dev/null
@@ -0,0 +1,27 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+
+//-----------------------------------------------------------------------------
+
+/** 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 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..cd0c556
--- /dev/null
@@ -0,0 +1,344 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PentobiTree.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PentobiTree.h"
+
+#include "BoardUpdater.h"
+#include "BoardUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::InvalidProperty;
+using libboardgame_sgf::SgfError;
+using libpentobi_base::get_current_position_as_setup;
+
+//-----------------------------------------------------------------------------
+
+PentobiTree::PentobiTree(Variant variant)
+{
+    init_variant(variant);
+}
+
+PentobiTree::PentobiTree(unique_ptr<SgfNode>& root)
+{
+    PentobiTree::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;
+    auto 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);
+    auto add_color = get_setup_property(*result, id);
+    if (! add_color.contains(mv)
+            && add_color.size() < Setup::PlacementList::max_size)
+    {
+        add_color.push_back(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::get_move(node, m_variant, c, points))
+        return ColorMove::null();
+    Move mv;
+    if (! m_bc->find_move(points, mv))
+        throw SgfError("Tree contains illegal move");
+    return {c, mv};
+}
+
+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 (has_move(child) && 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
+{
+    Setup::PlacementList result;
+    if (node.has_property(id))
+        for (auto& s : node.get_multi_property(id))
+        {
+            if (result.size() == Setup::PlacementList::max_size)
+                throw InvalidProperty(id, s);
+            Move mv;
+            if (! m_bc->from_string(mv, s))
+                throw InvalidProperty(id, s);
+            result.push_back(mv);
+        }
+    return result;
+}
+
+Variant PentobiTree::get_variant(const SgfNode& root)
+{
+    string game = root.get_property("GM");
+    Variant variant;
+    if (! parse_variant(game, variant))
+        throw InvalidProperty("GM", game);
+    return variant;
+}
+
+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 != nullptr)
+        {
+            if (has_move(*current) || has_setup(*current))
+            {
+                create_new_setup = true;
+                break;
+            }
+            current = current->get_parent_or_null();
+        }
+    }
+    if (create_new_setup)
+    {
+        auto bd = make_unique<Board>(m_variant);
+        BoardUpdater updater;
+        updater.update(*bd, *this, node);
+        Setup setup;
+        get_current_position_as_setup(*bd, setup);
+        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();
+}
+
+bool PentobiTree::remove_player(const SgfNode& node)
+{
+    return 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
+    {
+        auto add_empty = get_setup_property(*result, "AE");
+        if (! add_empty.contains(mv)
+                && add_empty.size() < Setup::PlacementList::max_size)
+        {
+            add_empty.push_back(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;
+    values.reserve(placements.size());
+    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..3589867
--- /dev/null
@@ -0,0 +1,152 @@
+//-----------------------------------------------------------------------------
+/** @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 "NodeUtil.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 InvalidProperty */
+    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);
+
+    bool has_move(const SgfNode& node) const;
+
+    /** Return move or ColorMove::null() if node has no move property.
+        @throws SgfError if the node has a move property with an invalid
+        value. */
+    ColorMove get_move(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;
+
+    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() */
+    bool 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 get_color_id(m_variant, c);
+}
+
+inline const char* PentobiTree::get_setup_prop_id(Color c) const
+{
+    return 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 libpentobi_base::has_move(node, m_variant);
+}
+
+inline void PentobiTree::set_move(const SgfNode& node, ColorMove mv)
+{
+    set_move(node, mv.color, mv.move);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_PENTOBI_TREE_H
diff --git a/src/libpentobi_base/PentobiTreeWriter.cpp b/src/libpentobi_base/PentobiTreeWriter.cpp
new file mode 100644 (file)
index 0000000..0e152fe
--- /dev/null
@@ -0,0 +1,74 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PentobiTreeWriter.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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())
+{
+}
+
+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..cc09456
--- /dev/null
@@ -0,0 +1,37 @@
+//-----------------------------------------------------------------------------
+/** @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);
+
+    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..b8246cb
--- /dev/null
@@ -0,0 +1,113 @@
+//-----------------------------------------------------------------------------
+/** @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 <cstdint>
+#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:
+    using IntType = uint_fast8_t;
+
+    /** 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==(Piece piece) const;
+
+    bool operator!=(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()
+{
+#ifdef LIBBOARDGAME_DEBUG
+    m_i = value_uninitialized;
+#endif
+}
+
+inline Piece::Piece(IntType i)
+{
+    LIBBOARDGAME_ASSERT(i < range);
+    m_i = i;
+}
+
+inline bool Piece::operator==(Piece piece) const
+{
+    return m_i == piece.m_i;
+}
+
+inline bool Piece::operator!=(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..e558e37
--- /dev/null
@@ -0,0 +1,207 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceInfo.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PieceInfo.h"
+
+#include <algorithm>
+#include "libboardgame_base/GeometryUtil.h"
+#include "libboardgame_util/Assert.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_sys/Compiler.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::geometry_util::normalize_offset;
+using libboardgame_base::geometry_util::type_match_shift;
+using libboardgame_sys::get_type_name;
+
+//-----------------------------------------------------------------------------
+
+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;
+    }
+};
+
+#ifdef 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,
+                     GeometryType geometry_type, CoordPoint label_pos,
+                     unsigned nu_instances)
+    : m_nu_instances(nu_instances),
+      m_points(points),
+      m_label_pos(label_pos),
+      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);
+    auto& all_transforms = transforms.get_all();
+    vector<NormalizedPoints> all_transformed_points;
+    all_transformed_points.reserve(all_transforms.size());
+    m_transforms.reserve(all_transforms.size()); // Upper limit
+    PiecePoints transformed_points;
+    for (auto transform : all_transforms)
+    {
+        if (log_piece_creation)
+            LIBBOARDGAME_LOG("Transformation ", get_type_name(*transform));
+        transformed_points = points;
+        transform->transform(transformed_points.begin(),
+                             transformed_points.end());
+        NormalizedPoints normalized = normalize(transformed_points,
+                                                transform->get_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_transforms.size(), ")");
+            m_equivalent_transform[transform] = transform;
+            m_transforms.push_back(transform);
+        }
+        all_transformed_points.push_back(normalized);
+    }
+    if (geometry_type == GeometryType::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 (geometry_type == GeometryType::gembloq)
+        m_score_points = 0.25f * static_cast<ScoreType>(points.size());
+    else if (points.size() == 1 && geometry_type == GeometryType::callisto)
+        m_score_points = 0;
+    else
+        m_score_points = static_cast<ScoreType>(points.size());
+}
+
+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_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_transforms.begin();
+    auto end = m_transforms.end();
+    auto pos = find(begin, end, transform);
+    LIBBOARDGAME_ASSERT(pos != end);
+    if (pos + 1 == end)
+        return *begin;
+    return *(pos + 1);
+}
+
+const Transform* PieceInfo::get_previous_transform(
+                                              const Transform* transform) const
+{
+    transform = get_equivalent_transform(transform);
+    auto begin = m_transforms.begin();
+    auto end = m_transforms.end();
+    auto pos = find(begin, end, transform);
+    LIBBOARDGAME_ASSERT(pos != end);
+    if (pos == begin)
+        return *(end - 1);
+    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..31f9dbd
--- /dev/null
@@ -0,0 +1,126 @@
+//-----------------------------------------------------------------------------
+/** @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;
+
+//-----------------------------------------------------------------------------
+
+using ScoreType = float;
+
+//-----------------------------------------------------------------------------
+
+class PieceInfo
+{
+public:
+    /** Maximum number of points in a piece. */
+    static const unsigned max_size = 22;
+
+    /** Maximum number of scored points in a piece.
+        Currently the same as max_size, needed for GembloQ. If Nexos was the
+        game with the largest pieces, some memory could be saved because
+        junction points in Nexos are not scored. */
+    static const unsigned max_scored_size = 22;
+
+    /** Maximum number of instances of a piece per player. */
+    static const unsigned max_instances = 3;
+
+    using Points = ArrayList<CoordPoint, max_size>;
+
+
+    /** Constructor.
+        @param name A short unique name for the piece.
+        @param points The coordinates of the piece elements.
+        @param geo
+        @param transforms
+        @param geometry_type
+        @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,
+              GeometryType geometry_type, 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_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;
+
+    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;
+
+    string m_name;
+
+    vector<const Transform*> m_transforms;
+
+    map<const Transform*, const Transform*> m_equivalent_transform;
+};
+
+//-----------------------------------------------------------------------------
+
+using PiecePoints = PieceInfo::Points;
+
+//-----------------------------------------------------------------------------
+
+} // 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..0a8bd76
--- /dev/null
@@ -0,0 +1,76 @@
+//-----------------------------------------------------------------------------
+/** @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);
+
+    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>
+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..a675041
--- /dev/null
@@ -0,0 +1,17 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransforms.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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..eabc889
--- /dev/null
@@ -0,0 +1,71 @@
+//-----------------------------------------------------------------------------
+/** @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;
+
+    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 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..2dee93b
--- /dev/null
@@ -0,0 +1,142 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransformsClassic.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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_rot90refl);
+    m_all.push_back(&m_rot180refl);
+    m_all.push_back(&m_rot270refl);
+    m_all.push_back(&m_refl);
+}
+
+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..21d4ed2
--- /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_BASE_PIECE_TRANSFORMS_CLASSIC_H
+#define LIBPENTOBI_BASE_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 final
+    : 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_BASE_PIECE_TRANSFORMS_CLASSIC_H
diff --git a/src/libpentobi_base/PieceTransformsGembloQ.cpp b/src/libpentobi_base/PieceTransformsGembloQ.cpp
new file mode 100644 (file)
index 0000000..cba5fe4
--- /dev/null
@@ -0,0 +1,142 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransformsGembloQ.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PieceTransformsGembloQ.h"
+
+#include "libboardgame_util/Assert.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+PieceTransformsGembloQ::PieceTransformsGembloQ()
+{
+    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_rot90refl);
+    m_all.push_back(&m_rot180refl);
+    m_all.push_back(&m_rot270refl);
+    m_all.push_back(&m_refl);
+}
+
+const Transform* PieceTransformsGembloQ::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* PieceTransformsGembloQ::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* PieceTransformsGembloQ::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* PieceTransformsGembloQ::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/PieceTransformsGembloQ.h b/src/libpentobi_base/PieceTransformsGembloQ.h
new file mode 100644 (file)
index 0000000..7e8acd0
--- /dev/null
@@ -0,0 +1,57 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransformsGembloQ.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_PIECE_TRANSFORMS_GEMBLOQ_H
+#define LIBPENTOBI_BASE_PIECE_TRANSFORMS_GEMBLOQ_H
+
+#include "PieceTransforms.h"
+#include "GembloQTransform.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+class PieceTransformsGembloQ final
+    : public PieceTransforms
+{
+public:
+    PieceTransformsGembloQ();
+
+    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:
+    TransfGembloQIdentity m_identity;
+
+    TransfGembloQRot90 m_rot90;
+
+    TransfGembloQRot180 m_rot180;
+
+    TransfGembloQRot270 m_rot270;
+
+    TransfGembloQRefl m_refl;
+
+    TransfGembloQRot90Refl m_rot90refl;
+
+    TransfGembloQRot180Refl m_rot180refl;
+
+    TransfGembloQRot270Refl m_rot270refl;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_PIECE_TRANSFORMS_GEMBLOQ_H
diff --git a/src/libpentobi_base/PieceTransformsTrigon.cpp b/src/libpentobi_base/PieceTransformsTrigon.cpp
new file mode 100644 (file)
index 0000000..ca52519
--- /dev/null
@@ -0,0 +1,178 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransformsTrigon.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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_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);
+    m_all.push_back(&m_refl);
+}
+
+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..025e395
--- /dev/null
@@ -0,0 +1,65 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransformsTrigon.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_PIECE_TRANSFORMS_TRIGON_H
+#define LIBPENTOBI_BASE_PIECE_TRANSFORMS_TRIGON_H
+
+#include "PieceTransforms.h"
+#include "TrigonTransform.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+class PieceTransformsTrigon final
+    : 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;
+
+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_BASE_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..064dfaf
--- /dev/null
@@ -0,0 +1,20 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PlayerBase.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PlayerBase.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+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..b04e685
--- /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() = default;
+
+    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..457480d
--- /dev/null
@@ -0,0 +1,25 @@
+//-----------------------------------------------------------------------------
+/** @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. */
+using Point = libboardgame_base::Point<1564, 56, 28, unsigned short>;
+
+//-----------------------------------------------------------------------------
+
+} // 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..a396474
--- /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 {
+
+//-----------------------------------------------------------------------------
+
+using PointList = libboardgame_util::ArrayList<Point, Point::range_onboard>;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_POINT_LIST_H
diff --git a/src/libpentobi_base/PointState.h b/src/libpentobi_base/PointState.h
new file mode 100644 (file)
index 0000000..610c937
--- /dev/null
@@ -0,0 +1,138 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PointState.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_POINT_STATE_H
+#define LIBPENTOBI_BASE_POINT_STATE_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:
+    using IntType = Color::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==(PointState s) const;
+
+    bool operator!=(PointState s) const;
+
+    bool operator==(Color c) const;
+
+    bool operator!=(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()
+{
+#ifdef 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==(PointState s) const
+{
+    return m_i == s.m_i;
+}
+
+inline bool PointState::operator==(Color c) const
+{
+    return m_i == c.to_int();
+}
+
+inline bool PointState::operator!=(PointState s) const
+{
+    return ! operator==(s);
+}
+
+inline bool PointState::operator!=(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;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_POINT_STATE_H
diff --git a/src/libpentobi_base/PrecompMoves.h b/src/libpentobi_base/PrecompMoves.h
new file mode 100644 (file)
index 0000000..806e5d9
--- /dev/null
@@ -0,0 +1,133 @@
+//-----------------------------------------------------------------------------
+/** @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. */
+#ifdef 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 == 5 ? 2356736 : 2628840;
+    static_assert(adj_status_nu_adj == 5 || 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. */
+    using Range = libboardgame_util::Range<const Move>;
+
+
+    /** 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();
+        return {begin, begin + range.size()};
+    }
+
+    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 + 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..ee30de4
--- /dev/null
@@ -0,0 +1,69 @@
+//-----------------------------------------------------------------------------
+/** @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)
+        {
+            if (sorted[j] == adjusted[i])
+            {
+                sum += factor * float_j;
+                ++n;
+            }
+            ++float_j;
+        }
+        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..ec4b337
--- /dev/null
@@ -0,0 +1,46 @@
+//-----------------------------------------------------------------------------
+/** @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"
+#include "libboardgame_util/ArrayList.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;
+
+    using PlacementList = libboardgame_util::ArrayList<Move, max_pieces>;
+
+
+    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..672f0f7
--- /dev/null
@@ -0,0 +1,137 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/StartingPoints.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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;
+    case BoardType::gembloq:
+        add_colored_starting_point(geo, 1, 0, Color(0));
+        add_colored_starting_point(geo, 2, 0, Color(0));
+        add_colored_starting_point(geo, 1, 1, Color(0));
+        add_colored_starting_point(geo, 2, 1, Color(0));
+        add_colored_starting_point(geo, 53, 0, Color(1));
+        add_colored_starting_point(geo, 54, 0, Color(1));
+        add_colored_starting_point(geo, 53, 1, Color(1));
+        add_colored_starting_point(geo, 54, 1, Color(1));
+        add_colored_starting_point(geo, 53, 26, Color(2));
+        add_colored_starting_point(geo, 54, 26, Color(2));
+        add_colored_starting_point(geo, 53, 27, Color(2));
+        add_colored_starting_point(geo, 54, 27, Color(2));
+        add_colored_starting_point(geo, 1, 26, Color(3));
+        add_colored_starting_point(geo, 2, 26, Color(3));
+        add_colored_starting_point(geo, 1, 27, Color(3));
+        add_colored_starting_point(geo, 2, 27, Color(3));
+        break;
+    case BoardType::gembloq_2:
+        add_colored_starting_point(geo, 13, 0, Color(0));
+        add_colored_starting_point(geo, 14, 0, Color(0));
+        add_colored_starting_point(geo, 13, 1, Color(0));
+        add_colored_starting_point(geo, 14, 1, Color(0));
+        add_colored_starting_point(geo, 29, 20, Color(1));
+        add_colored_starting_point(geo, 30, 20, Color(1));
+        add_colored_starting_point(geo, 29, 21, Color(1));
+        add_colored_starting_point(geo, 30, 21, Color(1));
+        break;
+    case BoardType::gembloq_3:
+        add_colored_starting_point(geo, 25, 24, Color(0));
+        add_colored_starting_point(geo, 26, 24, Color(0));
+        add_colored_starting_point(geo, 25, 25, Color(0));
+        add_colored_starting_point(geo, 26, 25, Color(0));
+        add_colored_starting_point(geo, 1, 6, Color(1));
+        add_colored_starting_point(geo, 2, 6, Color(1));
+        add_colored_starting_point(geo, 1, 7, Color(1));
+        add_colored_starting_point(geo, 2, 7, Color(1));
+        add_colored_starting_point(geo, 49, 6, Color(2));
+        add_colored_starting_point(geo, 50, 6, Color(2));
+        add_colored_starting_point(geo, 49, 7, Color(2));
+        add_colored_starting_point(geo, 50, 7, Color(2));
+        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..c156942
--- /dev/null
@@ -0,0 +1,27 @@
+//-----------------------------------------------------------------------------
+/** @file SymmetricPoints.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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..204d878
--- /dev/null
@@ -0,0 +1,90 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/TreeUtil.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "TreeUtil.h"
+
+#include "NodeUtil.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+const SgfNode* get_move_node(const PentobiTree& tree, const SgfNode& node,
+                             unsigned n)
+{
+    auto move_number = get_move_number(tree, node);
+    if (n == move_number)
+        return &node;
+    if (n < move_number)
+    {
+        auto current = &node;
+        do
+        {
+            if (tree.has_move(*current))
+            {
+                if (move_number == n)
+                    return current;
+                --move_number;
+            }
+            if (libpentobi_base::has_setup(*current))
+                break;
+            current = current->get_parent_or_null();
+        }
+        while (current != nullptr);
+    }
+    else
+    {
+        auto current = node.get_first_child_or_null();
+        while (current != nullptr)
+        {
+            if (libpentobi_base::has_setup(*current))
+                break;
+            if (tree.has_move(*current))
+            {
+                ++move_number;
+                if (move_number == n)
+                    return current;
+            }
+            current = current->get_first_child_or_null();
+        }
+    }
+    return nullptr;
+}
+
+unsigned get_move_number(const PentobiTree& tree, const SgfNode& node)
+{
+    unsigned move_number = 0;
+    auto current = &node;
+    do
+    {
+        if (tree.has_move(*current))
+            ++move_number;
+        if (libpentobi_base::has_setup(*current))
+            break;
+        current = current->get_parent_or_null();
+    }
+    while (current != nullptr);
+    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 != nullptr)
+    {
+        if (libpentobi_base::has_setup(*current))
+            break;
+        if (tree.has_move(*current))
+            ++moves_left;
+        current = current->get_first_child_or_null();
+    }
+    return moves_left;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
diff --git a/src/libpentobi_base/TreeUtil.h b/src/libpentobi_base/TreeUtil.h
new file mode 100644 (file)
index 0000000..b1a7ea7
--- /dev/null
@@ -0,0 +1,35 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+
+//-----------------------------------------------------------------------------
+
+/** Get the node with a given move number in the variation of a given node. */
+const SgfNode* get_move_node(const PentobiTree& tree, const SgfNode& node,
+                             unsigned n);
+
+/** 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);
+
+//-----------------------------------------------------------------------------
+
+} // 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..28ba1ef
--- /dev/null
@@ -0,0 +1,121 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/TrigonGeometry.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "TrigonGeometry.h"
+
+#include <map>
+#include <memory>
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_base::CoordPoint;
+
+//-----------------------------------------------------------------------------
+
+TrigonGeometry::TrigonGeometry(unsigned sz)
+{
+    m_sz = sz;
+    Geometry::init(sz * 4 - 1, sz * 2);
+}
+
+const TrigonGeometry& TrigonGeometry::get(unsigned sz)
+{
+    static map<unsigned, shared_ptr<TrigonGeometry>> s_geometry;
+
+    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
+{
+    // See Geometry::get_diag_coord() about advantageous ordering of the list
+    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;
+        return y % 2 != 0 ? 1 : 0;
+    }
+    if (x % 2 != 0)
+        return y % 2 == 0 ? 1 : 0;
+    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..a456d03
--- /dev/null
@@ -0,0 +1,61 @@
+//-----------------------------------------------------------------------------
+/** @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 "Geometry.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** 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);
+
+
+    explicit TrigonGeometry(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:
+    unsigned m_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..0f1a9dc
--- /dev/null
@@ -0,0 +1,131 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/TrigonTransform.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "TrigonTransform.h"
+
+#include <cmath>
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonIdentity::get_transformed(CoordPoint p) const
+{
+    return p;
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRefl::get_transformed(CoordPoint p) const
+{
+    return {-p.x, p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot60::get_transformed(CoordPoint p) const
+{
+    auto px = static_cast<float>(p.x);
+    auto py = static_cast<float>(p.y);
+    auto x = static_cast<int>(std::ceil(0.5f * px - 1.5f * py));
+    auto y = static_cast<int>(std::floor(0.5f * px + 0.5f * py));
+    return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot120::get_transformed(CoordPoint p) const
+{
+    auto px = static_cast<float>(p.x);
+    auto py = static_cast<float>(p.y);
+    auto x = static_cast<int>(std::ceil(-0.5f * px - 1.5f * py));
+    auto y = static_cast<int>(std::ceil(0.5f * px - 0.5f * py));
+    return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot180::get_transformed(CoordPoint p) const
+{
+    return {-p.x, -p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot240::get_transformed(CoordPoint p) const
+{
+    auto px = static_cast<float>(p.x);
+    auto py = static_cast<float>(p.y);
+    auto x = static_cast<int>(std::floor(-0.5f * px + 1.5f * py));
+    auto y = static_cast<int>(std::ceil(-0.5f * px - 0.5f * py));
+    return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot300::get_transformed(CoordPoint p) const
+{
+    auto px = static_cast<float>(p.x);
+    auto py = static_cast<float>(p.y);
+    auto x = static_cast<int>(std::floor(0.5f * px + 1.5f * py));
+    auto y = static_cast<int>(std::floor(-0.5f * px + 0.5f * py));
+    return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot60::get_transformed(CoordPoint p) const
+{
+    auto px = static_cast<float>(p.x);
+    auto py = static_cast<float>(p.y);
+    auto x = static_cast<int>(std::ceil(0.5f * (-px) - 1.5f * py));
+    auto y = static_cast<int>(std::floor(0.5f * (-px) + 0.5f * py));
+    return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot120::get_transformed(CoordPoint p) const
+{
+    auto px = static_cast<float>(p.x);
+    auto py = static_cast<float>(p.y);
+    auto x = static_cast<int>(std::ceil(-0.5f * (-px) - 1.5f * py));
+    auto y = static_cast<int>(std::ceil(0.5f * (-px) - 0.5f * py));
+    return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot180::get_transformed(CoordPoint p) const
+{
+    return {p.x, -p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot240::get_transformed(CoordPoint p) const
+{
+    auto px = static_cast<float>(p.x);
+    auto py = static_cast<float>(p.y);
+    auto x = static_cast<int>(std::floor(-0.5f * (-px) + 1.5f * py));
+    auto y = static_cast<int>(std::ceil(-0.5f * (-px) - 0.5f * py));
+    return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot300::get_transformed(CoordPoint p) const
+{
+    auto px = static_cast<float>(p.x);
+    auto py = static_cast<float>(p.y);
+    auto x = static_cast<int>(std::floor(0.5f * (-px) + 1.5f * py));
+    auto y = static_cast<int>(std::floor(-0.5f * (-px) + 0.5f * py));
+    return {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..1ec9801
--- /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(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot60
+    : public Transform
+{
+public:
+    TransfTrigonRot60() : Transform(1) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot120
+    : public Transform
+{
+public:
+    TransfTrigonRot120() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot180
+    : public Transform
+{
+public:
+    TransfTrigonRot180() : Transform(1) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot240
+    : public Transform
+{
+public:
+    TransfTrigonRot240() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot300
+    : public Transform
+{
+public:
+    TransfTrigonRot300() : Transform(1) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRefl
+    : public Transform
+{
+public:
+    TransfTrigonRefl() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot60
+    : public Transform
+{
+public:
+    TransfTrigonReflRot60() : Transform(1) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot120
+    : public Transform
+{
+public:
+    TransfTrigonReflRot120() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot180
+    : public Transform
+{
+public:
+    TransfTrigonReflRot180() : Transform(1) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot240
+    : public Transform
+{
+public:
+    TransfTrigonReflRot240() : Transform(0) {}
+
+    CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot300
+    : public Transform
+{
+public:
+    TransfTrigonReflRot300() : Transform(1) {}
+
+    CoordPoint get_transformed(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..9148e7e
--- /dev/null
@@ -0,0 +1,569 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Variant.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Variant.h"
+
+#include "CallistoGeometry.h"
+#include "GembloQGeometry.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:
+    case Variant::callisto_2_4:
+        result = BoardType::callisto;
+        break;
+    case Variant::callisto_2:
+        result = BoardType::callisto_2;
+        break;
+    case Variant::callisto_3:
+        result = BoardType::callisto_3;
+        break;
+    case Variant::gembloq:
+    case Variant::gembloq_2_4:
+        result = BoardType::gembloq;
+        break;
+    case Variant::gembloq_2:
+        result = BoardType::gembloq_2;
+        break;
+    case Variant::gembloq_3:
+        result = BoardType::gembloq_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();
+        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;
+    case BoardType::gembloq:
+        result = &GembloQGeometry::get(4);
+        break;
+    case BoardType::gembloq_2:
+        result = &GembloQGeometry::get(2);
+        break;
+    case BoardType::gembloq_3:
+        result = &GembloQGeometry::get(3);
+        break;
+    }
+    return *result;
+}
+
+const Geometry& get_geometry(Variant variant)
+{
+    return get_geometry(get_board_type(variant));
+}
+
+GeometryType get_geometry_type(Variant variant)
+{
+    GeometryType result = GeometryType::classic; // Init to avoid compiler warning
+    switch (variant)
+    {
+    case Variant::classic:
+    case Variant::classic_2:
+    case Variant::classic_3:
+    case Variant::duo:
+    case Variant::junior:
+        result = GeometryType::classic;
+        break;
+    case Variant::trigon:
+    case Variant::trigon_2:
+    case Variant::trigon_3:
+        result = GeometryType::trigon;
+        break;
+    case Variant::nexos:
+    case Variant::nexos_2:
+        result = GeometryType::nexos;
+        break;
+    case Variant::callisto:
+    case Variant::callisto_2:
+    case Variant::callisto_2_4:
+    case Variant::callisto_3:
+        result = GeometryType::callisto;
+        break;
+    case Variant::gembloq:
+    case Variant::gembloq_2:
+    case Variant::gembloq_2_4:
+    case Variant::gembloq_3:
+        result = GeometryType::gembloq;
+        break;
+    }
+    return result;
+}
+
+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:
+    case Variant::gembloq_2:
+        result = 2;
+        break;
+    case Variant::trigon_3:
+    case Variant::callisto_3:
+    case Variant::gembloq_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:
+    case Variant::callisto_2_4:
+    case Variant::gembloq:
+    case Variant::gembloq_2_4:
+        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:
+    case Variant::callisto_2_4:
+    case Variant::gembloq_2:
+    case Variant::gembloq_2_4:
+        result = 2;
+        break;
+    case Variant::classic_3:
+    case Variant::trigon_3:
+    case Variant::callisto_3:
+    case Variant::gembloq_3:
+        result = 3;
+        break;
+    case Variant::classic:
+    case Variant::trigon:
+    case Variant::nexos:
+    case Variant::callisto:
+    case Variant::gembloq:
+        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_2_4:
+    case Variant::callisto_3:
+        result = PieceSet::callisto;
+        break;
+    case Variant::gembloq:
+    case Variant::gembloq_2:
+    case Variant::gembloq_2_4:
+    case Variant::gembloq_3:
+        result = PieceSet::gembloq;
+        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::gembloq:
+    case BoardType::gembloq_2:
+    case BoardType::gembloq_3:
+    case BoardType::nexos:
+        break;
+    case BoardType::trigon_3:
+        // Can we use the same as for BoardType::trigon?
+        break;
+    }
+}
+
+bool has_central_symmetry(Variant variant)
+{
+    return variant == Variant::duo || variant == Variant::junior
+            || variant == Variant::trigon_2 || variant == Variant::callisto_2
+            || variant == Variant::gembloq_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 two-player four-color")
+        variant = Variant::callisto_2_4;
+    else if (t == "callisto three-player")
+        variant = Variant::callisto_3;
+    else if (t == "gembloq")
+        variant = Variant::gembloq;
+    else if (t == "gembloq two-player")
+        variant = Variant::gembloq_2;
+    else if (t == "gembloq two-player four-color")
+        variant = Variant::gembloq_2_4;
+    else if (t == "gembloq three-player")
+        variant = Variant::gembloq_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_2_4" || t == "ca24")
+        variant = Variant::callisto_2_4;
+    else if (t == "callisto_3" || t == "ca3")
+        variant = Variant::callisto_3;
+    else if (t == "gembloq" || t == "g")
+        variant = Variant::gembloq;
+    else if (t == "gembloq_2" || t == "g2")
+        variant = Variant::gembloq_2;
+    else if (t == "gembloq_2_4" || t == "g24")
+        variant = Variant::gembloq_2_4;
+    else if (t == "gembloq_3" || t == "g3")
+        variant = Variant::gembloq_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_2_4:
+        result = "Callisto Two-Player Four-Color";
+        break;
+    case Variant::callisto_3:
+        result = "Callisto Three-Player";
+        break;
+    case Variant::gembloq:
+        result = "GembloQ";
+        break;
+    case Variant::gembloq_2:
+        result = "GembloQ Two-Player";
+        break;
+    case Variant::gembloq_2_4:
+        result = "GembloQ Two-Player Four-Color";
+        break;
+    case Variant::gembloq_3:
+        result = "GembloQ 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_2_4:
+        result = "callisto_2_4";
+        break;
+    case Variant::callisto_3:
+        result = "callisto_3";
+        break;
+    case Variant::gembloq:
+        result = "gembloq";
+        break;
+    case Variant::gembloq_2:
+        result = "gembloq_2";
+        break;
+    case Variant::gembloq_2_4:
+        result = "gembloq_2_4";
+        break;
+    case Variant::gembloq_3:
+        result = "gembloq_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..03181f8
--- /dev/null
@@ -0,0 +1,187 @@
+//-----------------------------------------------------------------------------
+/** @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,
+
+    gembloq
+};
+
+//-----------------------------------------------------------------------------
+
+enum class BoardType
+{
+    classic,
+
+    duo,
+
+    trigon,
+
+    trigon_3,
+
+    nexos,
+
+    callisto,
+
+    callisto_2,
+
+    callisto_3,
+
+    gembloq,
+
+    gembloq_2,
+
+    gembloq_3
+};
+
+//-----------------------------------------------------------------------------
+
+enum class GeometryType
+{
+    classic,
+
+    trigon,
+
+    nexos,
+
+    callisto,
+
+    gembloq
+};
+
+//-----------------------------------------------------------------------------
+
+/** Game variant. */
+enum class Variant
+{
+    classic,
+
+    classic_2,
+
+    classic_3,
+
+    duo,
+
+    junior,
+
+    trigon,
+
+    trigon_2,
+
+    trigon_3,
+
+    nexos,
+
+    nexos_2,
+
+    callisto,
+
+    callisto_2,
+
+    /** Callisto two-player four-color. */
+    callisto_2_4,
+
+    callisto_3,
+
+    gembloq,
+
+    gembloq_2,
+
+    /** GembloQ two-player four-color. */
+    gembloq_2_4,
+
+    gembloq_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);
+
+GeometryType get_geometry_type(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_kde_thumbnailer/CMakeLists.txt b/src/libpentobi_kde_thumbnailer/CMakeLists.txt
new file mode 100644 (file)
index 0000000..f119061
--- /dev/null
@@ -0,0 +1,45 @@
+# 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).
+
+find_package(Qt5Gui 5.9 REQUIRED)
+
+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/Reader.cpp
+  ../libboardgame_sgf/SgfError.cpp
+  ../libboardgame_sgf/SgfNode.cpp
+  ../libboardgame_sgf/SgfTree.cpp
+  ../libboardgame_sgf/TreeReader.cpp
+  ../libpentobi_base/CallistoGeometry.cpp
+  ../libpentobi_base/GembloQGeometry.cpp
+  ../libpentobi_base/NexosGeometry.cpp
+  ../libpentobi_base/NodeUtil.cpp
+  ../libpentobi_base/TrigonGeometry.cpp
+  ../libpentobi_base/Variant.cpp
+  ../libpentobi_paint/Paint.cpp
+  ../libpentobi_thumbnail/CreateThumbnail.cpp
+)
+
+target_include_directories(pentobi_kde_thumbnailer PRIVATE ..)
+
+target_compile_definitions(pentobi_kde_thumbnailer PRIVATE
+    QT_DEPRECATED_WARNINGS
+    QT_DISABLE_DEPRECATED_BEFORE=0x051100)
+
+target_link_libraries(pentobi_kde_thumbnailer Qt5::Gui)
diff --git a/src/libpentobi_mcts/AnalyzeGame.cpp b/src/libpentobi_mcts/AnalyzeGame.cpp
new file mode 100644 (file)
index 0000000..df5e322
--- /dev/null
@@ -0,0 +1,130 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/AnalyzeGame.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "AnalyzeGame.h"
+
+#include "Search.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/WallTimeSource.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_sgf::SgfError;
+using libboardgame_util::clear_abort;
+using libboardgame_util::get_abort;
+using libboardgame_util::WallTimeSource;
+using libpentobi_base::BoardUpdater;
+
+//-----------------------------------------------------------------------------
+
+void AnalyzeGame::clear()
+{
+    m_moves.clear();
+    m_values.clear();
+}
+
+void AnalyzeGame::run(const Game& game, Search& search, size_t nu_simulations,
+                      const 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;
+    do
+    {
+        if (tree.has_move(*node))
+            ++total_moves;
+        node = node->get_first_child_or_null();
+    }
+    while (node != nullptr);
+    WallTimeSource time_source;
+    clear_abort();
+    node = &root;
+    unsigned move_number = 0;
+    auto tie_value = Search::SearchParamConst::tie_value;
+    const auto 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 dummy;
+    do
+    {
+        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(static_cast<double>(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());
+                    search.search(dummy, *bd, mv.color, max_count,
+                                  min_simulations, max_time, time_source);
+                    if (get_abort())
+                        break;
+                    m_moves.push_back(mv);
+                    m_values.push_back(static_cast<double>(
+                                           search.get_root_val().get_mean()));
+                }
+                catch (const SgfError&)
+                {
+                    break;
+                }
+            }
+            ++move_number;
+        }
+        if (! node->has_children())
+        {
+            updater.update(*bd, tree, *node);
+            LIBBOARDGAME_LOG("Analyzing last position");
+            Color c;
+            if (bd->is_game_over() && ! m_moves.empty())
+                // If game is over, analyze last position from viewpoint of
+                // color that played the last move to avoid using a color that
+                // might have run out of moves much earlier.
+                c = m_moves.back().color;
+            else
+                c = bd->get_effective_to_play();
+            search.search(dummy, *bd, c, max_count, min_simulations, max_time,
+                          time_source);
+            if (get_abort())
+                break;
+            m_moves.emplace_back(c, Move::null());
+            m_values.push_back(static_cast<double>(
+                                   search.get_root_val().get_mean()));
+        }
+        node = node->get_first_child_or_null();
+    }
+    while (node != nullptr);
+}
+
+void AnalyzeGame::set(Variant variant, const vector<ColorMove>& moves,
+                      const vector<double>& values)
+{
+    LIBBOARDGAME_ASSERT(moves.size() == values.size());
+    m_variant = variant;
+    m_moves = moves;
+    m_values = values;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
diff --git a/src/libpentobi_mcts/AnalyzeGame.h b/src/libpentobi_mcts/AnalyzeGame.h
new file mode 100644 (file)
index 0000000..0cb4cdd
--- /dev/null
@@ -0,0 +1,88 @@
+//-----------------------------------------------------------------------------
+/** @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:
+    void clear();
+
+    /** 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,
+             const 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;
+
+    void set(Variant variant, const vector<ColorMove>& moves,
+             const vector<double>& values);
+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..05020a8
--- /dev/null
@@ -0,0 +1,37 @@
+set(LIBPENTOBI_MCTS_FLOAT_TYPE "float" CACHE STRING
+    "Floating-point type for MCTS values")
+
+add_library(pentobi_mcts STATIC
+  AnalyzeGame.h
+  AnalyzeGame.cpp
+  Float.h
+  History.h
+  History.cpp
+  LocalPoints.h
+  LocalPoints.cpp
+  Player.h
+  Player.cpp
+  PlayoutFeatures.h
+  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
+)
+
+if(NOT LIBPENTOBI_MCTS_FLOAT_TYPE STREQUAL "float")
+  target_compile_definitions(pentobi_mcts PUBLIC
+      LIBPENTOBI_MCTS_FLOAT_TYPE=${LIBPENTOBI_MCTS_FLOAT_TYPE})
+endif()
+
+target_include_directories(pentobi_mcts PUBLIC ..)
+
+target_link_libraries(pentobi_mcts pentobi_base boardgame_mcts)
diff --git a/src/libpentobi_mcts/Float.h b/src/libpentobi_mcts/Float.h
new file mode 100644 (file)
index 0000000..9379459
--- /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
+using Float = LIBPENTOBI_MCTS_FLOAT_TYPE;
+#else
+using 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..1670fc2
--- /dev/null
@@ -0,0 +1,73 @@
+//----------------------------------------------------------------------------
+/** @file libpentobi_mcts/History.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//----------------------------------------------------------------------------
+
+#include "History.h"
+
+#include "libpentobi_base/BoardUtil.h"
+
+namespace libpentobi_mcts {
+
+using namespace std;
+using libpentobi_base::get_current_position_as_setup;
+
+//----------------------------------------------------------------------------
+
+void History::get_as_setup(Variant& variant, Setup& setup) const
+{
+    LIBBOARDGAME_ASSERT(is_valid());
+    variant = m_variant;
+    auto bd = make_unique<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..83f14c3
--- /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_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/LocalPoints.cpp b/src/libpentobi_mcts/LocalPoints.cpp
new file mode 100644 (file)
index 0000000..d67e73b
--- /dev/null
@@ -0,0 +1,20 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/LocalPoints.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "LocalPoints.h"
+
+namespace libpentobi_mcts {
+
+//-----------------------------------------------------------------------------
+
+LocalPoints::LocalPoints()
+{
+    m_is_local.fill_all(false);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
diff --git a/src/libpentobi_mcts/LocalPoints.h b/src/libpentobi_mcts/LocalPoints.h
new file mode 100644 (file)
index 0000000..51d2e31
--- /dev/null
@@ -0,0 +1,87 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/LocalPoints.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_LOCAL_POINTS_H
+#define LIBPENTOBI_MCTS_LOCAL_POINTS_H
+
+#include "libpentobi_base/Board.h"
+#include "libpentobi_base/Grid.h"
+
+namespace libpentobi_mcts {
+
+using libpentobi_base::Board;
+using libpentobi_base::BoardConst;
+using libpentobi_base::Color;
+using libpentobi_base::GridExt;
+using libpentobi_base::Point;
+using libpentobi_base::PointList;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+/** Find attach points of recent opponent moves on the board. */
+class LocalPoints
+{
+public:
+    LocalPoints();
+
+    template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+    void init(const Board& bd);
+
+    bool contains(Point p) const { return m_is_local[p]; }
+
+private:
+    GridExt<bool> m_is_local;
+
+    /** Points in m_is_local with value true. */
+    PointList m_points;
+};
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+inline void LocalPoints::init(const Board& bd)
+{
+    for (Point p : m_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] && ! m_is_local[*j])
+            {
+                m_points.get_unchecked(nu_local++) = *j;
+                m_is_local[*j] = true;
+            }
+        while (++j != end);
+    }
+    m_points.resize(nu_local);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_LOCAL_POINTS_H
diff --git a/src/libpentobi_mcts/Player.cpp b/src/libpentobi_mcts/Player.cpp
new file mode 100644 (file)
index 0000000..1235a64
--- /dev/null
@@ -0,0 +1,369 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/Player.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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.
+// * We only calibrate the numbers for the game variants we care most about.
+//   For other game variants, we use the numbers of game variants with similar
+//   playing strength and speed of simulations.
+
+const float counts_classic[Player::max_supported_level] =
+    { 3, 30, 90, 181, 667, 5028, 69809, 349044, 1745221 };
+
+const float counts_duo[Player::max_supported_level] =
+    { 3, 21, 77, 213, 861, 7280, 221867, 1109339, 5546695 };
+
+const float counts_trigon[Player::max_supported_level] =
+    { 100, 246, 457, 876, 1882, 5506, 19819, 99092, 495465 };
+
+const float counts_nexos[Player::max_supported_level] =
+    { 250, 347, 625, 1223, 3117, 8270, 20954, 104774, 523877 };
+
+const float counts_callisto_2[Player::max_supported_level] =
+    { 30, 87, 300, 1017, 4729, 20435, 122778, 613905, 3069529 };
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+Player::Player(Variant initial_variant, unsigned max_level,
+               const string&  books_dir, unsigned nu_threads)
+    : m_is_book_loaded(false),
+      m_use_book(true),
+      m_resign(false),
+      m_books_dir(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;
+        }
+    }
+}
+
+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:
+        case BoardType::gembloq_2:
+            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:
+        case BoardType::gembloq:
+        case BoardType::gembloq_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:
+            case BoardType::gembloq_2:
+                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:
+            case BoardType::gembloq:
+            case BoardType::gembloq_3:
+                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
+    size_t reasonable = available / 4;
+    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 calibrated and the ratings are also used for other game
+    // variants that we assume have comparable strength (e.g. multi-player 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: // Measured for classic_2
+        {
+            // Anchor 1000, scale 0.6
+            static double elo[Player::max_supported_level] =
+                { 1000, 1142, 1283, 1425, 1567, 1708, 1850, 1951, 2030 };
+            result = Rating(elo[level - 1]);
+        }
+        break;
+    case BoardType::duo:
+        {
+            // Anchor 1000, scale 0.74
+            static double elo[Player::max_supported_level] =
+                { 1000, 1189, 1378, 1567, 1755, 1945, 2134, 2185, 2209 };
+            result = Rating(elo[level - 1]);
+        }
+        break;
+    case BoardType::callisto_2:
+        {
+            // Anchor 1000, scale 0.49
+            static double elo[Player::max_supported_level] =
+                { 1000, 1113, 1225, 1338, 1450, 1563, 1675, 1783, 1868 };
+            result = Rating(elo[level - 1]);
+        }
+        break;
+    case BoardType::trigon: // Measured for trigon_2
+    case BoardType::trigon_3:
+        {
+            // Anchor 1000, scale 0.48
+            static double elo[Player::max_supported_level] =
+                { 1000, 1110, 1220, 1330, 1440, 1550, 1660, 1765, 1897 };
+            result = Rating(elo[level - 1]);
+        }
+        break;
+    case BoardType::nexos: // Measured for nexos_2
+    case BoardType::callisto:
+    case BoardType::callisto_3:
+    case BoardType::gembloq:
+    case BoardType::gembloq_2:
+    case BoardType::gembloq_3:
+        {
+            // Anchor 1000, scale 0.60
+            static double 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 = make_unique<CpuTimeSource>();
+    else
+        m_time_source = make_unique<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..74641d1
--- /dev/null
@@ -0,0 +1,192 @@
+//-----------------------------------------------------------------------------
+/** @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, const string& books_dir,
+           unsigned nu_threads = 0);
+
+    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.h b/src/libpentobi_mcts/PlayoutFeatures.h
new file mode 100644 (file)
index 0000000..280708b
--- /dev/null
@@ -0,0 +1,228 @@
+//-----------------------------------------------------------------------------
+/** @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 libpentobi_base::Board;
+using libpentobi_base::BoardConst;
+using libpentobi_base::Color;
+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:
+    /** Integer type used in the implementation.
+        Should be fast and have enough space for the masks used. Note that
+        logically, we want uint_fast32_t, but that is an 8-byte unsigned
+        with GCC on Intel CPUs, which is *slower* than a 4-byte unsigned. */
+    using IntType = uint_least32_t;
+
+    /** 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;
+    static_assert(max_local < 0x01000u, ""); // Value for forbidden status
+    static_assert(PieceInfo::max_size <= 0xff, ""); // Mask for forbidden status
+
+    /** 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 & 0xff000u) != 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;
+
+
+    /** 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, bool IS_CALLISTO>
+    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;
+
+
+    void add_local(const Board& bd, Point p, Color to_play,
+                   unsigned& nu_local);
+};
+
+
+inline void PlayoutFeatures::add_local(const Board& bd, Point p, Color to_play,
+                                       unsigned& nu_local)
+{
+    if (m_point_value[p] == 0)
+    {
+        m_local_points.get_unchecked(nu_local++) = p;
+        m_point_value[p] =
+                1 + static_cast<IntType>(bd.is_attach_point(p, to_play));
+    }
+}
+
+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] ? 0x01000u : 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] = 0x01000u;
+    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] = 0x01000u;
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+inline void PlayoutFeatures::set_local(const Board& bd)
+{
+    // Clear old info about local points
+    for (Point p : m_local_points)
+        m_point_value[p] &= 0xff000u;
+    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;
+            add_local(bd, *j, to_play, nu_local);
+            if (MAX_SIZE == 7 || IS_CALLISTO)
+            {
+                // Nexos or Callisto don't use adjacent points, use 2nd-order
+                // "diagonal" points instead
+                LIBBOARDGAME_ASSERT(geo.get_adj(*j).empty());
+                for (Point k : geo.get_diag(*j))
+                    if (! is_forbidden[k])
+                        add_local(bd, k, to_play, nu_local);
+            }
+            else
+                for (Point k : geo.get_adj(*j))
+                    if (! is_forbidden[k])
+                        add_local(bd, k, to_play, nu_local);
+        }
+        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..5f27191
--- /dev/null
@@ -0,0 +1,418 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/PriorKnowledge.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PriorKnowledge.h"
+
+#include <cmath>
+
+namespace libpentobi_mcts {
+
+using libpentobi_base::BoardType;
+using libpentobi_base::GeometryType;
+using libpentobi_base::Color;
+using libpentobi_base::PointState;
+using libpentobi_base::PieceSet;
+
+//-----------------------------------------------------------------------------
+
+PriorKnowledge::PriorKnowledge(const Board& bd)
+{
+    init_variant(bd);
+}
+
+void PriorKnowledge::init_variant(const Board& bd)
+{
+    auto variant = bd.get_variant();
+    m_variant = variant;
+    auto& geo = bd.get_geometry();
+    auto board_type = bd.get_board_type();
+    auto piece_set = bd.get_piece_set();
+    auto geometry_type = bd.get_geometry_type();
+
+    // Init m_dist_to_center
+    auto width = static_cast<float>(geo.get_width());
+    auto 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)
+    {
+        auto x = static_cast<float>(geo.get_x(p));
+        auto 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 (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:
+    case Variant::callisto_2_4:
+        m_check_dist_to_center.fill(true);
+        m_dist_to_center_max_pieces = 8;
+        m_max_dist_diff = 4;
+        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 = 6;
+        m_max_dist_diff = 3;
+        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;
+    case Variant::gembloq:
+    case Variant::gembloq_2_4:
+        m_check_dist_to_center.fill(true);
+        m_dist_to_center_max_pieces = 12;
+        m_max_dist_diff = 0.5f;
+        break;
+    case Variant::gembloq_3:
+        m_check_dist_to_center.fill(true);
+        m_dist_to_center_max_pieces = 9;
+        m_max_dist_diff = 0.5f;
+        break;
+    case Variant::gembloq_2:
+        m_check_dist_to_center.fill(true);
+        m_dist_to_center_max_pieces = 4;
+        m_max_dist_diff = 0.5f;
+        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;
+        }
+
+    // Init gammas. The values are learned using pentobi/src/learn_tool.
+    Float gamma_piece_score_0;
+    Float gamma_piece_score_1;
+    Float gamma_piece_score_2;
+    Float gamma_piece_score_3;
+    Float gamma_piece_score_4;
+    Float gamma_piece_score_5;
+    Float gamma_piece_score_6;
+    if (variant == Variant::duo || variant == Variant::junior)
+    {
+        Float temperature = 0.84f;
+        // Tuned for duo
+        m_gamma_point_other = exp(0.394f / temperature);
+        m_gamma_point_opp_attach_or_nb = exp(1.399f / temperature);
+        m_gamma_point_second_color_attach = 1; // unused
+        m_gamma_adj_connect = 1; // unused
+        m_gamma_adj_occupied_other = exp(0.359f / temperature);
+        m_gamma_adj_forbidden_other = exp(0.404f / temperature);
+        m_gamma_adj_own_attach = exp(-1.164f / temperature);
+        m_gamma_adj_nonforbidden = exp(-0.461f / temperature);
+        m_gamma_attach_to_play = exp(-0.082f / temperature);
+        m_gamma_attach_forbidden_other = exp(-0.305f / temperature);
+        m_gamma_attach_nonforbidden[0] = exp(-0.200f / temperature);
+        m_gamma_attach_nonforbidden[1] = exp(-0.116f / temperature);
+        m_gamma_attach_nonforbidden[2] = exp(0.331f / temperature);
+        m_gamma_attach_nonforbidden[3] = exp(0.588f / temperature);
+        m_gamma_attach_nonforbidden[4] = exp(0.923f / temperature);
+        m_gamma_attach_nonforbidden[5] = 1; // unused
+        m_gamma_attach_nonforbidden[6] = 1; // unused
+        m_gamma_attach_second_color = 1; // unused
+        m_gamma_local = exp(0.336f / temperature);
+        gamma_piece_score_0 = 1; // unused
+        gamma_piece_score_1 = exp(0.330f / temperature);
+        gamma_piece_score_2 = exp(-0.402f / temperature);
+        gamma_piece_score_3 = exp(-0.845f / temperature);
+        gamma_piece_score_4 = exp(-0.245f / temperature);
+        gamma_piece_score_5 = exp(1.149f / temperature);
+        gamma_piece_score_6 = 1; // unused
+    }
+    else if (variant == Variant::callisto_2)
+    {
+        Float temperature = 0.84f;
+        m_gamma_point_other = exp(0.305f / temperature);
+        m_gamma_point_opp_attach_or_nb = exp(2.047f / temperature);
+        m_gamma_point_second_color_attach = 1; // unused
+        m_gamma_adj_connect = exp(-0.022f / temperature);
+        m_gamma_adj_occupied_other = exp(-0.049f / temperature);
+        m_gamma_adj_forbidden_other = 1; // unused
+        m_gamma_adj_own_attach = exp(-0.631f / temperature);
+        m_gamma_adj_nonforbidden = exp(0.221f / temperature);
+        m_gamma_attach_to_play = exp(-0.273f / temperature);
+        m_gamma_attach_forbidden_other = exp(-0.597f / temperature);
+        m_gamma_attach_nonforbidden[0] = 1; // unused
+        m_gamma_attach_nonforbidden[1] = exp(-0.070f / temperature);
+        m_gamma_attach_nonforbidden[2] = exp(0.140f / temperature);
+        m_gamma_attach_nonforbidden[3] = exp(0.075f / temperature);
+        m_gamma_attach_nonforbidden[4] = exp(0.199f / temperature);
+        m_gamma_attach_nonforbidden[5] = 1; // unused
+        m_gamma_attach_nonforbidden[6] = 1; // unused
+        m_gamma_attach_second_color = 1; // unused
+        m_gamma_local = exp(0.203f / temperature);
+        gamma_piece_score_0 = exp(0.942f / temperature);
+        gamma_piece_score_1 = 1; // unused
+        gamma_piece_score_2 = exp(-1.642f / temperature);
+        gamma_piece_score_3 = exp(-0.800f / temperature);
+        gamma_piece_score_4 = exp(0.436f / temperature);
+        gamma_piece_score_5 = exp(1.082f / temperature);
+        gamma_piece_score_6 = 1; // unused
+    }
+    else if (variant == Variant::gembloq_2)
+    {
+        Float temperature = 0.84f;
+        m_gamma_point_other = exp(0.120f / temperature);
+        m_gamma_point_opp_attach_or_nb = exp(0.315f / temperature);
+        m_gamma_point_second_color_attach = 1; // unused
+        m_gamma_adj_connect = 1; // unused
+        m_gamma_adj_occupied_other = exp(0.350f / temperature);
+        m_gamma_adj_forbidden_other = exp(0.511f / temperature);
+        m_gamma_adj_own_attach = exp(0.285f / temperature);
+        m_gamma_adj_nonforbidden = exp(0.095f / temperature);
+        m_gamma_attach_to_play = exp(0.181f / temperature);
+        m_gamma_attach_forbidden_other = exp(-0.127f / temperature);
+        m_gamma_attach_nonforbidden[0] = exp(-0.165f / temperature);
+        m_gamma_attach_nonforbidden[1] = exp(-0.031f / temperature);
+        m_gamma_attach_nonforbidden[2] = exp(-0.058f / temperature);
+        m_gamma_attach_nonforbidden[3] = exp(-0.057f / temperature);
+        m_gamma_attach_nonforbidden[4] = 1; // unused
+        m_gamma_attach_nonforbidden[5] = 1; // unused
+        m_gamma_attach_nonforbidden[6] = 1; // unused
+        m_gamma_attach_second_color = 1; // unused
+        m_gamma_local = exp(1.109f / temperature);
+        gamma_piece_score_0 = 1; // unused
+        gamma_piece_score_1 = exp(-0.468f / temperature);
+        gamma_piece_score_2 = exp(-0.647f / temperature);
+        gamma_piece_score_3 = exp(-0.386f / temperature);
+        gamma_piece_score_4 = exp(0.250f / temperature);
+        gamma_piece_score_5 = exp(1.283f / temperature);
+        gamma_piece_score_6 = 1; // unused
+    }
+    else if (piece_set == PieceSet::trigon)
+    {
+        Float temperature = 0.84f;
+        // Tuned for trigon_2
+        m_gamma_point_other = exp(0.182f / temperature);
+        m_gamma_point_opp_attach_or_nb = exp(0.828f / temperature);
+        m_gamma_point_second_color_attach = exp(0.016f / temperature);
+        m_gamma_adj_connect = exp(1.032f / temperature);
+        m_gamma_adj_occupied_other = exp(1.024f / temperature);
+        m_gamma_adj_forbidden_other = exp(0.671f / temperature);
+        m_gamma_adj_own_attach = exp(0.193f / temperature);
+        m_gamma_adj_nonforbidden = exp(-0.155f / temperature);
+        m_gamma_attach_to_play = exp(-0.153f / temperature);
+        m_gamma_attach_forbidden_other = exp(-0.382f / temperature);
+        m_gamma_attach_nonforbidden[0] = exp(-0.220f / temperature);
+        m_gamma_attach_nonforbidden[1] = exp(-0.263f / temperature);
+        m_gamma_attach_nonforbidden[2] = exp(-0.155f / temperature);
+        m_gamma_attach_nonforbidden[3] = exp(0.059f / temperature);
+        m_gamma_attach_nonforbidden[4] = 1; // unused
+        m_gamma_attach_nonforbidden[5] = 1; // unused
+        m_gamma_attach_nonforbidden[6] = 1; // unused
+        m_gamma_attach_second_color = exp(-0.051f / temperature);
+        m_gamma_local = exp(0.536f / temperature);
+        gamma_piece_score_0 = 1; // unused
+        gamma_piece_score_1 = exp(0.453f / temperature);
+        gamma_piece_score_2 = exp(0.083f / temperature);
+        gamma_piece_score_3 = exp(-0.620f / temperature);
+        gamma_piece_score_4 = exp(-0.687f / temperature);
+        gamma_piece_score_5 = exp(-0.373f / temperature);
+        gamma_piece_score_6 = exp(1.153f / temperature);
+    }
+    else if (piece_set == PieceSet::nexos)
+    {
+        Float temperature = 0.84f;
+        // Tuned for nexos_2
+        m_gamma_point_other = exp(0.601f / temperature);
+        m_gamma_point_opp_attach_or_nb = exp(1.525f / temperature);
+        m_gamma_point_second_color_attach = exp(0.112f / temperature);
+        m_gamma_adj_connect = exp(0.026f / temperature);
+        m_gamma_adj_occupied_other = exp(0.002f / temperature);
+        m_gamma_adj_forbidden_other = 1; // unused
+        m_gamma_adj_own_attach = exp(-0.251f / temperature);
+        m_gamma_adj_nonforbidden = exp(0.036f / temperature);
+        m_gamma_attach_to_play = exp(0.021f / temperature);
+        m_gamma_attach_forbidden_other = exp(-0.037f / temperature);
+        m_gamma_attach_nonforbidden[0] = 1; // unused
+        m_gamma_attach_nonforbidden[1] = exp(0.074f / temperature);
+        m_gamma_attach_nonforbidden[2] = exp(-0.104f / temperature);
+        m_gamma_attach_nonforbidden[3] = exp(-0.067f / temperature);
+        m_gamma_attach_nonforbidden[4] = exp(-0.113f / temperature);
+        m_gamma_attach_nonforbidden[5] = exp(-0.035f / temperature);
+        m_gamma_attach_nonforbidden[6] = exp(0.127f / temperature);
+        m_gamma_attach_second_color = exp(0.075f / temperature);
+        m_gamma_local = exp(1.101f / temperature);
+        gamma_piece_score_0 = 1; // unused
+        gamma_piece_score_1 = exp(-0.167f / temperature);
+        gamma_piece_score_2 = exp(-0.387f / temperature);
+        gamma_piece_score_3 = exp(-0.306f / temperature);
+        gamma_piece_score_4 = exp(0.852f / temperature);
+        gamma_piece_score_5 = 1; // unused
+        gamma_piece_score_6 = 1; // unused
+    }
+    else if (geometry_type == GeometryType::callisto)
+    {
+        Float temperature = 0.84f;
+        // Tuned for callisto_2_4
+        m_gamma_point_other = exp(0.310f / temperature);
+        m_gamma_point_opp_attach_or_nb = exp(2.043f / temperature);
+        m_gamma_point_second_color_attach = exp(-0.017f / temperature);
+        m_gamma_adj_connect = exp(0.189f / temperature);
+        m_gamma_adj_occupied_other = exp(-0.033f / temperature);
+        m_gamma_adj_forbidden_other = 1; // unused
+        m_gamma_adj_own_attach = exp(-0.500f / temperature);
+        m_gamma_adj_nonforbidden = exp(0.100f / temperature);
+        m_gamma_attach_to_play = exp(-0.239f / temperature);
+        m_gamma_attach_forbidden_other = exp(-0.545f / temperature);
+        m_gamma_attach_nonforbidden[0] = 1; // unused
+        m_gamma_attach_nonforbidden[1] = exp(0.152f / temperature);
+        m_gamma_attach_nonforbidden[2] = exp(0.159f / temperature);
+        m_gamma_attach_nonforbidden[3] = exp(0.104f / temperature);
+        m_gamma_attach_nonforbidden[4] = exp(0.122f / temperature);
+        m_gamma_attach_nonforbidden[5] = 1; // unused
+        m_gamma_attach_nonforbidden[6] = 1; // unused
+        m_gamma_attach_second_color = exp(-0.107f / temperature);
+        m_gamma_local = exp(0.182f / temperature);
+        gamma_piece_score_0 = exp(0.823f / temperature);
+        gamma_piece_score_1 = 1; // unused
+        gamma_piece_score_2 = exp(-1.507f / temperature);
+        gamma_piece_score_3 = exp(-0.726f / temperature);
+        gamma_piece_score_4 = exp(0.436f / temperature);
+        gamma_piece_score_5 = exp(1.003f / temperature);
+        gamma_piece_score_6 = 1; // unused
+    }
+    else if (piece_set == PieceSet::gembloq)
+    {
+        Float temperature = 0.84f;
+        // Tuned for gembloq_2_4
+        m_gamma_point_other = exp(0.174f / temperature);
+        m_gamma_point_opp_attach_or_nb = exp(0.304f / temperature);
+        m_gamma_point_second_color_attach = exp(0.098f / temperature);
+        m_gamma_adj_connect = exp(0.296f / temperature);
+        m_gamma_adj_occupied_other = exp(0.314f / temperature);
+        m_gamma_adj_forbidden_other = exp(0.141f / temperature);
+        m_gamma_adj_own_attach = exp(0.138f / temperature);
+        m_gamma_adj_nonforbidden = exp(-0.195f / temperature);
+        m_gamma_attach_to_play = exp(0.191f / temperature);
+        m_gamma_attach_forbidden_other = exp(-0.129f / temperature);
+        m_gamma_attach_nonforbidden[0] = exp(-0.123f / temperature);
+        m_gamma_attach_nonforbidden[1] = exp(0.104f / temperature);
+        m_gamma_attach_nonforbidden[2] = exp(-0.005f / temperature);
+        m_gamma_attach_nonforbidden[3] = exp(0.045f / temperature);
+        m_gamma_attach_nonforbidden[4] = 1; // unused
+        m_gamma_attach_nonforbidden[5] = 1; // unused
+        m_gamma_attach_nonforbidden[6] = 1; // unused
+        m_gamma_attach_second_color = exp(0.128f / temperature);
+        m_gamma_local = exp(1.078f / temperature);
+        gamma_piece_score_0 = 1; // unused
+        gamma_piece_score_1 = exp(-0.094f / temperature);
+        gamma_piece_score_2 = exp(-0.563f / temperature);
+        gamma_piece_score_3 = exp(-0.661f / temperature);
+        gamma_piece_score_4 = exp(-0.021f / temperature);
+        gamma_piece_score_5 = exp(1.349f / temperature);
+        gamma_piece_score_6 = 1; // unused
+    }
+    else
+    {
+        // Tuned for classic_2
+        Float temperature = 0.84f;
+        m_gamma_point_other = exp(0.137f / temperature);
+        m_gamma_point_opp_attach_or_nb = exp(0.898f / temperature);
+        m_gamma_point_second_color_attach = exp(-0.248f / temperature);
+        m_gamma_adj_connect = exp(0.616f / temperature);
+        m_gamma_adj_occupied_other = exp(0.568f / temperature);
+        m_gamma_adj_forbidden_other = exp(0.544f / temperature);
+        m_gamma_adj_own_attach = exp(-0.849f / temperature);
+        m_gamma_adj_nonforbidden = exp(-0.115f / temperature);
+        m_gamma_attach_to_play = exp(0.007f / temperature);
+        m_gamma_attach_forbidden_other = exp(-0.439f / temperature);
+        m_gamma_attach_nonforbidden[0] = exp(-0.177f / temperature);
+        m_gamma_attach_nonforbidden[1] = exp(-0.002f / temperature);
+        m_gamma_attach_nonforbidden[2] = exp(0.232f / temperature);
+        m_gamma_attach_nonforbidden[3] = exp(0.342f / temperature);
+        m_gamma_attach_nonforbidden[4] = exp(0.694f / temperature);
+        m_gamma_attach_nonforbidden[5] = 1; // unused
+        m_gamma_attach_nonforbidden[6] = 1; // unused
+        m_gamma_attach_second_color = exp(-0.011f / temperature);
+        m_gamma_local = exp(0.610f / temperature);
+        gamma_piece_score_0 = 1; // unused
+        gamma_piece_score_1 = exp(0.476f / temperature);
+        gamma_piece_score_2 = exp(-0.316f / temperature);
+        gamma_piece_score_3 = exp(-0.842f / temperature);
+        gamma_piece_score_4 = exp(-0.301f / temperature);
+        gamma_piece_score_5 = exp(0.969f / temperature);
+        gamma_piece_score_6 = 1; // unused
+    }
+    for (Piece::IntType i = 0; i < bd.get_nu_uniq_pieces(); ++i)
+        switch (static_cast<unsigned>(
+                    bd.get_piece_info(Piece(i)).get_score_points()))
+        {
+        case 0: m_gamma_piece_score[Piece(i)] = gamma_piece_score_0; break;
+        case 1: m_gamma_piece_score[Piece(i)] = gamma_piece_score_1; break;
+        case 2: m_gamma_piece_score[Piece(i)] = gamma_piece_score_2; break;
+        case 3: m_gamma_piece_score[Piece(i)] = gamma_piece_score_3; break;
+        case 4: m_gamma_piece_score[Piece(i)] = gamma_piece_score_4; break;
+        case 5: m_gamma_piece_score[Piece(i)] = gamma_piece_score_5; break;
+        default: m_gamma_piece_score[Piece(i)] = gamma_piece_score_6; break;
+        }
+}
+
+void PriorKnowledge::start_search(const Board& bd)
+{
+    if (bd.get_variant() != m_variant)
+        init_variant(bd);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
diff --git a/src/libpentobi_mcts/PriorKnowledge.h b/src/libpentobi_mcts/PriorKnowledge.h
new file mode 100644 (file)
index 0000000..b3c567b
--- /dev/null
@@ -0,0 +1,440 @@
+//-----------------------------------------------------------------------------
+/** @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 "LocalPoints.h"
+#include "SearchParamConst.h"
+#include "libboardgame_mcts/Tree.h"
+#include "libpentobi_base/Board.h"
+
+namespace libpentobi_mcts {
+
+using namespace std;
+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::Piece;
+using libpentobi_base::PieceMap;
+using libpentobi_base::Point;
+using libpentobi_base::PointList;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+/** Initializes newly created nodes with move prior, count and value.
+    Computes move priors of the form exp(phi*x) with a weight vector phi and a
+    feature vector x. These weights can be learned with softmax training from
+    existing games (see pentobi/src/learn_tool).
+
+    The move generation also prunes certain moves in some game variants (e.g.
+    opening moves that don't go towards the center). */
+class PriorKnowledge
+{
+public:
+    using Node =
+        libboardgame_mcts::Node<Move, Float, SearchParamConst::multithread>;
+
+    using Tree = libboardgame_mcts::Tree<Node>;
+
+
+    explicit PriorKnowledge(const Board& bd);
+
+    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 IS_CALLISTO>
+    bool gen_children(const Board& bd, const MoveList& moves,
+                      bool is_symmetry_broken, Tree::NodeExpander& expander,
+                      Float root_val);
+
+private:
+    struct MoveFeatures
+    {
+        /** Heuristic unnormalized probability of the move. */
+        Float gamma;
+
+        /** 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;
+
+    /** @name Gammas for move scores. */
+    /** @{ */
+
+    Float m_gamma_point_other;
+
+    /** Point is opponent attach point or adjacent to it. */
+    Float m_gamma_point_opp_attach_or_nb;
+
+    /** Point is attach point of second color. */
+    Float m_gamma_point_second_color_attach;
+
+    /** Adjacent point connects two own colors. */
+    Float m_gamma_adj_connect;
+
+    /** Adjacent point is occupied by opponent. */
+    Float m_gamma_adj_occupied_other;
+
+    Float m_gamma_adj_forbidden_other;
+
+    /** Adjacent point is own attach point. */
+    Float m_gamma_adj_own_attach;
+
+    /** Adjacent point is not already forbidden. */
+    Float m_gamma_adj_nonforbidden;
+
+    Float m_gamma_attach_to_play;
+
+    Float m_gamma_attach_forbidden_other;
+
+    /** Attach point is attach point of another own color. */
+    Float m_gamma_attach_second_color;
+
+    /** Move occupies an attach point of a recent opponent move. */
+    Float m_gamma_local;
+
+    PieceMap<Float> m_gamma_piece_score;
+
+    /** Attach point is nonforbidden and has n non-forbidden neighbors.
+        Nexos/Callisto use "diagonal" neighbors instead of "adjacent", so the
+        index is [0..6] */
+    array<Float, 7> m_gamma_attach_nonforbidden;
+
+    /** @} */ // @name
+
+    /** Maximum of Features::gamma for all moves. */
+    Float m_max_gamma;
+
+    /** Sum of Features::gamma for all moves. */
+    Float m_sum_gamma;
+
+    bool m_has_connect_move;
+
+    ColorMap<bool> m_check_dist_to_center;
+
+    Variant m_variant;
+
+    unsigned m_dist_to_center_max_pieces;
+
+    float m_min_dist_to_center;
+
+    float m_max_dist_diff;
+
+    LocalPoints m_local_points;
+
+    /** Distance to center heuristic. */
+    GridExt<float> m_dist_to_center;
+
+
+    template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+    void compute_features(const Board& bd, const MoveList& moves,
+                          bool check_dist_to_center, bool check_connect);
+
+    void init_variant(const Board& bd);
+};
+
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+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 feature without
+    // needing an extra check below because adj_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> gamma_point;
+    gamma_point[Point::null()] = 1;
+    Grid<Float> gamma_attach;
+    Grid<Float> gamma_adj;
+    for (Point p : geo)
+    {
+        auto s = bd.get_point_state(p);
+        if (is_forbidden[p])
+        {
+            // No need to initialize gamma_point[p] for forbidden points
+            if (s != to_play)
+                gamma_attach[p] = m_gamma_attach_forbidden_other;
+            else
+                gamma_attach[p] = m_gamma_attach_to_play;
+            if (s == connect_color)
+                gamma_adj[p] = m_gamma_adj_connect;
+            else if (! s.is_empty())
+                // Occupied by opponent (no need to check if s == to_play,
+                // such moves are illegal)
+                gamma_adj[p] = m_gamma_adj_occupied_other;
+            else
+                gamma_adj[p] = m_gamma_adj_forbidden_other;
+        }
+        else
+        {
+            gamma_point[p] = m_gamma_point_other;
+            if (bd.is_attach_point(p, to_play))
+                gamma_adj[p] = m_gamma_adj_own_attach;
+            else
+                gamma_adj[p] = m_gamma_adj_nonforbidden;
+            unsigned n = 0;
+            if (MAX_SIZE == 7 || IS_CALLISTO)
+            {
+                // Nexos and Callisto don't use "adjacent" points, use
+                // "diagonal" instead
+                LIBBOARDGAME_ASSERT(geo.get_adj(p).empty());
+                for (auto pa : geo.get_diag(p))
+                    n += 1u - static_cast<unsigned>(is_forbidden[pa]);
+            }
+            else
+                for (auto pa : geo.get_adj(p))
+                    n += 1u - static_cast<unsigned>(is_forbidden[pa]);
+            LIBBOARDGAME_ASSERT(n < m_gamma_attach_nonforbidden.size());
+            gamma_attach[p] = m_gamma_attach_nonforbidden[n];
+        }
+    }
+    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])
+            {
+                gamma_point[p] = m_gamma_point_opp_attach_or_nb;
+                if (MAX_SIZE == 7 || IS_CALLISTO)
+                    // Nexos or Callisto
+                    LIBBOARDGAME_ASSERT(geo.get_adj(p).empty());
+                else
+                    for (Point j : geo.get_adj(p))
+                        if (! is_forbidden[j])
+                            gamma_point[j] = m_gamma_point_opp_attach_or_nb;
+            }
+    }
+    if (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])
+            {
+                gamma_point[p] *= m_gamma_point_second_color_attach;
+                if (! is_forbidden[p])
+                    gamma_attach[p] *= m_gamma_attach_second_color;
+            }
+    }
+    m_max_gamma = -numeric_limits<Float>::max();
+    m_sum_gamma = 0;
+    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_ext = BoardConst::get_move_info_ext<MAX_ADJ_ATTACH>(
+                    mv, move_info_ext_array);
+        auto& features = m_features[i];
+        auto& info = BoardConst::get_move_info<MAX_SIZE>(mv, move_info_array);
+        auto j = info.begin();
+        Float gamma = gamma_point[*j];
+        bool local = m_local_points.contains(*j);
+        if (! check_dist_to_center)
+            for (unsigned k = 1; k < MAX_SIZE; ++k)
+            {
+                ++j;
+                gamma *= gamma_point[*j];
+                local |= m_local_points.contains(*j);
+            }
+        else
+        {
+            features.dist_to_center = m_dist_to_center[*j];
+            for (unsigned k = 1; k < MAX_SIZE; ++k)
+            {
+                ++j;
+                gamma *= gamma_point[*j];
+                local |= m_local_points.contains(*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);
+        }
+        if (local)
+            gamma *= m_gamma_local;
+        j = info_ext.begin_attach();
+        auto end = info_ext.end_attach();
+        gamma *= gamma_attach[*j];
+        while (++j != end)
+            gamma *= gamma_attach[*j];
+        if (MAX_SIZE == 7 || IS_CALLISTO)
+        {
+            // Nexos and Callisto don't use "adjacent" points, only "diagonal"
+            // Use the features of gamma_adj also for the attach points
+            LIBBOARDGAME_ASSERT(info_ext.size_adj_points == 0);
+            LIBBOARDGAME_ASSERT(! check_connect);
+            j = info_ext.begin_attach();
+            end = info_ext.end_attach();
+            for ( ; j != end; ++j)
+            {
+                gamma *= gamma_attach[*j];
+                gamma *= gamma_adj[*j];
+            }
+        }
+        else
+        {
+            j = info_ext.begin_adj();
+            end = info_ext.end_adj();
+            if (! check_connect)
+            {
+                for ( ; j != end; ++j)
+                    gamma *= gamma_adj[*j];
+            }
+            else
+            {
+                features.connect = (bd.get_point_state(*j) == second_color);
+                for ( ; j != end; ++j)
+                {
+                    gamma *= gamma_adj[*j];
+                    if (bd.get_point_state(*j) == second_color)
+                        features.connect = true;
+                }
+                if (features.connect)
+                    m_has_connect_move = true;
+            }
+        }
+        gamma *= m_gamma_piece_score[info.get_piece()];
+        m_sum_gamma += gamma;
+        if (gamma > m_max_gamma)
+            m_max_gamma = gamma;
+        features.gamma = gamma;
+    }
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+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 in-tree phase of the search assumes alternating
+        // moves, because the color of a move is not stored in the nodes and
+        // it wouldn't know who is to play otherwise without generating moves.
+        if (! expander.check_capacity(1))
+            return false;
+        expander.add_child(Move::null(), root_val,
+                           SearchParamConst::child_min_count, 1);
+        return true;
+    }
+    m_local_points.init<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, IS_CALLISTO>(
+                bd, moves, check_dist_to_center, check_connect);
+    if (! m_has_connect_move)
+        check_connect = false;
+    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)
+            {
+                // If a symmetric draw is still possible, encourage exploring
+                // the move that keeps the symmetry
+                ColorMove last = bd.get_move(nu_moves - 1);
+                Move symmetric_mv =
+                        bd.get_move_info_ext_2(last.move).symmetric_move;
+                for (unsigned i = 0; i < moves.size(); ++i)
+                    if (moves[i] == symmetric_mv)
+                    {
+                        m_sum_gamma -= m_features[i].gamma;
+                        m_features[i].gamma *= 100.f;
+                        m_sum_gamma += m_features[i].gamma;
+                        if (m_features[i].gamma > m_max_gamma)
+                            m_max_gamma = m_features[i].gamma;
+                        break;
+                    }
+            }
+        }
+        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;
+    auto inv_max_gamma = 1.f / m_max_gamma;
+    auto inv_sum_gamma = 1.f / m_sum_gamma;
+
+    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 ((check_dist_to_center
+             && features.dist_to_center > m_min_dist_to_center)
+                || (check_connect && ! features.connect))
+            continue;
+        auto mv = moves[i];
+        // If a symmetric draw is still possible, consider only moves that
+        // break the symmetry
+        if (has_symmetry_breaker
+                && ! bd.get_move_info_ext_2(mv).breaks_symmetry)
+            continue;
+        Float move_prior = features.gamma * inv_sum_gamma;
+        // Empirical good formula for value initialization
+        Float value = root_val * sqrt(features.gamma * inv_max_gamma);
+        LIBBOARDGAME_ASSERT(bd.is_legal(to_play, mv));
+        expander.add_child(mv, value, SearchParamConst::child_min_count,
+                           move_prior);
+    }
+    return true;
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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..ab0c54c
--- /dev/null
@@ -0,0 +1,170 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/Search.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Search.h"
+
+#include "Util.h"
+
+namespace libpentobi_mcts {
+
+//-----------------------------------------------------------------------------
+
+Search::Search(Variant initial_variant, unsigned nu_threads, size_t memory)
+    : SearchBase(nu_threads == 0 ? get_nu_threads() : nu_threads, memory),
+      m_variant(initial_variant),
+      m_shared_const(m_to_play)
+{
+    set_default_param(m_variant);
+    create_threads();
+}
+
+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 make_unique<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 (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_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:
+    case Variant::gembloq:
+    case Variant::gembloq_2_4:
+    case Variant::gembloq_3:
+        // Tuned for classic_2
+        set_exploration_constant(4.5f);
+        set_rave_parent_max(50000);
+        break;
+    case Variant::duo:
+    case Variant::junior:
+    case Variant::gembloq_2:
+        // Tuned for duo
+        set_exploration_constant(4.0f);
+        set_rave_parent_max(25000);
+        break;
+    case Variant::trigon:
+    case Variant::trigon_2:
+    case Variant::trigon_3:
+    case Variant::callisto:
+    case Variant::callisto_2_4:
+    case Variant::callisto_3:
+        // Tuned for trigon_2
+        set_exploration_constant(6.0f);
+        set_rave_parent_max(50000);
+        break;
+    case Variant::nexos:
+    case Variant::nexos_2:
+        // Tuned for nexos_2
+        set_exploration_constant(3.7f);
+        set_rave_parent_max(50000);
+        break;
+    case Variant::callisto_2:
+        set_exploration_constant(4.0f);
+        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..5913615
--- /dev/null
@@ -0,0 +1,142 @@
+//-----------------------------------------------------------------------------
+/** @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 one additional pseudo-player for each real player that shares the
+    game result with the main color of the real player.
+    The maximum number of players is 6, which occurs in Classic 3 with 3
+    real players and 3 pseudo-players.
+
+    Some user-changeable parameters that have different optimal values for
+    different game variants are automatically changed whenever the game variant
+    changes.
+    @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);
+
+
+    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);
+
+    /** @} */ // @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:
+    /** 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_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());
+    return to_play;
+}
+
+inline Color Search::get_to_play() const
+{
+    return m_to_play;
+}
+
+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..a230a25
--- /dev/null
@@ -0,0 +1,82 @@
+//-----------------------------------------------------------------------------
+/** @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
+{
+    using Float = libpentobi_mcts::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;
+
+#ifdef 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 max_move_prior = 1;
+
+    static constexpr Float tie_value = 0.5f;
+
+    static constexpr Float prune_count_start = 16;
+
+    static constexpr Float expansion_threshold = 1;
+
+    static constexpr Float expansion_threshold_inc = 0.5f;
+
+    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..c42c31e
--- /dev/null
@@ -0,0 +1,369 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/SharedConst.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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;
+}
+
+/** Check if a point is a useless move for the 1-piece in Callisto.
+    @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 is_useless_one_piece_point(const Board& bd, Point p)
+{
+    for (Point pp: bd.get_geometry().get_diag(p))
+        if (bd.get_point_state(pp).is_empty())
+            return false;
+    return true;
+}
+
+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::gembloq_2:
+        if (nu_moves < nu_colors)
+        {
+            is_piece_considered.fill(false);
+            set_piece_considered(bc, "I5", is_piece_considered);
+        }
+        else 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::gembloq:
+        if (nu_moves < nu_colors)
+        {
+            is_piece_considered.fill(false);
+            set_piece_considered(bc, "I5", is_piece_considered);
+        }
+        else if (nu_moves < 2 * nu_colors)
+        {
+            is_piece_considered.fill(false);
+            set_piece_considered(bc, "I5", is_piece_considered);
+            set_piece_considered(bc, "I4", is_piece_considered);
+            set_piece_considered(bc, "L5", is_piece_considered);
+            set_piece_considered(bc, "N5", is_piece_considered);
+            set_piece_considered(bc, "Y", is_piece_considered);
+        }
+        else if (nu_moves < 3 * nu_colors)
+            filter_min_size(bc, 5, is_piece_considered);
+        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::gembloq_3:
+        if (nu_moves < nu_colors)
+        {
+            is_piece_considered.fill(false);
+            set_piece_considered(bc, "I5", is_piece_considered);
+            set_piece_considered(bc, "L5", is_piece_considered);
+        }
+        else 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, 5, is_piece_considered);
+        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::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
+    PointList points;
+    unsigned n = 0;
+    for (Point p : bd)
+        if (bd.get_point_state(p).is_empty() && bc.has_adj_status_points(p))
+            points.get_unchecked(n++) = p;
+    points.resize(n);
+    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();
+
+        // 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));
+
+        for (Point p : points)
+            if (! bd.is_forbidden(p, c))
+            {
+                auto adj_status = bd.get_adj_status(p, c);
+                for (Piece piece : pieces)
+                {
+                    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);
+                }
+            }
+        if (! is_followup)
+            for (Point p : points)
+                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 : points)
+        {
+            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(bd, 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(bd, 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.is_callisto();
+    for (auto i = bd.get_nu_onboard_pieces(); i < Board::max_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);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
diff --git a/src/libpentobi_mcts/SharedConst.h b/src/libpentobi_mcts/SharedConst.h
new file mode 100644 (file)
index 0000000..196fe7c
--- /dev/null
@@ -0,0 +1,94 @@
+//-----------------------------------------------------------------------------
+/** @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_moves> is_piece_considered;
+
+    /** List of unique values for is_piece_considered. */
+    ArrayList<PieceMap<bool>, Board::max_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::range_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();
+};
+
+//-----------------------------------------------------------------------------
+
+} // 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..f4fcd3c
--- /dev/null
@@ -0,0 +1,925 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/State.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "State.h"
+
+#include "libboardgame_util/MathUtil.h"
+#include "libpentobi_base/ScoreUtil.h"
+#ifdef 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 {
+
+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),
+    m_prior_knowledge(m_bd)
+{
+}
+
+template<unsigned MAX_SIZE>
+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>(
+                           mv, get_move_info<MAX_SIZE>(mv), gamma_piece, moves,
+                           nu_moves, playout_features, total_gamma))
+                marker.set(mv);
+    }
+}
+
+void State::add_callisto_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;
+    bool is_gembloq = (m_bd.get_piece_set() == PieceSet::gembloq);
+    for (Piece piece : pieces)
+        for (Move mv : get_moves(c, piece, p, 0))
+        {
+            // In GembloQ, not all moves covering one starting point
+            // (=quarter-square tringle) are legal.
+            if (is_gembloq && ! m_bd.is_legal(c, mv))
+                continue;
+            if (check_forbidden<MAX_SIZE>(is_forbidden, mv, moves, nu_moves))
+            {
+                LIBBOARDGAME_ASSERT(! marker[mv]);
+                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 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)
+{
+    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;
+    gamma *= m_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>
+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>(
+                mv, info, m_gamma_piece[info.get_piece()], moves, nu_moves,
+                playout_features, total_gamma);
+}
+
+#ifdef LIBBOARDGAME_DEBUG
+string State::dump() const
+{
+    ostringstream s;
+    s << "pentobi_mcts::State:\n" << libpentobi_base::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)
+    {
+        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;
+    res += get_quality_bonus(Color(0), res, s)
+            + get_quality_bonus_attach_multicolor();
+    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 (m_bd.get_variant() == Variant::classic_3)
+    {
+        result[3] = result[0];
+        result[4] = result[1];
+        result[5] = result[2];
+    }
+}
+
+/** Evaluation function for game variants with 2 colors. */
+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)
+    {
+        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;
+    res += get_quality_bonus(Color(0), res, s);
+    if (m_is_callisto)
+        res += get_quality_bonus_attach_twocolor();
+    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;
+        }
+        auto px = static_cast<float>(geo.get_x(p));
+        auto 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())
+                {
+                    auto ppx = static_cast<float>(geo.get_x(pp));
+                    auto 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)
+    {
+        if (! m_is_callisto)
+        {
+            init_moves_without_gamma<5, false>(to_play);
+            return m_prior_knowledge.gen_children<5, 16, false>(
+                        m_bd, m_moves[to_play], m_is_symmetry_broken,
+                        expander, root_val);
+        }
+        init_moves_without_gamma<5, true>(to_play);
+        return m_prior_knowledge.gen_children<5, 16, true>(
+                    m_bd, m_moves[to_play], m_is_symmetry_broken,
+                    expander, root_val);
+    }
+    if (m_max_piece_size == 6)
+    {
+        init_moves_without_gamma<6, false>(to_play);
+        return m_prior_knowledge.gen_children<6, 22, false>(
+                    m_bd, m_moves[to_play], m_is_symmetry_broken, expander,
+                    root_val);
+    }
+    if (m_max_piece_size == 7)
+    {
+        init_moves_without_gamma<7, false>(to_play);
+        return m_prior_knowledge.gen_children<7, 12, false>(
+                    m_bd, m_moves[to_play], m_is_symmetry_broken, expander,
+                    root_val);
+    }
+    LIBBOARDGAME_ASSERT(m_max_piece_size == 22);
+    init_moves_without_gamma<22, false>(to_play);
+    return m_prior_knowledge.gen_children<22, 44, false>(
+                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 if (m_max_piece_size == 7)
+                init_moves_with_gamma<7, 12, false>(to_play);
+            else
+                init_moves_with_gamma<22, 44, 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 if (m_max_piece_size == 7)
+                update_moves<7, 12, false>(to_play);
+            else
+                update_moves<22, 44, 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)])
+        {
+            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];
+    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. */
+template<bool IS_CALLISTO>
+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
+            && ! 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
+    auto 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 = static_cast<int>(m_bd.get_attach_points(Color(0)).size())
+            - static_cast<int>(m_bd.get_attach_points(Color(1)).size());
+    for (Point p : m_bd.get_attach_points(Color(0)))
+        n -= static_cast<int>(m_bd.is_forbidden(p, Color(0)));
+    for (Point p : m_bd.get_attach_points(Color(1)))
+        n += static_cast<int>(m_bd.is_forbidden(p, Color(1)));
+    auto 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 = static_cast<int>(m_bd.get_attach_points(Color(0)).size())
+            + static_cast<int>(m_bd.get_attach_points(Color(2)).size())
+            - static_cast<int>(m_bd.get_attach_points(Color(1)).size())
+            - static_cast<int>(m_bd.get_attach_points(Color(3)).size());
+    for (Point p : m_bd.get_attach_points(Color(0)))
+        n -= static_cast<int>(m_bd.is_forbidden(p, Color(0)));
+    for (Point p : m_bd.get_attach_points(Color(2)))
+        n -= static_cast<int>(m_bd.is_forbidden(p, Color(2)));
+    for (Point p : m_bd.get_attach_points(Color(1)))
+        n += static_cast<int>(m_bd.is_forbidden(p, Color(1)));
+    for (Point p : m_bd.get_attach_points(Color(3)))
+        n += static_cast<int>(m_bd.is_forbidden(p, Color(3)));
+    auto 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;
+}
+
+void State::init_gamma()
+{
+    auto& bd = *m_shared_const.board;
+    const auto piece_set = bd.get_piece_set();
+    if (piece_set == PieceSet::gembloq)
+    {
+        static_assert(PlayoutFeatures::max_local + 1 >= 20, "");
+        m_gamma_local[0] = 1;
+        m_gamma_local[1] = 1e6f;
+        m_gamma_local[2] = 1e6f;
+        m_gamma_local[3] = 1e6f;
+        m_gamma_local[4] = 1e6f;
+        m_gamma_local[5] = 1e6f;
+        m_gamma_local[6] = 1e6f;
+        m_gamma_local[7] = 1e6f;
+        m_gamma_local[8] = 1e12f;
+        m_gamma_local[9] = 1e12f;
+        m_gamma_local[10] = 1e12f;
+        m_gamma_local[11] = 1e12f;
+        m_gamma_local[12] = 1e18f;
+        m_gamma_local[13] = 1e18f;
+        m_gamma_local[14] = 1e18f;
+        m_gamma_local[15] = 1e18f;
+        m_gamma_local[16] = 1e24f;
+        m_gamma_local[17] = 1e24f;
+        m_gamma_local[18] = 1e24f;
+        m_gamma_local[19] = 1e24f;
+        for (unsigned i = 20; i < PlayoutFeatures::max_local + 1; ++i)
+            m_gamma_local[i] = 1e25f;
+    }
+    else if (piece_set == PieceSet::trigon)
+    {
+        static_assert(PlayoutFeatures::max_local + 1 >= 5, "");
+        m_gamma_local[0] = 1;
+        m_gamma_local[1] = 1e6f;
+        m_gamma_local[2] = 1e12f;
+        m_gamma_local[3] = 1e18f;
+        m_gamma_local[4] = 1e24f;
+        for (unsigned i = 5; i < PlayoutFeatures::max_local + 1; ++i)
+            m_gamma_local[i] = 1e30f;
+    }
+    else if (piece_set == PieceSet::nexos)
+    {
+        static_assert(PlayoutFeatures::max_local + 1 >= 4, "");
+        m_gamma_local[0] = 1;
+        m_gamma_local[1] = 1e6f;
+        m_gamma_local[2] = 1e12f;
+        m_gamma_local[3] = 1e18f;
+        for (unsigned i = 4; i < PlayoutFeatures::max_local + 1; ++i)
+            m_gamma_local[i] = 1e24f;
+    }
+    else
+    {
+        static_assert(PlayoutFeatures::max_local + 1 >= 5, "");
+        m_gamma_local[0] = 1;
+        m_gamma_local[1] = 1e6f;
+        m_gamma_local[2] = 1e12f;
+        m_gamma_local[3] = 1e18f;
+        m_gamma_local[4] = 1e24f;
+        for (unsigned i = 5; i < PlayoutFeatures::max_local + 1; ++i)
+            m_gamma_local[i] = 1e25f;
+    }
+    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;
+    case BoardType::gembloq_2:
+    case BoardType::gembloq: // Not tuned
+    case BoardType::gembloq_3: // Not tuned
+        gamma_size_factor = 1.5f;
+        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);
+    }
+    if (m_is_callisto)
+        // Playing 1-piece in Callisto early in playouts is bad, make sure it
+        // gets a low gamma even if it is on a local point.
+        m_gamma_piece[m_bd.get_one_piece()] = 1e-13f;
+}
+
+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, IS_CALLISTO>(m_bd);
+    auto& marker = m_marker[c];
+    auto& moves = m_moves[c];
+    marker.clear(moves);
+    auto& pieces = get_pieces_considered<IS_CALLISTO>(c);
+    if (m_bd.is_first_piece(c) && ! IS_CALLISTO)
+        add_starting_moves<MAX_SIZE>(c, pieces, true, moves);
+    else
+    {
+        unsigned nu_moves = 0;
+        float total_gamma = 0;
+        if (IS_CALLISTO)
+            add_callisto_one_piece_moves(c, true, total_gamma, moves, nu_moves);
+        if (m_is_piece_considered[c]
+                != &m_shared_const.is_piece_considered_none)
+            for (Point p : m_bd.get_attach_points(c))
+            {
+                if (m_bd.is_forbidden(p, c))
+                    continue;
+                add_moves<MAX_SIZE>(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, bool IS_CALLISTO>
+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<IS_CALLISTO>(c);
+    auto& is_forbidden = m_bd.is_forbidden(c);
+    if (m_bd.is_first_piece(c) && ! IS_CALLISTO)
+        add_starting_moves<MAX_SIZE>(c, pieces, false, moves);
+    else
+    {
+        unsigned nu_moves = 0;
+        if (IS_CALLISTO)
+        {
+            float total_gamma_dummy;
+            add_callisto_one_piece_moves(c, false, total_gamma_dummy, moves,
+                                         nu_moves);
+        }
+        if (m_is_piece_considered[c]
+                != &m_shared_const.is_piece_considered_none)
+            for (Point p : m_bd.get_attach_points(c))
+            {
+                if (is_forbidden[p])
+                    continue;
+                auto adj_status = m_bd.get_adj_status(p, c);
+                for (Piece piece : pieces)
+                {
+                    if (! has_moves(c, piece, p, adj_status))
+                        continue;
+                    for (Move mv : get_moves(c, piece, p, adj_status))
+                        if (! marker[mv]
+                                && check_forbidden<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, IS_CALLISTO>(c);
+    }
+}
+
+void State::play_expanded_child(Move mv)
+{
+    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;
+    }
+}
+
+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.is_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
+                            || variant == Variant::gembloq_2);
+        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();
+}
+
+void State::start_simulation(size_t n)
+{
+    LIBBOARDGAME_UNUSED(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, IS_CALLISTO>(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>(
+                             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>(
+                             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<IS_CALLISTO>(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>(*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>(
+                        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..60a2a8d
--- /dev/null
@@ -0,0 +1,543 @@
+//-----------------------------------------------------------------------------
+/** @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/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 probability
+    that a move is played in the playout phase. */
+class State
+{
+public:
+    using Node =
+        libboardgame_mcts::Node<Move, Float, SearchParamConst::multithread>;
+
+    using Tree = libboardgame_mcts::Tree<Node>;
+
+    using LastGoodReply =
+        libboardgame_mcts::LastGoodReply<Move,
+                                         SearchParamConst::max_players,
+                                         SearchParamConst::lgr_hash_table_size,
+                                         SearchParamConst::multithread>;
+
+
+    /** 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);
+
+    /** 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>& mv);
+
+    void evaluate_playout(array<Float, 6>& result);
+
+    void play_playout(Move mv);
+
+    /** Check if RAVE value for this move should not be updated. */
+    bool skip_rave(Move mv) const;
+
+#ifdef LIBBOARDGAME_DEBUG
+    string dump() const;
+#endif
+
+    string get_info() const;
+
+private:
+    /** 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 PlayoutFeatures::get_nu_local(). */
+    array<float, PlayoutFeatures::max_local + 1> m_gamma_local;
+
+    /** 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>
+    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);
+
+    LIBBOARDGAME_NOINLINE
+    void add_callisto_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;
+
+    template<bool IS_CALLISTO>
+    const Board::PiecesLeftList& get_pieces_considered(Color c);
+
+    void init_gamma();
+
+    template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+    void init_moves_with_gamma(Color c);
+
+    template<unsigned MAX_SIZE, bool IS_CALLISTO>
+    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 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 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();
+    int has_attach_point = 0;
+    do
+    {
+        if (m_bd.is_forbidden(*i, c))
+            return false;
+        has_attach_point |= static_cast<int>(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 != 0;
+}
+
+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 (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()
+        return false;
+    PlayerInt player = get_player();
+    Move lgr2 = lgr.get_lgr2(player, last, second_last);
+    if (check_lgr(lgr2))
+    {
+        mv = PlayerMove<Move>(player, lgr2);
+        return true;
+    }
+    Move lgr1 = lgr.get_lgr1(player, last);
+    if (check_lgr(lgr1))
+    {
+        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_range());
+    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_range());
+    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 if (m_max_piece_size == 7)
+        {
+            m_bd.play<7, 12>(to_play, mv);
+            update_playout_features<7, 12>(to_play, mv);
+        }
+        else
+        {
+            m_bd.play<22, 44>(to_play, mv);
+            update_playout_features<22, 44>(to_play, mv);
+        }
+    }
+    else
+    {
+        ++m_nu_passes;
+        m_bd.set_to_play(to_play.get_next(m_nu_colors));
+    }
+}
+
+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 if (m_max_piece_size == 7)
+    {
+        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
+        LIBBOARDGAME_ASSERT(m_is_symmetry_broken);
+    }
+    else
+    {
+        m_bd.play<22, 44>(to_play, mv);
+        update_playout_features<22, 44>(to_play, mv);
+        if (! m_is_symmetry_broken)
+            update_symmetry_broken<22>(mv);
+    }
+    ++m_nu_new_moves[to_play];
+    m_last_move[to_play] = mv;
+    m_nu_passes = 0;
+}
+
+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);
+    if (MAX_SIZE == 7) // Nexos
+        LIBBOARDGAME_ASSERT(get_move_info_ext<MAX_ADJ_ATTACH>(mv).size_adj_points == 0);
+    else
+        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..a7c8173
--- /dev/null
@@ -0,0 +1,95 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/StateUtil.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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::IntType, Color::range> symmetric_state{ {1, 0, 3, 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));
+#ifdef 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.to_int() != 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.to_int() != 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..3ca8d2d
--- /dev/null
@@ -0,0 +1,112 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/Util.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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 {
+
+using libboardgame_sgf::Writer;
+using libpentobi_base::write_setup;
+using libpentobi_base::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()
+            << "\nPrior: " << node.get_move_prior()
+            << "\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;
+    children.reserve(node.get_nu_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 8 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 8 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 > 8)
+        nu_threads = 8;
+    return nu_threads;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
diff --git a/src/libpentobi_mcts/Util.h b/src/libpentobi_mcts/Util.h
new file mode 100644 (file)
index 0000000..0a85243
--- /dev/null
@@ -0,0 +1,33 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+
+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 libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_UTIL_H
diff --git a/src/libpentobi_paint/CMakeLists.txt b/src/libpentobi_paint/CMakeLists.txt
new file mode 100644 (file)
index 0000000..dd1724f
--- /dev/null
@@ -0,0 +1,12 @@
+find_package(Qt5Gui 5.9 REQUIRED)
+
+add_library(pentobi_paint STATIC
+Paint.cpp
+Paint.h
+)
+
+target_compile_definitions(pentobi_paint PRIVATE
+    QT_DEPRECATED_WARNINGS
+    QT_DISABLE_DEPRECATED_BEFORE=0x051100)
+
+target_link_libraries(pentobi_paint pentobi_base Qt5::Gui)
diff --git a/src/libpentobi_paint/Paint.cpp b/src/libpentobi_paint/Paint.cpp
new file mode 100644 (file)
index 0000000..0d608ca
--- /dev/null
@@ -0,0 +1,900 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_paint/Paint.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Paint.h"
+
+#include <QPainter>
+#include "libpentobi_base/CallistoGeometry.h"
+#include "libpentobi_base/ColorMap.h"
+
+using namespace std;
+using libboardgame_util::ArrayList;
+using libpentobi_base::CallistoGeometry;
+using libpentobi_base::Color;
+using libpentobi_base::ColorMap;
+using libpentobi_base::Geometry;
+using libpentobi_base::GeometryType;
+using libpentobi_base::Point;
+
+namespace libpentobi_paint {
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void paintQuarterSquareBase(QPainter& painter, qreal x, qreal y, qreal width,
+                            qreal height, const QColor& base)
+{
+    const QPointF polygon[3] =
+    {
+        QPointF(x, y),
+        QPointF(x + width, y),
+        QPointF(x, y + height)
+    };
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(base);
+    painter.drawConvexPolygon(polygon, 3);
+}
+
+void paintQuarterSquareFrame(QPainter& painter, qreal x, qreal y, qreal width,
+                             qreal height, const QColor& light)
+{
+    const QPointF polygon[4] =
+    {
+        QPointF(x, y + height),
+        QPointF(x, y + 0.9 * height),
+        QPointF(x + 0.9 * width, y),
+        QPointF(x + width, y)
+    };
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(light);
+    painter.drawConvexPolygon(polygon, 4);
+}
+
+void paintSquareFrame(QPainter& painter, qreal x, qreal y, qreal width,
+                      qreal height, const QColor& light,
+                      const QColor& dark)
+{
+    painter.save();
+    painter.translate(x, y);
+    qreal border = 0.05 * max(width, height);
+    const QPointF down[4] =
+        {
+            QPointF(border, height - border),
+            QPointF(width - border, height - border),
+            QPointF(width, height),
+            QPointF(0, height)
+        };
+    const QPointF right[4] =
+        {
+            QPointF(width - border, height - border),
+            QPointF(width - border, border),
+            QPointF(width, 0),
+            QPointF(width, height)
+        };
+    const QPointF up[4] =
+        {
+            QPointF(0, 0),
+            QPointF(width, 0),
+            QPointF(width - border, border),
+            QPointF(border, border)
+        };
+    const QPointF left[4] =
+        {
+            QPointF(0, 0),
+            QPointF(border, border),
+            QPointF(border, height - border),
+            QPointF(0, height)
+        };
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(dark);
+    painter.drawConvexPolygon(down, 4);
+    painter.drawConvexPolygon(right, 4);
+    painter.setBrush(light);
+    painter.drawConvexPolygon(up, 4);
+    painter.drawConvexPolygon(left, 4);
+    painter.restore();
+}
+
+void paintTriangleDownFrame(QPainter& painter, qreal x, qreal y, qreal width,
+                            qreal height, const QColor& light,
+                            const QColor& dark)
+{
+    painter.save();
+    painter.translate(x, y);
+    auto border = 0.05 * height;
+    const QPointF left[4] =
+    {
+        QPointF(0.5 * width, height),
+        QPointF(0.5 * width, height - 2 * border),
+        QPointF(width - 1.732 * border, border),
+        QPointF(width, 0)
+    };
+    const QPointF right[4] =
+    {
+        QPointF(0.5 * width, height),
+        QPointF(0.5 * width, height - 2 * border),
+        QPointF(1.732 * border, border),
+        QPointF(0, 0)
+    };
+    const QPointF up[4] =
+    {
+        QPointF(width, 0),
+        QPointF(width - 1.732 * border, border),
+        QPointF(1.732 * border, border),
+        QPointF(0, 0)
+    };
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(dark);
+    painter.drawConvexPolygon(left, 4);
+    painter.drawConvexPolygon(right, 4);
+    painter.setBrush(light);
+    painter.drawConvexPolygon(up, 4);
+    painter.restore();
+}
+
+void paintTriangleUpFrame(QPainter& painter, qreal x, qreal y, qreal width,
+                          qreal height, const QColor& light,
+                          const QColor& dark)
+{
+    painter.save();
+    painter.translate(x, y);
+    auto border = 0.05 * height;
+    const QPointF down[4] =
+    {
+        QPointF(0, height),
+        QPointF(width, height),
+        QPointF(width - 1.732 * border, height - border),
+        QPointF(1.732 * border, height - border)
+    };
+    const QPointF left[4] =
+    {
+        QPointF(0.5 * width, 0),
+        QPointF(0.5 * width, 2 * border),
+        QPointF(1.732 * border, height - border),
+        QPointF(0, height)
+    };
+    const QPointF right[4] =
+    {
+        QPointF(0.5 * width, 0),
+        QPointF(0.5 * width, 2 * border),
+        QPointF(width - 1.732 * border, height - border),
+        QPointF(width, height)
+    };
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(dark);
+    painter.drawConvexPolygon(down, 4);
+    painter.setBrush(light);
+    painter.drawConvexPolygon(left, 4);
+    painter.drawConvexPolygon(right, 4);
+    painter.restore();
+}
+
+void paintBoardCallisto(QPainter& painter, qreal width, qreal height,
+                        const Geometry& geo, unsigned nuColors,
+                        const QColor& base, const QColor& light,
+                        const QColor& dark, const QColor& centerBase,
+                        const QColor& centerLight, const QColor& centerDark)
+{
+    auto gridWidth = width / geo.get_width();
+    auto gridHeight = height / geo.get_height();
+    for (auto p : geo)
+    {
+        auto x = geo.get_x(p);
+        auto y = geo.get_y(p);
+        if (CallistoGeometry::is_center_section(x, y, nuColors))
+        {
+            painter.fillRect(QRectF(x * gridWidth, y * gridHeight, gridWidth,
+                                    gridHeight), base);
+            painter.fillRect(QRectF(x * gridWidth + 0.05 * gridWidth,
+                                    y * gridHeight + 0.05 * gridHeight,
+                                    0.9 * gridWidth, 0.9 * gridHeight),
+                             centerBase);
+            paintSquareFrame(painter, x * gridWidth + 0.05 * gridWidth,
+                             y * gridHeight + 0.05 * gridHeight,
+                             0.9 * gridWidth, 0.9 * gridHeight, centerDark,
+                             centerLight);
+        }
+        else
+        {
+            painter.fillRect(QRectF(x * gridWidth, y * gridHeight, gridWidth,
+                                    gridHeight), base);
+            paintSquareFrame(painter, x * gridWidth + 0.05 * gridWidth,
+                             y * gridHeight + 0.05 * gridHeight,
+                             0.9 * gridWidth, 0.9 * gridHeight, dark, light);
+        }
+    }
+}
+
+void paintBoardClassic(QPainter& painter, qreal width, qreal height,
+                       const Geometry& geo, const QColor& base,
+                       const QColor& light, const QColor& dark)
+{
+    painter.fillRect(QRectF(0, 0, width, height), base);
+    auto gridWidth = width / geo.get_width();
+    auto gridHeight = height / geo.get_height();
+    for (unsigned x = 0; x < geo.get_width(); ++x)
+        for (unsigned y = 0; y < geo.get_height(); ++y)
+            paintSquareFrame(painter, x * gridWidth, y * gridHeight, gridWidth,
+                             gridHeight, dark, light);
+}
+
+void paintBoardNexos(QPainter& painter, qreal width, qreal height,
+                     const Geometry& geo, const QColor& base,
+                     const QColor& light, const QColor& dark)
+{
+    painter.fillRect(QRectF(0, 0, width, height), base);
+    auto gridWidth = width / (geo.get_width() - 0.5);
+    auto gridHeight = height / (geo.get_height() - 0.5);
+    for (unsigned x = 1; x < geo.get_width(); x += 2)
+        for (unsigned y = 0; y < geo.get_height(); y += 2)
+            paintSquareFrame(painter, x * gridWidth - 0.5 * gridWidth,
+                             y * gridHeight, 1.5 * gridWidth, 0.5 * gridHeight,
+                             dark, light);
+    for (unsigned x = 0; x < geo.get_width(); x += 2)
+        for (unsigned y = 1; y < geo.get_height(); y += 2)
+            paintSquareFrame(painter, x * gridWidth,
+                             y * gridHeight - 0.5 * gridHeight,
+                             0.5 * gridWidth, 1.5 * gridHeight, dark, light);
+}
+
+void paintBoardGembloQ(QPainter& painter, qreal width, qreal height,
+                      const Geometry& geo, const QColor& base,
+                      const QColor& light, const QColor& dark)
+{
+    auto gridWidth = width / geo.get_width();
+    auto gridHeight = height / geo.get_height();
+    qreal distX, distY;
+    switch (geo.get_height())
+    {
+    case 22:
+    case 26:
+        distX = 14 * gridWidth;
+        distY = 7 * gridHeight;
+        break;
+    default:
+        LIBBOARDGAME_ASSERT(geo.get_height() == 28);
+        distX = 2 * gridWidth;
+        distY = gridHeight;
+        break;
+    }
+    const QPointF board[8] =
+    {
+        QPointF(distX, 0),
+        QPointF(width - distX, 0),
+        QPointF(width, distY),
+        QPointF(width, height - distY),
+        QPointF(width - distX, height),
+        QPointF(distX, height),
+        QPointF(0, height - distY),
+        QPointF(0, distY)
+    };
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(base);
+    painter.drawConvexPolygon(board, 8);
+    for (auto p : geo)
+    {
+        painter.save();
+        painter.translate(QPointF(geo.get_x(p) * gridWidth,
+                                  geo.get_y(p) * gridHeight));
+        QColor border;
+        switch (geo.get_point_type(p))
+        {
+        case 0:
+            border = light;
+            break;
+        case 1:
+            border = dark;
+            painter.rotate(180);
+            painter.translate(-gridWidth, -gridHeight);
+            break;
+        case 2:
+            border = dark;
+            painter.rotate(270);
+            painter.translate(-gridHeight, 0);
+            break;
+        case 3:
+            border = light;
+            painter.rotate(90);
+            painter.translate(0, -gridWidth);
+            break;
+        }
+        paintQuarterSquareFrame(painter, 0, 0, 2 * gridWidth, gridHeight,
+                                border);
+        painter.restore();
+    }
+}
+
+void paintBoardTrigon(QPainter& painter, qreal width, qreal height,
+                      const Geometry& geo, const QColor& base,
+                      const QColor& light, const QColor& dark)
+{
+    auto gridWidth = width / (geo.get_width() + 1);
+    auto gridHeight = height / geo.get_height();
+    auto dist = (geo.get_width() + 1 - geo.get_height()) * gridWidth/ 2;
+    const QPointF board[6] =
+    {
+        QPointF(dist, 0),
+        QPointF(width - dist, 0),
+        QPointF(width, height / 2),
+        QPointF(width - dist, height),
+        QPointF(dist, height),
+        QPointF(0, height / 2)
+    };
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(base);
+    painter.drawConvexPolygon(board, 6);
+    for (auto p : geo)
+        if (geo.get_point_type(p) == 0)
+            paintTriangleUpFrame(painter, geo.get_x(p) * gridWidth - 0.5,
+                                 geo.get_y(p) * gridHeight, 2 * gridWidth,
+                                 gridHeight, dark, light);
+        else
+            paintTriangleDownFrame(painter, geo.get_x(p) * gridWidth - 0.5,
+                                   geo.get_y(p) * gridHeight, 2 * gridWidth,
+                                   gridHeight, dark, light);
+}
+
+void paintPiecesCallisto(
+        QPainter& painter, qreal width, qreal height, const Geometry& geo,
+        const Grid<PointState>& pointState, const Grid<unsigned>& pieceId,
+        const ColorMap<QColor>& base, const ColorMap<QColor>& light,
+        const ColorMap<QColor>& dark)
+{
+    auto gridWidth = width / geo.get_width();
+    auto gridHeight = height / geo.get_height();
+    for (auto p : geo)
+    {
+        if (pointState[p].is_empty())
+            continue;
+        auto c = pointState[p].to_color();
+        auto x = geo.get_x(p);
+        auto y = geo.get_y(p);
+        bool hasLeft =
+                (geo.is_onboard(x - 1, y)
+                 && pieceId[p] == pieceId[geo.get_point(x - 1, y)]);
+        bool hasRight =
+                (geo.is_onboard(x + 1, y)
+                 && pieceId[p] == pieceId[geo.get_point(x + 1, y)]);
+        bool hasUp =
+                (geo.is_onboard(x, y - 1)
+                 && pieceId[p] == pieceId[geo.get_point(x, y - 1)]);
+        bool hasDown =
+                (geo.is_onboard(x, y + 1)
+                 && pieceId[p] == pieceId[geo.get_point(x, y + 1)]);
+        if (! hasLeft && ! hasRight && ! hasUp && ! hasDown)
+        {
+            paintCallistoOnePiece(painter, x * gridWidth + 0.05 * gridWidth,
+                                  y * gridHeight + 0.05 * gridHeight,
+                                  0.9 * gridWidth, 0.9 * gridHeight, base[c],
+                                  light[c], dark[c]);
+            continue;
+        }
+        if (hasRight)
+            painter.fillRect(
+                        QRectF(x * gridWidth + 0.96 * gridWidth,
+                               y * gridHeight + 0.07 * gridHeight,
+                               0.08 * gridWidth, 0.86 * gridHeight), base[c]);
+        if (hasDown)
+            painter.fillRect(
+                        QRectF(x * gridWidth + 0.07 * gridWidth,
+                               y * gridHeight + 0.96 * gridHeight,
+                               0.86 * gridWidth, 0.08 * gridHeight), base[c]);
+        paintSquare(painter, x * gridWidth + 0.05 * gridWidth,
+                    y * gridHeight + 0.05 * gridHeight, 0.9 * gridWidth,
+                    0.9 * gridHeight, base[c], light[c], dark[c]);
+    }
+}
+
+void paintPiecesClassic(
+        QPainter& painter, qreal width, qreal height, const Geometry& geo,
+        const Grid<PointState>& pointState, const ColorMap<QColor>& base,
+        const ColorMap<QColor>& light, const ColorMap<QColor>& dark)
+{
+    auto gridWidth = width / geo.get_width();
+    auto gridHeight = height / geo.get_height();
+    for (auto p : geo)
+    {
+        if (pointState[p].is_empty())
+            continue;
+        auto c = pointState[p].to_color();
+        paintSquare(painter, geo.get_x(p) * gridWidth,
+                    geo.get_y(p) * gridHeight, gridWidth, gridHeight, base[c],
+                    light[c], dark[c]);
+    }
+}
+
+void paintPiecesGembloQ(
+        QPainter& painter, qreal width, qreal height, const Geometry& geo,
+        const Grid<PointState>& pointState, const ColorMap<QColor>& base,
+        const ColorMap<QColor>& light, const ColorMap<QColor>& dark)
+{
+    auto gridWidth = width / geo.get_width();
+    auto gridHeight = height / geo.get_height();
+    for (auto p : geo)
+    {
+        if (pointState[p].is_empty())
+            continue;
+        auto c = pointState[p].to_color();
+        painter.save();
+        painter.translate(QPointF(geo.get_x(p) * gridWidth,
+                                  geo.get_y(p) * gridHeight));
+        QColor border;
+        switch (geo.get_point_type(p))
+        {
+        case 0:
+            border = light[c];
+            break;
+        case 1:
+            border = dark[c];
+            painter.rotate(180);
+            painter.translate(-gridWidth, -gridHeight);
+            break;
+        case 2:
+            border = dark[c];
+            painter.rotate(270);
+            painter.translate(-gridHeight, 0);
+            break;
+        case 3:
+            border = light[c];
+            painter.rotate(90);
+            painter.translate(0, -gridWidth);
+            break;
+        }
+        // Antialiasing cause unwanted seams between quarter squares
+        painter.setRenderHint(QPainter::Antialiasing, false);
+        paintQuarterSquareBase(painter, 0, 0, 2 * gridWidth, gridHeight,
+                               base[c]);
+        painter.setRenderHint(QPainter::Antialiasing);
+        paintQuarterSquareFrame(painter, 0, 0, 2 * gridWidth, gridHeight,
+                           border);
+        painter.restore();
+    }
+}
+
+void paintJunction(QPainter& painter, const Geometry& geo,
+                   const Grid<PointState>& pointState,
+                   const Grid<unsigned>& pieceId, Point p, qreal gridWidth,
+                   qreal gridHeight, const ColorMap<QColor>& base)
+{
+    auto x = geo.get_x(p);
+    auto y = geo.get_y(p);
+    ArrayList<unsigned, 4> pieces;
+    if (x > 0)
+    {
+        auto piece = pieceId[geo.get_point(x - 1, y)];
+        if (piece != 0)
+            pieces.include(piece);
+    }
+    if (x < geo.get_width() - 1)
+    {
+        auto piece = pieceId[geo.get_point(x + 1, y)];
+        if (piece != 0)
+            pieces.include(piece);
+    }
+    if (y > 0)
+    {
+        auto piece = pieceId[geo.get_point(x, y - 1)];
+        if (piece != 0)
+            pieces.include(piece);
+    }
+    if (y < geo.get_height() - 1)
+    {
+        auto piece = pieceId[geo.get_point(x, y + 1)];
+        if (piece != 0)
+            pieces.include(piece);
+    }
+    for (auto piece : pieces)
+    {
+        Color c;
+        bool hasLeft = false;
+        if (x > 0)
+        {
+            auto p = geo.get_point(x - 1, y);
+            if (pieceId[p] == piece)
+            {
+                hasLeft = true;
+                c = pointState[p].to_color();
+            }
+        }
+        bool hasRight = false;
+        if (x < geo.get_width() - 1)
+        {
+            auto p = geo.get_point(x + 1, y);
+            if (pieceId[p] == piece)
+            {
+                hasRight = true;
+                c = pointState[p].to_color();
+            }
+        }
+        bool hasUp = false;
+        if (y > 0)
+        {
+            auto p = geo.get_point(x, y - 1);
+            if (pieceId[p] == piece)
+            {
+                hasUp = true;
+                c = pointState[p].to_color();
+            }
+        }
+        bool hasDown = false;
+        if (y < geo.get_height() - 1)
+        {
+            auto p = geo.get_point(x, y + 1);
+            if (pieceId[p] == piece)
+            {
+                hasDown = true;
+                c = pointState[p].to_color();
+            }
+        }
+        auto w = 0.5 * gridWidth;
+        auto h = 0.5 * gridHeight;
+        painter.save();
+        painter.translate(x * gridWidth, y * gridHeight);
+        if (hasLeft && hasRight && hasUp && hasDown)
+            paintJunctionAll(painter, 0, 0, w, h, base[c]);
+        else if (hasLeft && hasRight && ! hasUp && ! hasDown)
+            paintJunctionStraight(painter, 0, 0, w, h, base[c]);
+        else if (! hasLeft && ! hasRight && hasUp && hasDown)
+        {
+            painter.save();
+            painter.rotate(90);
+            painter.translate(0, -w);
+            paintJunctionStraight(painter, 0, 0, w, h, base[c]);
+            painter.restore();
+        }
+        else if (! hasLeft && hasRight && ! hasUp && hasDown)
+            paintJunctionRight(painter, 0, 0, w, h, base[c]);
+        else if (hasLeft && ! hasRight && ! hasUp && hasDown)
+        {
+            painter.save();
+            painter.rotate(90);
+            painter.translate(0, -w);
+            paintJunctionRight(painter, 0, 0, w, h, base[c]);
+            painter.restore();
+        }
+        else if (hasLeft && ! hasRight && hasUp && ! hasDown)
+        {
+            painter.save();
+            painter.rotate(180);
+            painter.translate(-w, -h);
+            paintJunctionRight(painter, 0, 0, w, h, base[c]);
+            painter.restore();
+        }
+        else if (! hasLeft && hasRight && hasUp && ! hasDown)
+        {
+            painter.save();
+            painter.rotate(270);
+            painter.translate(-h, 0);
+            paintJunctionRight(painter, 0, 0, w, h, base[c]);
+            painter.restore();
+        }
+        else if (hasLeft && hasRight && ! hasUp && hasDown)
+            paintJunctionT(painter, 0, 0, w, h, base[c]);
+        else if (hasLeft && ! hasRight && hasUp && hasDown)
+        {
+            painter.save();
+            painter.rotate(90);
+            painter.translate(0, -w);
+            paintJunctionT(painter, 0, 0, w, h, base[c]);
+            painter.restore();
+        }
+        else if (hasLeft && hasRight && hasUp && ! hasDown)
+        {
+            painter.save();
+            painter.rotate(180);
+            painter.translate(-w, -h);
+            paintJunctionT(painter, 0, 0, w, h, base[c]);
+            painter.restore();
+        }
+        else if (! hasLeft && hasRight && hasUp && hasDown)
+        {
+            painter.save();
+            painter.rotate(270);
+            painter.translate(-h, 0);
+            paintJunctionT(painter, 0, 0, w, h, base[c]);
+            painter.restore();
+        }
+        painter.restore();
+    }
+}
+
+void paintPiecesNexos(
+        QPainter& painter, qreal width, qreal height, const Geometry& geo,
+        const Grid<PointState>& pointState, const Grid<unsigned>& pieceId,
+        const ColorMap<QColor>& base, const ColorMap<QColor>& light,
+        const ColorMap<QColor>& dark)
+{
+    auto gridWidth = width / (geo.get_width() - 0.5);
+    auto gridHeight = height / (geo.get_height() - 0.5);
+    for (auto p : geo)
+    {
+        switch (geo.get_point_type(p))
+        {
+        case 0:
+            paintJunction(painter, geo, pointState, pieceId, p, gridWidth,
+                          gridHeight, base);
+            break;
+        case 1:
+        {
+            if (pointState[p].is_empty())
+                continue;
+            auto c = pointState[p].to_color();
+            paintSquare(painter, geo.get_x(p) * gridWidth - 0.5 * gridWidth,
+                        geo.get_y(p) * gridHeight, 1.5 * gridWidth,
+                        0.5 * gridHeight, base[c], light[c], dark[c]);
+            break;
+        }
+        case 2:
+        {
+            if (pointState[p].is_empty())
+                continue;
+            auto c = pointState[p].to_color();
+            paintSquare(painter, geo.get_x(p) * gridWidth,
+                        geo.get_y(p) * gridHeight - 0.5 * gridHeight,
+                        0.5 * gridWidth, 1.5 * gridHeight, base[c], light[c],
+                        dark[c]);
+            break;
+        }
+        }
+    }
+}
+
+void paintPiecesTrigon(
+        QPainter& painter, qreal width, qreal height, const Geometry& geo,
+        const Grid<PointState>& pointState, const ColorMap<QColor>& base,
+        const ColorMap<QColor>& light, const ColorMap<QColor>& dark)
+{
+    auto gridWidth = width / (geo.get_width() + 1);
+    auto gridHeight = height / geo.get_height();
+    for (auto p : geo)
+    {
+        if (pointState[p].is_empty())
+            continue;
+        auto c = pointState[p].to_color();
+        if (geo.get_point_type(p) == 0)
+            paintTriangleUp(painter, geo.get_x(p) * gridWidth - 0.5,
+                            geo.get_y(p) * gridHeight, 2 * gridWidth,
+                            gridHeight, base[c], light[c], dark[c]);
+        else
+            paintTriangleDown(painter, geo.get_x(p) * gridWidth - 0.5,
+                              geo.get_y(p) * gridHeight, 2 * gridWidth,
+                              gridHeight, base[c], light[c], dark[c]);
+    }
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+void paint(QPainter& painter, qreal width, qreal height, Variant variant,
+           const Geometry& geo, const Grid<PointState>& pointState,
+           const Grid<unsigned>& pieceId)
+{
+    const QColor boardBase(174, 167, 172);
+    const QColor boardLight(199, 191, 197);
+    const QColor boardDark(134, 128, 132);
+    const QColor centerBase(145, 139, 143);
+    const QColor centerLight(160, 154, 159);
+    const QColor centerDark(124, 119, 123);
+    painter.setRenderHint(QPainter::Antialiasing);
+    paintBoard(painter, width, height, variant, boardBase, boardLight,
+               boardDark, centerBase, centerLight, centerDark);
+    array<QColor, 3> blue{ {
+        QColor(0, 115, 207), QColor(20, 153, 255), QColor(0, 72, 129)} };
+    array<QColor, 3> green{ {
+        QColor(0, 192, 0), QColor(0, 250, 0), QColor(0, 120, 0)} };
+    array<QColor, 3> orange{ {
+        QColor(240, 146, 23), QColor(255, 187, 103), QColor(157, 94, 11)} };
+    array<QColor, 3> purple{ {
+        QColor(161, 44, 207), QColor(190, 112, 220), QColor(109, 39, 135)} };
+    array<QColor, 3> red{ {
+        QColor(230, 62, 44), QColor(255, 101, 90), QColor(144, 38, 27)} };
+    array<QColor, 3> yellow{ {
+        QColor(245, 195, 32), QColor(255, 219, 88), QColor(170, 133, 22)} };
+    ColorMap<QColor> piecesBase;
+    ColorMap<QColor> piecesLight;
+    ColorMap<QColor> piecesDark;
+    if (variant == Variant::duo)
+    {
+        piecesBase[Color(0)] = purple[0];
+        piecesLight[Color(0)] = purple[1];
+        piecesDark[Color(0)] = purple[2];
+    }
+    else if (variant == Variant::junior)
+    {
+        piecesBase[Color(0)] = green[0];
+        piecesLight[Color(0)] = green[1];
+        piecesDark[Color(0)] = green[2];
+    }
+    else
+    {
+        piecesBase[Color(0)] = blue[0];
+        piecesLight[Color(0)] = blue[1];
+        piecesDark[Color(0)] = blue[2];
+    }
+    if (variant == Variant::duo || variant == Variant::junior)
+    {
+        piecesBase[Color(1)] = orange[0];
+        piecesLight[Color(1)] = orange[1];
+        piecesDark[Color(1)] = orange[2];
+    }
+    else if (get_nu_colors(variant) == 2)
+    {
+        piecesBase[Color(1)] = green[0];
+        piecesLight[Color(1)] = green[1];
+        piecesDark[Color(1)] = green[2];
+    }
+    else
+    {
+        piecesBase[Color(1)] = yellow[0];
+        piecesLight[Color(1)] = yellow[1];
+        piecesDark[Color(1)] = yellow[2];
+    }
+    piecesBase[Color(2)] = red[0];
+    piecesLight[Color(2)] = red[1];
+    piecesDark[Color(2)] = red[2];
+    piecesBase[Color(3)] = green[0];
+    piecesLight[Color(3)] = green[1];
+    piecesDark[Color(3)] = green[2];
+    switch (get_geometry_type(variant))
+    {
+    case GeometryType::classic:
+        paintPiecesClassic(painter, width, height, geo, pointState, piecesBase,
+                           piecesLight, piecesDark);
+        break;
+    case GeometryType::trigon:
+        paintPiecesTrigon(painter, width, height, geo, pointState, piecesBase,
+                          piecesLight, piecesDark);
+        break;
+    case GeometryType::nexos:
+        paintPiecesNexos(painter, width, height, geo, pointState, pieceId,
+                         piecesBase, piecesLight, piecesDark);
+        break;
+    case GeometryType::callisto:
+        paintPiecesCallisto(painter, width, height, geo, pointState, pieceId,
+                            piecesBase, piecesLight, piecesDark);
+        break;
+    case GeometryType::gembloq:
+        paintPiecesGembloQ(painter, width, height, geo, pointState, piecesBase,
+                           piecesLight, piecesDark);
+        break;
+    }
+}
+
+void paintBoard(QPainter& painter, qreal width, qreal height, Variant variant,
+                const QColor& base, const QColor& light, const QColor& dark,
+                const QColor& centerBase, const QColor& centerLight,
+                const QColor& centerDark)
+{
+    auto& geo = get_geometry(variant);
+    switch (get_geometry_type(variant))
+    {
+    case GeometryType::classic:
+        paintBoardClassic(painter, width, height, geo, base, light, dark);
+        break;
+    case GeometryType::trigon:
+        paintBoardTrigon(painter, width, height, geo, base, light, dark);
+        break;
+    case GeometryType::nexos:
+        paintBoardNexos(painter, width, height, geo, base, light, dark);
+        break;
+    case GeometryType::callisto:
+        paintBoardCallisto(painter, width, height, geo, get_nu_colors(variant),
+                           base, light, dark, centerBase, centerLight,
+                           centerDark);
+        break;
+    case GeometryType::gembloq:
+        paintBoardGembloQ(painter, width, height, geo, base, light, dark);
+        break;
+    }
+}
+
+void paintCallistoOnePiece(QPainter& painter, qreal x, qreal y, qreal width,
+                           qreal height, const QColor& base,
+                           const QColor& light, const QColor& dark)
+{
+    auto dx = 0.175 * width;
+    auto dy = 0.175 * height;
+    painter.fillRect(QRectF(x, y, width, dy), base);
+    painter.fillRect(QRectF(x, y + height - dy, width, dy), base);
+    painter.fillRect(QRectF(x, y, dx, height), base);
+    painter.fillRect(QRectF(x + width - dx, y, dx, height), base);
+    paintSquareFrame(painter, x, y, width, height, light, dark);
+}
+
+void paintJunctionAll(QPainter& painter, qreal x, qreal y, qreal width,
+                      qreal height, const QColor& base)
+{
+    auto dx = 0.22 * width;
+    auto dy = 0.22 * height;
+    painter.fillRect(QRectF(x + dx, y, width - 2 * dx, height), base);
+    painter.fillRect(QRectF(x, y + dy, width, height - 2 * dy), base);
+}
+
+void paintJunctionRight(QPainter& painter, qreal x, qreal y, qreal width,
+                        qreal height, const QColor& base)
+{
+    auto dx = 0.3 * width;
+    auto dy = 0.3 * height;
+    const QPointF polygon[3] =
+    {
+        QPointF(x + dx, y + height),
+        QPointF(x + width, y + height),
+        QPointF(x + width, y + dy)
+    };
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(base);
+    painter.drawConvexPolygon(polygon, 3);
+}
+
+void paintJunctionStraight(QPainter& painter, qreal x, qreal y, qreal width,
+                           qreal height, const QColor& base)
+{
+    auto dy = 0.22 * height;
+    painter.fillRect(QRectF(x, y + dy, width, height - 2 * dy), base);
+}
+
+void paintJunctionT(QPainter& painter, qreal x, qreal y, qreal width,
+                    qreal height, const QColor& base)
+{
+    auto dx = 0.22 * width;
+    auto dy = 0.22 * height;
+    painter.fillRect(QRectF(x + dx, y + dy, width - 2 * dx, height - dy),
+                     base);
+    painter.fillRect(QRectF(x, y + dy, width, height - 2 * dy), base);
+}
+
+void paintQuarterSquare(QPainter& painter, qreal x, qreal y, qreal width,
+                        qreal height, const QColor& base, const QColor& light)
+{
+    paintQuarterSquareBase(painter, x, y, width, height, base);
+    paintQuarterSquareFrame(painter, x, y, width, height, light);
+}
+
+void paintSquare(QPainter& painter, qreal x, qreal y, qreal width,
+                 qreal height, const QColor& base, const QColor& light,
+                 const QColor& dark)
+{
+    painter.fillRect(QRectF(x, y, width, height), base);
+    paintSquareFrame(painter, x, y, width, height, light, dark);
+}
+
+void paintTriangleDown(QPainter& painter, qreal x, qreal y, qreal width,
+                       qreal height, const QColor& base, const QColor& light,
+                       const QColor& dark)
+{
+    const QPointF polygon[3] =
+    {
+        QPointF(x, y),
+        QPointF(x + width, y),
+        QPointF(x + 0.5 * width, y + height)
+    };
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(base);
+    painter.drawConvexPolygon(polygon, 3);
+    paintTriangleDownFrame(painter, x, y, width, height, light, dark);
+}
+
+void paintTriangleUp(QPainter& painter, qreal x, qreal y, qreal width,
+                     qreal height, const QColor& base, const QColor& light,
+                     const QColor& dark)
+{
+    const QPointF polygon[3] =
+    {
+        QPointF(x, y + height),
+        QPointF(x + width, y + height),
+        QPointF(x + 0.5 * width, y)
+    };
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(base);
+    painter.drawConvexPolygon(polygon, 3);
+    paintTriangleUpFrame(painter, x, y, width, height, light, dark);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_paint
diff --git a/src/libpentobi_paint/Paint.h b/src/libpentobi_paint/Paint.h
new file mode 100644 (file)
index 0000000..2a5c339
--- /dev/null
@@ -0,0 +1,81 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_paint/Paint.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_PAINT_H
+#define LIBPENTOBI_PAINT_H
+
+#include <QtGlobal>
+#include "libpentobi_base/Grid.h"
+#include "libpentobi_base/PointState.h"
+#include "libpentobi_base/Variant.h"
+
+class QColor;
+class QPainter;
+
+namespace libpentobi_paint {
+
+using libpentobi_base::Grid;
+using libpentobi_base::Geometry;
+using libpentobi_base::PointState;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+/** Paint the board and pieces.
+    This function takes a Grid<PointState> for the board positions instead of
+    an instance of libpentobi_base::Board, because creating a Board is too
+    expensive for lightweight use cases like a thumbnailer.
+
+    The pieceId parameter only needs to be initialized in game variants Nexos
+    and Callisto. It is needed to paint the junctions between piece elements.
+    They must be 0 for empty points and contain a unique value for points
+    of the same piece. */
+void paint(QPainter& painter, qreal width, qreal height, Variant variant,
+           const Geometry& geo, const Grid<PointState>& pointState,
+           const Grid<unsigned>& pieceId);
+
+/** Paint empty board. */
+void paintBoard(QPainter& painter, qreal width, qreal height, Variant variant,
+                const QColor& base, const QColor& light, const QColor& dark,
+                const QColor& centerBase, const QColor& centerLight,
+                const QColor& centerDark);
+
+void paintCallistoOnePiece(QPainter& painter, qreal x, qreal y, qreal width,
+                           qreal height, const QColor& base,
+                           const QColor& light, const QColor& dark);
+
+void paintJunctionAll(QPainter& painter, qreal x, qreal y, qreal width,
+                      qreal height, const QColor& base);
+
+void paintJunctionRight(QPainter& painter, qreal x, qreal y, qreal width,
+                        qreal height, const QColor& base);
+
+void paintJunctionStraight(QPainter& painter, qreal x, qreal y, qreal width,
+                           qreal height, const QColor& base);
+
+void paintJunctionT(QPainter& painter, qreal x, qreal y, qreal width,
+                    qreal height, const QColor& base);
+
+void paintQuarterSquare(QPainter& painter, qreal x, qreal y, qreal width,
+                        qreal height, const QColor& base, const QColor& light);
+
+void paintSquare(QPainter& painter, qreal x, qreal y, qreal width,
+                 qreal height, const QColor& base, const QColor& light,
+                 const QColor& dark);
+
+void paintTriangleDown(QPainter& painter, qreal x, qreal y, qreal width,
+                       qreal height, const QColor& base, const QColor& light,
+                       const QColor& dark);
+
+void paintTriangleUp(QPainter& painter, qreal x, qreal y, qreal width,
+                     qreal height, const QColor& base, const QColor& light,
+                     const QColor& dark);
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_paint
+
+#endif // LIBPENTOBI_PAINT_H
diff --git a/src/libpentobi_thumbnail/CMakeLists.txt b/src/libpentobi_thumbnail/CMakeLists.txt
new file mode 100644 (file)
index 0000000..cf3433d
--- /dev/null
@@ -0,0 +1,12 @@
+find_package(Qt5Gui 5.9 REQUIRED)
+
+add_library(pentobi_thumbnail STATIC
+    CreateThumbnail.h
+    CreateThumbnail.cpp
+    )
+
+target_compile_definitions(pentobi_thumbnail PRIVATE
+    QT_DEPRECATED_WARNINGS
+    QT_DISABLE_DEPRECATED_BEFORE=0x051100)
+
+target_link_libraries(pentobi_thumbnail pentobi_paint)
diff --git a/src/libpentobi_thumbnail/CreateThumbnail.cpp b/src/libpentobi_thumbnail/CreateThumbnail.cpp
new file mode 100644 (file)
index 0000000..1e4ba95
--- /dev/null
@@ -0,0 +1,169 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_thumbnail/CreateThumbnail.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CreateThumbnail.h"
+
+#include <QPainter>
+#include "libboardgame_sgf/TreeReader.h"
+#include "libpentobi_base/NodeUtil.h"
+#include "libpentobi_paint/Paint.h"
+
+using namespace std;
+using libboardgame_sgf::SgfNode;
+using libboardgame_sgf::TreeReader;
+using libpentobi_base::Color;
+using libpentobi_base::Geometry;
+using libpentobi_base::Grid;
+using libpentobi_base::MovePoints;
+using libpentobi_base::PieceSet;
+using libpentobi_base::Point;
+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)
+{
+    if (! node.has_property(id))
+        return;
+    for (auto& s : node.get_multi_property(id))
+    {
+        ++currentPieceId;
+        auto begin = s.begin();
+        auto end = begin;
+        while (true)
+        {
+            while (end != s.end() && *end != ',')
+                ++end;
+            Point p;
+            if (geo.from_string(begin, end, p))
+            {
+                pointState[p] = PointState(c);
+                pieceId[p] = currentPieceId;
+            }
+            if (end == s.end())
+                break;
+            ++end;
+            begin = end;
+        }
+    }
+}
+
+/** Helper function for getFinalPosition() */
+void handleSetupEmpty(const SgfNode& node, const Geometry& geo,
+                      Grid<PointState>& pointState, Grid<unsigned>& pieceId)
+{
+    if (! node.has_property("AE"))
+        return;
+    for (auto& s : node.get_multi_property("AE"))
+    {
+        auto begin = s.begin();
+        auto end = begin;
+        while (true)
+        {
+            while (end != s.end() && *end != ',')
+                ++end;
+            Point p;
+            if (geo.from_string(begin, end, p))
+            {
+                pointState[p] = PointState::empty();
+                pieceId[p] = 0;
+            }
+            if (end == s.end())
+                break;
+            ++end;
+            begin = end;
+        }
+    }
+}
+
+/** 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;
+    do
+    {
+        if (libpentobi_base::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::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();
+    }
+    while (node != nullptr);
+    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; // Init to avoid compiler warning
+    const Geometry* geo;
+    Grid<PointState> pointState;
+    Grid<unsigned> pieceId;
+    if (! getFinalPosition(reader.get_tree(), variant, geo, pointState,
+                           pieceId))
+        return false;
+    qreal ratio;
+    if (get_piece_set(variant) == PieceSet::trigon)
+        ratio = geo->get_height() * 1.732 / geo->get_width();
+    else
+        ratio = 1;
+    qreal paintWidth = min(static_cast<qreal>(width), height / ratio);
+    qreal paintHeight = ratio * paintWidth;
+    QPainter painter(&image);
+    painter.translate(QPointF((width - paintWidth) / 2,
+                              (height - paintHeight) / 2));
+    libpentobi_paint::paint(painter, paintWidth, paintHeight, variant, *geo,
+                            pointState, pieceId);
+    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/AnalyzeGameModel.cpp b/src/pentobi/AnalyzeGameModel.cpp
new file mode 100644 (file)
index 0000000..63bc065
--- /dev/null
@@ -0,0 +1,275 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/AnalyzeGameModel.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "AnalyzeGameModel.h"
+
+#include <QSettings>
+#include <QtConcurrentRun>
+#include "GameModel.h"
+#include "PlayerModel.h"
+#include "libboardgame_sgf/SgfUtil.h"
+#include "libboardgame_util/Abort.h"
+
+using libboardgame_sgf::is_main_variation;
+using libboardgame_sgf::find_root;
+using libboardgame_util::clear_abort;
+using libboardgame_util::set_abort;
+using libboardgame_util::ArrayList;
+using libpentobi_base::ColorMove;
+
+//-----------------------------------------------------------------------------
+
+AnalyzeGameElement::AnalyzeGameElement(QObject* parent, int moveColor,
+                                       double value)
+    : QObject(parent),
+      m_moveColor(moveColor),
+      m_value(value)
+{
+}
+
+//-----------------------------------------------------------------------------
+
+AnalyzeGameModel::AnalyzeGameModel(QObject* parent)
+    : QObject(parent)
+{
+    connect(&m_watcher, &QFutureWatcher<void>::finished, this, [this]
+    {
+        updateElements();
+        // Set isRunning after updating elements because in GameDisplayDesktop
+        // either isRunning must be true or elements.length > 0 to show the
+        // analysis and we don't want it to disappear if a game with one move
+        // was analyzed.
+        setIsRunning(false);
+    });
+}
+
+AnalyzeGameModel::~AnalyzeGameModel()
+{
+    cancel();
+}
+
+void AnalyzeGameModel::asyncRun(const Game* game, Search* search)
+{
+    auto progressCallback =
+        [&](unsigned movesAnalyzed, unsigned totalMoves)
+        {
+            Q_UNUSED(movesAnalyzed);
+            Q_UNUSED(totalMoves);
+            // Use invokeMethod() because callback runs in different thread
+            QMetaObject::invokeMethod(this, "updateElements",
+                                      Qt::BlockingQueuedConnection);
+        };
+    m_analyzeGame.run(*game, *search, m_nuSimulations, progressCallback);
+}
+
+void AnalyzeGameModel::autoSave(GameModel* gameModel)
+{
+    auto& bd = gameModel->getGame().get_board();
+    QVariantList list;
+    auto variant = bd.get_variant();
+    auto nuMoves = m_analyzeGame.get_nu_moves();
+    QSettings settings;
+    if (nuMoves == 0 || m_analyzeGame.get_variant() != variant)
+        settings.remove(QStringLiteral("analyzeGame"));
+    else
+    {
+        list.append(to_string_id(variant));
+        list.append(nuMoves);
+        for (unsigned i = 0; i < nuMoves; ++i)
+        {
+            auto mv = m_analyzeGame.get_move(i);
+            list.append(mv.color.to_int());
+            list.append(bd.to_string(mv.move).c_str());
+            list.append(m_analyzeGame.get_value(i));
+        }
+        settings.setValue(QStringLiteral("analyzeGame"),
+                          QVariant::fromValue(list));
+    }
+}
+
+void AnalyzeGameModel::cancel()
+{
+    if (! m_isRunning)
+        return;
+    set_abort();
+    m_watcher.waitForFinished();
+    setIsRunning(false);
+}
+
+void AnalyzeGameModel::clear()
+{
+    cancel();
+    if (m_elements.empty())
+        return;
+    m_analyzeGame.clear();
+    m_markMoveNumber = -1;
+    m_elements.clear();
+    emit elementsChanged();
+}
+
+QQmlListProperty<AnalyzeGameElement> AnalyzeGameModel::elements()
+{
+    return {this, m_elements};
+}
+
+void AnalyzeGameModel::gotoMove(GameModel* gameModel, int moveNumber)
+{
+    if (moveNumber < 0)
+        return;
+    auto n = static_cast<unsigned>(moveNumber);
+    if (n >= m_analyzeGame.get_nu_moves())
+        return;
+    auto& game = gameModel->getGame();
+    if (game.get_variant() != m_analyzeGame.get_variant())
+        return;
+    auto& tree = game.get_tree();
+    auto node = &tree.get_root();
+    if (tree.has_move(*node))
+    {
+        // Move in root node not supported.
+        setMarkMoveNumber(-1);
+        return;
+    }
+    for (unsigned i = 0; i < n; ++i)
+    {
+        auto mv = m_analyzeGame.get_move(i);
+        bool found = false;
+        for (auto& child : node->get_children())
+            if (tree.get_move(child) == mv)
+            {
+                found = true;
+                node = &child;
+                break;
+            }
+        if (! found)
+        {
+            setMarkMoveNumber(-1);
+            return;
+        }
+    }
+    gameModel->gotoNode(*node);
+    setMarkMoveNumber(moveNumber);
+}
+
+void AnalyzeGameModel::loadAutoSave(GameModel* gameModel)
+{
+    QSettings settings;
+    auto list =
+            settings.value(
+                QStringLiteral("analyzeGame")).value<QVariantList>();
+    int size = list.size();
+    int index = 0;
+    if (index >= size)
+        return;
+    auto variant = list[index++].toString();
+    auto& bd = gameModel->getGame().get_board();
+    if (variant != to_string_id(bd.get_variant()))
+        return;
+    if (index >= size)
+        return;
+    auto nuMoves = list[index++].toUInt();
+    vector<ColorMove> moves;
+    vector<double> values;
+    for (unsigned i = 0; i < nuMoves; ++i)
+    {
+        if (index >= size)
+            return;
+        auto color = list[index++].toUInt();
+        if (color >= bd.get_nu_colors())
+            return;
+        if (index >= size)
+            return;
+        auto moveString = list[index++].toString();
+        Move mv;
+        if (! bd.from_string(mv, moveString.toLatin1().constData()))
+            return;
+        if (index >= size)
+            return;
+        auto value = list[index++].toDouble();
+        moves.emplace_back(Color(static_cast<Color::IntType>(color)), mv);
+        values.push_back(value);
+    }
+    m_analyzeGame.set(bd.get_variant(), moves, values);
+    updateElements();
+}
+
+void AnalyzeGameModel::markCurrentMove(GameModel* gameModel)
+{
+    auto& game = gameModel->getGame();
+    auto& node = game.get_current();
+    int moveNumber = -1;
+    if (is_main_variation(node))
+    {
+        ArrayList<ColorMove, Board::max_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_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;
+            moveNumber = static_cast<int>(moves.size());
+        }
+    }
+    setMarkMoveNumber(moveNumber);
+}
+
+void AnalyzeGameModel::setIsRunning(bool isRunning)
+{
+    if (m_isRunning == isRunning)
+        return;
+    m_isRunning = isRunning;
+    emit isRunningChanged();
+}
+
+void AnalyzeGameModel::setMarkMoveNumber(int markMoveNumber)
+{
+    if (m_markMoveNumber == markMoveNumber)
+        return;
+    m_markMoveNumber = markMoveNumber;
+    emit markMoveNumberChanged();
+}
+
+void AnalyzeGameModel::start(GameModel* gameModel, PlayerModel* playerModel,
+                             int nuSimulations)
+{
+    if (nuSimulations <= 0)
+        return;
+    m_markMoveNumber = -1;
+    m_nuSimulations = static_cast<size_t>(nuSimulations);
+    cancel();
+    clear_abort();
+    auto future = QtConcurrent::run(this, &AnalyzeGameModel::asyncRun,
+                                    &gameModel->getGame(),
+                                    &playerModel->getSearch());
+    m_watcher.setFuture(future);
+    setIsRunning(true);
+}
+
+void AnalyzeGameModel::updateElements()
+{
+    m_elements.clear();
+    for (unsigned i = 0; i < m_analyzeGame.get_nu_moves(); ++i)
+    {
+        auto moveColor = m_analyzeGame.get_move(i).color.to_int();
+        // Values of search are supposed to be win/loss probabilities but can
+        // be slightly outside [0..1] (see libpentobi_mcts::State).
+        auto value = max(0., min(1., m_analyzeGame.get_value(i)));
+        m_elements.append(new AnalyzeGameElement(this, moveColor, value));
+    }
+    emit elementsChanged();
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/pentobi/AnalyzeGameModel.h b/src/pentobi/AnalyzeGameModel.h
new file mode 100644 (file)
index 0000000..2a67fb6
--- /dev/null
@@ -0,0 +1,116 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/AnalyzeGameModel.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_ANALYZE_GAME_MODEL_H
+#define PENTOBI_ANALYZE_GAME_MODEL_H
+
+#include <QFutureWatcher>
+#include <QQmlListProperty>
+#include "libpentobi_mcts/AnalyzeGame.h"
+
+class GameModel;
+class PlayerModel;
+namespace libpentobi_base { class Game; }
+namespace libpentobi_mcts { class Search; }
+
+using libpentobi_base::Game;
+using libpentobi_mcts::AnalyzeGame;
+using libpentobi_mcts::Search;
+
+//-----------------------------------------------------------------------------
+
+class AnalyzeGameElement
+    : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(int moveColor MEMBER m_moveColor CONSTANT)
+    Q_PROPERTY(double value MEMBER m_value CONSTANT)
+
+public:
+    explicit AnalyzeGameElement(QObject* parent, int moveColor, double value);
+
+private:
+    int m_moveColor;
+
+    double m_value;
+};
+
+//-----------------------------------------------------------------------------
+
+class AnalyzeGameModel
+    : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QQmlListProperty<AnalyzeGameElement> elements READ elements NOTIFY elementsChanged)
+    Q_PROPERTY(bool isRunning READ isRunning NOTIFY isRunningChanged)
+    Q_PROPERTY(int markMoveNumber READ markMoveNumber NOTIFY markMoveNumberChanged)
+
+public:
+    explicit AnalyzeGameModel(QObject* parent = nullptr);
+
+    ~AnalyzeGameModel() override;
+
+
+    Q_INVOKABLE void autoSave(GameModel* gameModel);
+
+    Q_INVOKABLE void cancel();
+
+    Q_INVOKABLE void clear();
+
+    Q_INVOKABLE void gotoMove(GameModel* gameModel, int moveNumber);
+
+    Q_INVOKABLE void loadAutoSave(GameModel* gameModel);
+
+    Q_INVOKABLE void markCurrentMove(GameModel* gameModel);
+
+    Q_INVOKABLE void start(GameModel* gameModel, PlayerModel* playerModel,
+                           int nuSimulations);
+
+
+    bool isRunning() const { return m_isRunning; }
+
+    int markMoveNumber() const { return m_markMoveNumber; }
+
+    QQmlListProperty<AnalyzeGameElement> elements();
+
+signals:
+    void isRunningChanged();
+
+    void markMoveNumberChanged();
+
+    void progressChanged();
+
+    void elementsChanged();
+
+private:
+    bool m_isRunning = false;
+
+    int m_markMoveNumber = -1;
+
+    size_t m_nuSimulations;
+
+    QList<AnalyzeGameElement*> m_elements;
+
+    QFutureWatcher<void> m_watcher;
+
+    AnalyzeGame m_analyzeGame;
+
+
+    Q_INVOKABLE void updateElements();
+
+
+    void asyncRun(const Game* game, Search* search);
+
+    void setIsRunning(bool isRunning);
+
+    void setMarkMoveNumber(int markMoveNumber);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_ANALYZE_GAME_MODEL_H
diff --git a/src/pentobi/AndroidUtils.cpp b/src/pentobi/AndroidUtils.cpp
new file mode 100644 (file)
index 0000000..3bd26a4
--- /dev/null
@@ -0,0 +1,149 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/AndroidUtils.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "AndroidUtils.h"
+
+#include <QCoreApplication>
+#include <QStandardPaths>
+
+#ifdef Q_OS_ANDROID
+#include <QDir>
+#include <QDirIterator>
+#include <QtAndroidExtras/QtAndroid>
+#include <QtAndroidExtras/QAndroidJniObject>
+#endif
+
+//-----------------------------------------------------------------------------
+
+bool AndroidUtils::checkPermission(const QString& permission)
+{
+#ifdef Q_OS_ANDROID
+    return QtAndroid::checkPermission(permission) ==
+           QtAndroid::PermissionResult::Granted;
+#else
+    Q_UNUSED(permission);
+    return true;
+#endif
+}
+
+QUrl AndroidUtils::extractHelp(const QString& language)
+{
+#ifdef Q_OS_ANDROID
+    if (language != QStringLiteral("C"))
+        // Other languages use pictures from C
+        extractHelp(QStringLiteral("C"));
+    auto activity = QtAndroid::androidActivity();
+    auto filesDir =
+            activity.callObjectMethod("getFilesDir", "()Ljava/io/File;");
+    if (! filesDir.isValid())
+        return {};
+    auto filesDirString = filesDir.callObjectMethod("toString",
+                                                    "()Ljava/lang/String;");
+    if (! filesDirString.isValid())
+        return {};
+    QDir dir(filesDirString.toString() + "/help/"
+             + QCoreApplication::applicationVersion() + "/" + language
+             + "/pentobi");
+    auto dirPath = dir.path();
+    if (QFileInfo::exists(dirPath + "/index.html"))
+        return QUrl::fromLocalFile(dirPath + "/index.html");
+    if (! QFileInfo::exists(filesDirString.toString() + "/help/"
+                            + QCoreApplication::applicationVersion()
+                            + "/C/pentobi/index.html"))
+        // No need to keep files from older versions around
+        QDir(filesDirString.toString() + "/help").removeRecursively();
+    QDirIterator it(":qml/help/" + language + "/pentobi");
+    while (it.hasNext())
+    {
+        it.next();
+        if (! it.fileInfo().isFile())
+            continue;
+        QFile dest(dirPath + "/" + it.fileName());
+        QFileInfo(dest).dir().mkpath(QStringLiteral("."));
+        dest.remove();
+        QFile::copy(it.filePath(), dest.fileName());
+    }
+    auto file = QFileInfo(dirPath + "/index.html").absoluteFilePath();
+    return QUrl::fromLocalFile(file);
+#else
+    Q_UNUSED(language);
+    return {};
+#endif
+}
+
+QUrl AndroidUtils::getDefaultFolder()
+{
+#ifdef Q_OS_ANDROID
+    QUrl fallback(QStringLiteral("file:///sdcard"));
+    auto file = QAndroidJniObject::callStaticObjectMethod(
+                "android/os/Environment", "getExternalStorageDirectory",
+                "()Ljava/io/File;");
+    if (! file.isValid())
+        return fallback;
+    auto fileString = file.callObjectMethod("toString",
+                                            "()Ljava/lang/String;");
+    if (! fileString.isValid())
+        return fallback;
+    return QUrl::fromLocalFile(fileString.toString());
+#else
+    return QUrl::fromLocalFile(
+                QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
+#endif
+}
+
+#ifdef Q_OS_ANDROID
+float AndroidUtils::getDensity()
+{
+    auto resources = QtAndroid::androidActivity().callObjectMethod(
+                "getResources", "()Landroid/content/res/Resources;");
+    if (! resources.isValid())
+        return 0;
+    auto metrics = resources.callObjectMethod(
+                "getDisplayMetrics", "()Landroid/util/DisplayMetrics;");
+    if (! metrics.isValid())
+        return 0;
+    return metrics.getField<jfloat>("density");
+}
+#endif
+
+void AndroidUtils::scanFile(const QString& pathname)
+{
+#ifdef Q_OS_ANDROID
+    // Corresponding Java code:
+    //   sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
+    //                       Uri.fromFile(File(pathname).getCanonicalFile())));
+    auto action = QAndroidJniObject::getStaticObjectField<jstring>(
+                "android/content/Intent", "ACTION_MEDIA_SCANNER_SCAN_FILE");
+    if (! action.isValid())
+        return;
+    auto pathnameString = QAndroidJniObject::fromString(pathname);
+    QAndroidJniObject file("java/io/File", "(Ljava/lang/String;)V",
+                           pathnameString.object<jstring>());
+    if (! file.isValid())
+        return;
+    auto absoluteFile = file.callObjectMethod(
+                "getAbsoluteFile", "()Ljava/io/File;");
+    if (! absoluteFile.isValid())
+        return;
+    auto uri = QAndroidJniObject::callStaticObjectMethod(
+                "android/net/Uri", "fromFile",
+                "(Ljava/io/File;)Landroid/net/Uri;", absoluteFile.object());
+    if (! uri.isValid())
+        return;
+    QAndroidJniObject intent("android/content/Intent",
+                             "(Ljava/lang/String;Landroid/net/Uri;)V",
+                             action.object<jstring>(), uri.object());
+    if (! intent.isValid())
+        return;
+    QtAndroid::androidActivity().callMethod<void>(
+                "sendBroadcast", "(Landroid/content/Intent;)V",
+                intent.object());
+#else
+    Q_UNUSED(pathname);
+#endif
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/pentobi/AndroidUtils.h b/src/pentobi/AndroidUtils.h
new file mode 100644 (file)
index 0000000..d532522
--- /dev/null
@@ -0,0 +1,53 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/AndroidUtils.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_ANDROID_UTILS_H
+#define PENTOBI_ANDROID_UTILS_H
+
+#include <QObject>
+#include <QUrl>
+
+//-----------------------------------------------------------------------------
+
+class AndroidUtils
+    : public QObject
+{
+    Q_OBJECT
+
+public:
+    using QObject::QObject;
+
+    /** Calls QtAndroid::checkPermission().
+        On platforms other than Android, always returns true. */
+    Q_INVOKABLE static bool checkPermission(const QString& permission);
+
+    Q_INVOKABLE static QUrl extractHelp(const QString& language);
+
+    /** Return a directory for storing files.
+        Avoids a dependency on qt.labs.platform only for StandardPaths and
+        handles Android better. On Android, it returns
+        android.os.Environment.getExternalStorageDirectory(). On other
+        platforms, it returns QStandardPaths::HomeLocation */
+    Q_INVOKABLE static QUrl getDefaultFolder();
+
+    /** Request the Android media scanner to scan a file.
+        Ensures that the file will be visible via MTP. On platforms other
+        than Android, this function does nothing. */
+    Q_INVOKABLE static void scanFile(const QString& pathname);
+
+#ifdef Q_OS_ANDROID
+    /** Return the logical density of the display.
+        Returns android.util.DisplayMetrics.density. This should be the same as
+        Screen.devicePixelRatio, but can be used before QGuiApplication is
+        constructed.
+        @return The density or 0 on error. */
+    static float getDensity();
+#endif
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_ANDROID_UTILS_H
diff --git a/src/pentobi/CMakeLists.txt b/src/pentobi/CMakeLists.txt
new file mode 100644 (file)
index 0000000..d31929c
--- /dev/null
@@ -0,0 +1,85 @@
+set(CMAKE_AUTOMOC TRUE)
+
+find_package(Threads)
+find_package(Qt5Concurrent 5.11 REQUIRED)
+find_package(Qt5QuickControls2 5.11 REQUIRED)
+find_package(Qt5LinguistTools 5.11 REQUIRED)
+find_package(Qt5Svg 5.11 REQUIRED)
+find_package(Qt5QuickCompiler REQUIRED)
+find_package(Qt5WebView 5.11)
+
+qt5_add_translation(pentobi_QM
+    qml/i18n/qml_de.ts
+    qml/i18n/qml_fr.ts
+    qml/i18n/qml_nb_NO.ts
+    OPTIONS -removeidentical -nounfinished
+    )
+add_custom_command(
+    OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/translations.qrc"
+    COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/qml/i18n/translations.qrc"
+    "${CMAKE_CURRENT_BINARY_DIR}"
+    DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/qml/i18n/translations.qrc" ${pentobi_QM}
+    )
+
+qt5_add_resources(pentobi_RC_SRCS
+    "${CMAKE_CURRENT_BINARY_DIR}/translations.qrc"
+    ../books/pentobi_books.qrc
+    ../icon/pentobi_icon.qrc
+    ../icon/pentobi_icon_desktop.qrc
+    )
+
+qtquick_compiler_add_resources(pentobi_RC_SRCS_QML
+    resources.qrc
+    resources_desktop.qrc
+    qml/themes/themes.qrc
+    )
+
+add_executable(pentobi WIN32
+    ${pentobi_RC_SRCS}
+    ${pentobi_RC_SRCS_QML}
+    AnalyzeGameModel.h
+    AnalyzeGameModel.cpp
+    AndroidUtils.h
+    AndroidUtils.cpp
+    GameModel.h
+    GameModel.cpp
+    ImageProvider.h
+    ImageProvider.cpp
+    Main.cpp
+    PieceModel.h
+    PieceModel.cpp
+    PlayerModel.h
+    PlayerModel.cpp
+    RatingModel.h
+    RatingModel.cpp
+    SyncSettings.h
+    )
+
+file(GLOB qml_SRC "qml/*.qml" "qml/*.js" "qml/i18n/*.ts" "qml/themes/*/*.qml")
+target_sources(pentobi PRIVATE ${qml_SRC})
+
+target_compile_definitions(pentobi PRIVATE
+    QT_DEPRECATED_WARNINGS
+    QT_DISABLE_DEPRECATED_BEFORE=0x051100
+    QT_NO_NARROWING_CONVERSIONS_IN_CONNECT
+    PENTOBI_HELP_DIR="${CMAKE_INSTALL_FULL_DATAROOTDIR}/help"
+    VERSION="${PENTOBI_VERSION}"
+    )
+
+target_link_libraries(pentobi
+    pentobi_paint
+    pentobi_mcts
+    Qt5::Concurrent
+    Qt5::Qml
+    Qt5::QuickControls2
+    Qt5::Svg
+    Threads::Threads
+    )
+
+if (Qt5WebView_FOUND AND NOT PENTOBI_OPEN_HELP_EXTERNALLY)
+    target_link_libraries(pentobi Qt5::WebView)
+else()
+    target_compile_definitions(pentobi PRIVATE PENTOBI_OPEN_HELP_EXTERNALLY)
+endif()
+
+install(TARGETS pentobi DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/src/pentobi/GameModel.cpp b/src/pentobi/GameModel.cpp
new file mode 100644 (file)
index 0000000..1c70b52
--- /dev/null
@@ -0,0 +1,1930 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/GameModel.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "GameModel.h"
+
+#include <cerrno>
+#include <cmath>
+#include <cstring>
+#include <fstream>
+#include <QClipboard>
+#include <QGuiApplication>
+#include <QDebug>
+#include <QDir>
+#include <QFileInfo>
+#include <QSettings>
+#include <QTextCodec>
+#include "AndroidUtils.h"
+#include "libboardgame_sgf/SgfUtil.h"
+#include "libboardgame_sgf/TreeReader.h"
+#include "libpentobi_base/MoveMarker.h"
+#include "libpentobi_base/NodeUtil.h"
+#include "libpentobi_base/PentobiTreeWriter.h"
+#include "libpentobi_base/TreeUtil.h"
+
+using namespace std;
+using libboardgame_sgf::SgfError;
+using libboardgame_sgf::TreeReader;
+using libboardgame_sgf::back_to_main_variation;
+using libboardgame_sgf::beginning_of_branch;
+using libboardgame_sgf::find_next_comment;
+using libboardgame_sgf::get_last_node;
+using libboardgame_sgf::has_comment;
+using libboardgame_sgf::has_earlier_variation;
+using libboardgame_sgf::is_main_variation;
+using libboardgame_util::ArrayList;
+using libboardgame_util::get_letter_coord;
+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::has_setup;
+using libpentobi_base::get_move_number;
+using libpentobi_base::get_moves_left;
+using libpentobi_base::get_move_node;
+
+//-----------------------------------------------------------------------------
+
+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() < qreal(0.01);
+}
+
+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);
+}
+
+/** Board uses 4 starting points per Color in GembloQ for technical reasons,
+    GameModel only needs one for displaying the colored dot, shifted to the
+    center of a square. */
+QPointF getGembloQStartingPoint(const Board& bd, Color c)
+{
+    auto p = bd.get_starting_points(c)[0];
+    auto& geo = bd.get_geometry();
+    qreal x = geo.get_x(p);
+    qreal y = geo.get_y(p);
+    if (geo.get_x(p) % 2 == 0)
+        x -= 0.5;
+    else
+        x += 0.5;
+    if (geo.get_y(p) % 2 == 0)
+        y += 0.5;
+    else
+        y -= 0.5;
+    return {x, y};
+}
+
+/** Simple heuristic used for sorting the list used in GameModel::findMove().
+    Prefers larger pieces, and moves of the same piece (in that order). */
+float getHeuristic(const Board& bd, Move mv)
+{
+    auto piece = bd.get_move_piece(mv);
+    auto points = bd.get_piece_info(piece).get_score_points();
+    return Piece::max_pieces * points - piece.to_int();
+}
+
+/** 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;
+    }
+    return nuSiblingMoves != 1;
+}
+
+} //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()),
+      m_nuPlayers(getBoard().get_nu_players())
+{
+    loadRecentFiles();
+    initGame(m_game.get_variant());
+    createPieceModels();
+    updateProperties();
+}
+
+GameModel::~GameModel() = default;
+
+PieceModel* GameModel::addEmpty(const QPoint& pos)
+{
+    if (! checkSetupAllowed())
+        return nullptr;
+    auto move = getMoveAt(pos);
+    if (move.is_null())
+        return nullptr;
+    auto c = move.color;
+    auto mv = move.move;
+    auto& bd = getBoard();
+    LIBBOARDGAME_ASSERT(bd.get_setup().placements[c].contains(mv));
+    auto gameCoord = getGameCoord(bd, mv);
+    PieceModel* result = nullptr;
+    for (auto& variant : qAsConst(m_pieceModels[c]))
+    {
+        auto pieceModel = qvariant_cast<PieceModel*>(variant);
+        if (compareGameCoord(pieceModel->gameCoord(), gameCoord))
+        {
+            result = pieceModel;
+            break;
+        }
+    }
+    preparePositionChange();
+    m_game.remove_setup(c, mv);
+    setSetupPlayer();
+    updateProperties();
+    return result;
+}
+
+void GameModel::addRecentFile(const QString& file)
+{
+    m_recentFiles.removeAll(file);
+    m_recentFiles.prepend(file);
+    while (m_recentFiles.length() > maxRecentFiles)
+        m_recentFiles.removeLast();
+    QSettings settings;
+    settings.setValue(QStringLiteral("recentFiles"), m_recentFiles);
+    emit recentFilesChanged();
+}
+
+void GameModel::addSetup(PieceModel* pieceModel, QPointF coord)
+{
+    if (! checkSetupAllowed())
+        return;
+    Color c(static_cast<Color::IntType>(pieceModel->color()));
+    Move mv;
+    if (! findMove(*pieceModel, pieceModel->state(), coord, mv))
+        return;
+    preparePositionChange();
+    preparePieceGameCoord(pieceModel, mv);
+    pieceModel->setIsPlayed(true);
+    preparePieceTransform(pieceModel, mv);
+    try
+    {
+        m_game.add_setup(c, mv);
+    }
+    catch (const SgfError& error)
+    {
+        m_error = error.what();
+        emit invalidSgfFile();
+    }
+    setSetupPlayer();
+    updateProperties();
+}
+
+void GameModel::autoSave()
+{
+    auto& tree = m_game.get_tree();
+    QSettings settings;
+    settings.setValue(QStringLiteral("variant"),
+                      to_string_id(m_game.get_variant()));
+    if (! m_file.isEmpty() && ! m_isModified)
+        settings.remove(QStringLiteral("autosave"));
+    else
+        settings.setValue(QStringLiteral("autosave"), getSgf());
+    settings.setValue(QStringLiteral("file"), m_file);
+    settings.setValue(QStringLiteral("fileDate"), m_fileDate);
+    settings.setValue(QStringLiteral("isModified"), m_isModified);
+    m_autosaveDate = QDateTime::currentDateTime();
+    settings.setValue(QStringLiteral("autosaveDate"), m_autosaveDate);
+    QVariantList location;
+    uint depth = 0;
+    auto node = &m_game.get_current();
+    while (node != &tree.get_root())
+    {
+        auto& parent = node->get_parent();
+        if (parent.get_nu_children() > 1)
+            location.prepend(parent.get_child_index(*node));
+        node = &parent;
+        ++depth;
+    }
+    location.prepend(depth);
+    settings.setValue(QStringLiteral("autosaveLocation"), location);
+}
+
+void GameModel::backToMainVar()
+{
+    gotoNode(back_to_main_variation(m_game.get_current()));
+}
+
+void GameModel::changeGameVariant(const QString& gameVariant)
+{
+    Variant variant;
+    if (! parse_variant_id(gameVariant.toLocal8Bit().constData(), variant))
+    {
+        qWarning("GameModel: invalid game variant");
+        return;
+    }
+    initGameVariant(variant);
+    setIsModified(false);
+    clearFile();
+}
+
+bool GameModel::checkAutosaveModifiedOutside()
+{
+    QSettings settings;
+    auto autosaveDate =
+            settings.value(QStringLiteral("autosaveDate")).toDateTime();
+    return m_autosaveDate.isValid() && autosaveDate.isValid()
+            && m_autosaveDate != autosaveDate
+            && settings.value(QStringLiteral("isModified")).toBool()
+            && settings.value(QStringLiteral("autosave")).toByteArray() != getSgf();
+}
+
+bool GameModel::checkFileExists(const QString& file)
+{
+    return QFileInfo::exists(file);
+}
+
+bool GameModel::checkFileModifiedOutside()
+{
+    if (m_file.isEmpty() || ! m_fileDate.isValid())
+        return false;
+    QFileInfo fileInfo(m_file);
+    if (! fileInfo.exists())
+        return false;
+    return fileInfo.lastModified() != m_fileDate;
+}
+
+/** Check if setup is allowed in the current position.
+    Currently, we support 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).*/
+bool GameModel::checkSetupAllowed() const
+{
+    return ! m_canGoBackward && ! m_canGoForward && m_moveNumber == 0;
+}
+
+void GameModel::clearFile()
+{
+    if (m_file.isEmpty())
+        return;
+    m_file.clear();
+    emit fileChanged();
+}
+
+void GameModel::clearRecentFiles()
+{
+    m_recentFiles.clear();
+    QSettings settings;
+    settings.setValue(QStringLiteral("recentFiles"), m_recentFiles);
+    emit recentFilesChanged();
+}
+
+bool GameModel::createFolder(const QUrl& folder)
+{
+    auto localFolder = folder.toLocalFile();
+    if (! QDir().mkdir(localFolder)) {
+        m_error = QString::fromLocal8Bit(strerror(errno));
+        return false;
+    }
+    AndroidUtils::scanFile(localFolder);
+    return true;
+}
+
+void GameModel::createPieceModels()
+{
+    createPieceModels(Color(0));
+    createPieceModels(Color(1));
+    if (m_nuColors > 2)
+        createPieceModels(Color(2));
+    else
+        m_pieceModels[Color(2)].clear();
+    if (m_nuColors > 3)
+        createPieceModels(Color(3));
+    else
+        m_pieceModels[Color(3)].clear();
+}
+
+void GameModel::createPieceModels(Color c)
+{
+    auto& bd = getBoard();
+    auto nuPieces = bd.get_nu_uniq_pieces();
+    m_pieceModels[c].clear();
+    m_pieceModels[c].reserve(nuPieces);
+    for (Piece::IntType i = 0; i < nuPieces; ++i)
+    {
+        Piece piece(i);
+        auto nuInstances = bd.get_piece_info(piece).get_nu_instances();
+        for (unsigned j = 0; j < nuInstances; ++j)
+        {
+            auto variant =
+                    QVariant::fromValue(new PieceModel(this, bd, piece, c));
+            m_pieceModels[c].append(variant);
+        }
+    }
+}
+
+QString GameModel::decode(const string& s) const
+{
+    return m_textCodec->toUnicode(s.c_str());
+}
+
+void GameModel::deleteAllVar()
+{
+    if (! is_main_variation(m_game.get_current()))
+        preparePositionChange();
+    m_game.delete_all_variations();
+    updateProperties();
+}
+
+
+QByteArray GameModel::encode(const QString& s) const
+{
+    return m_textCodec->fromUnicode(s);
+}
+
+GameMove* GameModel::findMoveNext()
+{
+    prepareFindMove();
+    if (m_legalMoves->empty())
+        return nullptr;
+    auto i = m_legalMoveIndex >= m_legalMoves->size() ? 0 : m_legalMoveIndex;
+    auto mv = (*m_legalMoves)[i];
+    m_legalMoveIndex = i + 1;
+    return new GameMove(this, ColorMove(getBoard().get_to_play(), mv));
+}
+
+GameMove* GameModel::findMovePrevious()
+{
+    prepareFindMove();
+    if (m_legalMoves->empty())
+        return nullptr;
+    auto i = m_legalMoveIndex > 1 ? m_legalMoveIndex - 2
+                                  : m_legalMoves->size() - 1;
+    auto mv = (*m_legalMoves)[i];
+    m_legalMoveIndex = i + 1;
+    return new GameMove(this, ColorMove(getBoard().get_to_play(), mv));
+}
+
+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());
+    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);
+    auto pointType = transform->get_point_type();
+    auto dx = coord.x() - center.x();
+    auto dy = coord.y() - center.y();
+    int offX;
+    if (bd.get_piece_set() == PieceSet::gembloq)
+    {
+        // In GembloQ, every piece has at least one full square, so we can use
+        // half the x resolution, which makes positioning easier for the user.
+        if (pointType == 0 || pointType == 2)
+            offX = static_cast<int>(round(dx * qreal(0.5))) * 2;
+        else
+            offX = static_cast<int>(round((dx - 1) * qreal(0.5))) * 2 + 1;
+    }
+    else
+        offX = static_cast<int>(round(dx));
+    auto offY = static_cast<int>(round(dy));
+    auto& geo = bd.get_geometry();
+    if (geo.get_point_type(offX, offY) != pointType)
+        return false;
+    MovePoints points;
+    for (auto& p : piecePoints)
+    {
+        int x = p.x + offX;
+        int y = p.y + offY;
+        if (! geo.is_onboard(x, y))
+            return false;
+        points.push_back(geo.get_point(x, y));
+    }
+    return bd.find_move(points, piece, mv);
+}
+
+bool GameModel::findNextComment()
+{
+    auto node = find_next_comment(m_game.get_current());
+    if (! node)
+        return false;
+    gotoNode(*node);
+    return true;
+}
+
+bool GameModel::findNextCommentContinueFromRoot()
+{
+    auto node = &m_game.get_root();
+    if (! has_comment(*node))
+        node = find_next_comment(*node);
+    if (! node)
+        return false;
+    gotoNode(*node);
+    return true;
+}
+
+PieceModel* GameModel::findUnplayedPieceModel(Color c, Piece piece)
+{
+    for (auto& variant : qAsConst(m_pieceModels[c]))
+    {
+        auto pieceModel = qvariant_cast<PieceModel*>(variant);
+        if (pieceModel->getPiece() == piece && ! pieceModel->isPlayed())
+            return pieceModel;
+    }
+    return nullptr;
+}
+
+QVariantList GameModel::getPieceModels(int color)
+{
+    if (color >= 0 && color <= static_cast<int>(Color::range))
+        return m_pieceModels[Color(static_cast<Color::IntType>(color))];
+    return m_pieceModels[Color(0)];
+}
+
+QString GameModel::getPlayerString(int player)
+{
+    auto variant = m_game.get_variant();
+    bool isMulticolor = (m_nuColors > m_nuPlayers && variant != Variant::classic_3);
+    switch (player) {
+    case 0:
+        if (isMulticolor)
+            return tr("Blue/Red");
+        else if (variant == Variant::duo)
+            return tr("Purple");
+        else if (variant == Variant::junior)
+            return tr("Green");
+        else
+            return tr("Blue");
+    case 1:
+        if (isMulticolor)
+            return tr("Yellow/Green");
+        else if (variant == Variant::duo || variant == Variant::junior)
+            return tr("Orange");
+        else if (m_nuColors == 2)
+            return tr("Green");
+        else
+            return tr("Yellow");
+    case 2:
+        return tr("Red");
+    case 3:
+        return tr("Green");
+    }
+    return {};
+}
+
+Variant GameModel::getInitialGameVariant()
+{
+    QSettings settings;
+    auto variantString = settings.value(QStringLiteral("variant")).toString();
+    Variant variant;
+    if (! parse_variant_id(variantString.toLocal8Bit().constData(), variant))
+        variant = Variant::duo;
+    return variant;
+}
+
+QString GameModel::getMoveAnnotation(int moveNumber)
+{
+    if (moveNumber <= 0)
+        return {};
+    auto node = get_move_node(m_game.get_tree(), m_game.get_current(),
+                              static_cast<unsigned>(moveNumber));
+    if (node == nullptr)
+        return {};
+    return getMoveAnnotationAtNode(*node);
+}
+
+QString GameModel::getMoveAnnotationAtNode(const SgfNode& node) const
+{
+    try
+    {
+        if (m_game.get_good_move(node) == 2)
+            return QStringLiteral("‼");
+        if (m_game.get_good_move(node) == 1)
+            return QStringLiteral("!");
+        if (m_game.is_interesting_move(node))
+            return QStringLiteral("⁉");
+        if (m_game.is_doubtful_move(node))
+            return QStringLiteral("⁈");
+        if (m_game.get_bad_move(node) == 1)
+            return QStringLiteral("?");
+        if (m_game.get_bad_move(node) == 2)
+            return QStringLiteral("⁇");
+    }
+    catch (const SgfError&)
+    {
+        // Silently ignore GM, BM properties with invalid value
+    }
+    return {};
+}
+
+ColorMove GameModel::getMoveAt(const QPoint& pos) const
+{
+    auto& bd = getBoard();
+    auto& geo = bd.get_geometry();
+    if (pos.x() < 0 || pos.y() < 0)
+        return ColorMove::null();
+    if (! geo.is_onboard(pos.x(), pos.y()))
+        return ColorMove::null();
+    auto p = geo.get_point(static_cast<unsigned>(pos.x()),
+                           static_cast<unsigned>(pos.y()));
+    auto s = bd.get_point_state(p);
+    if (s.is_empty())
+        return ColorMove::null();
+    auto c = s.to_color();
+    auto mv = bd.get_move_at(p);
+    return {c, mv};
+}
+
+int GameModel::getMoveNumberAt(const QPoint& pos)
+{
+    auto move = getMoveAt(pos);
+    if (move.is_null())
+        return -1;
+    auto n = m_moveNumber;
+    auto& tree = m_game.get_tree();
+    auto node = &m_game.get_current();
+    do
+    {
+        if (tree.has_move(*node))
+        {
+            if (tree.get_move(*node) == move)
+                return n;
+            --n;
+        }
+        node = node->get_parent_or_null();
+    }
+    while (node != nullptr);
+    return -1;
+}
+
+QString GameModel::getResultMessage()
+{
+    auto& bd = getBoard();
+    auto variant = bd.get_variant();
+    if (variant == Variant::duo)
+    {
+        auto score = static_cast<double>(m_points0 - m_points1);
+        if (score == 1)
+            return tr("Purple wins with 1 point.");
+        if (score > 0)
+            return tr("Purple wins with %L1 points.").arg(score);
+        if (score == -1)
+            return tr("Orange wins with 1 point.");
+        if (score < 0)
+            return tr("Orange wins with %L1 points.").arg(-score);
+        return tr("Game ends in a tie.");
+    }
+    if (variant == Variant::junior)
+    {
+        auto score = static_cast<double>(m_points0 - m_points1);
+        if (score == 1)
+            return tr("Green wins with 1 point.");
+        if (score > 0)
+            return tr("Green wins with %L1 points.").arg(score);
+        if (score == -1)
+            return tr("Orange wins with 1 point.");
+        if (score < 0)
+            return tr("Orange wins with %L1 points.").arg(-score);
+        return tr("Game ends in a tie.");
+    }
+    bool breakTies = (bd.get_piece_set() == PieceSet::callisto);
+    if (m_nuColors == 2)
+    {
+        auto score = static_cast<double>(m_points0 - m_points1);
+        if (score == 1)
+            return tr("Blue wins with 1 point.");
+        if (score > 0)
+            return tr("Blue wins with %L1 points.").arg(score);
+        if (score == -1)
+            return tr("Green wins with 1 point.");
+        if (score < 0)
+            return tr("Green wins with %L1 points.").arg(-score);
+        if (breakTies)
+            //: Game variant with tie-breaker rule made later player win.
+            return tr("Green wins (tie resolved).");
+        return tr("Game ends in a tie.");
+    }
+    if (m_nuColors == 4 && m_nuPlayers == 2)
+    {
+        auto score = static_cast<double>(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 %L1 points.").arg(score);
+        if (score == -1)
+            return tr("Yellow/Green wins with 1 point.");
+        if (score < 0)
+            return tr("Yellow/Green wins with %L1 points.").arg(-score);
+        if (breakTies)
+            //: Game variant with tie-breaker rule made later player win.
+            return tr("Yellow/Green wins (tie resolved).");
+        return tr("Game ends in a tie.");
+    }
+    if (m_nuPlayers == 3)
+    {
+        auto maxPoints = 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)
+            //: Game variant with tie-breaker rule made later player win.
+            return tr("Red wins (tie resolved).");
+        if (m_points1 == maxPoints && breakTies)
+            //: Game variant with tie-breaker rule made later player win.
+            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({m_points0, m_points1, 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)
+        //: Game variant with tie-breaker rule made later player win.
+        return tr("Green wins (tie resolved).");
+    if (m_points2 == maxPoints && breakTies)
+        //: Game variant with tie-breaker rule made later player win.
+        return tr("Red wins (tie resolved).");
+    if (m_points1 == maxPoints && breakTies)
+        //: Game variant with tie-breaker rule made later player win.
+        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.");
+}
+
+QByteArray GameModel::getSgf() const
+{
+    auto& tree = m_game.get_tree();
+    ostringstream s;
+    PentobiTreeWriter writer(s, tree);
+    writer.set_indent(-1);
+    writer.write();
+    return QByteArray(s.str().c_str());
+}
+
+QString GameModel::getVariationInfo() const
+{
+    auto moveNumber = getBoard().get_nu_moves();
+    QString s = QString::number(moveNumber);
+    unsigned moveIndex;
+    if (getVariationIndex(m_game.get_tree(), m_game.get_current(), moveIndex))
+        s.append(get_letter_coord(moveIndex).c_str());
+    return s;
+}
+
+void GameModel::goBackward()
+{
+    gotoNode(m_game.get_current().get_parent_or_null());
+}
+
+void GameModel::goBackward10()
+{
+    auto node = &m_game.get_current();
+    for (unsigned i = 0; i < 10; ++i)
+    {
+        auto parent = node->get_parent_or_null();
+        if (parent == nullptr)
+            break;
+        node = parent;
+    }
+    gotoNode(node);
+}
+
+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::goForward10()
+{
+    auto node = &m_game.get_current();
+    for (unsigned i = 0; i < 10; ++i)
+    {
+        auto child = node->get_first_child_or_null();
+        if (child == nullptr)
+            break;
+        node = child;
+    }
+    gotoNode(node);
+}
+
+void GameModel::goNextVar()
+{
+    gotoNode(m_game.get_current().get_sibling());
+}
+
+void GameModel::goPrevVar()
+{
+    gotoNode(m_game.get_current().get_previous_sibling());
+}
+
+void GameModel::gotoBeginningOfBranch()
+{
+    gotoNode(beginning_of_branch(m_game.get_current()));
+}
+
+void GameModel::gotoMove(int n)
+{
+    if (n == 0)
+        goBeginning();
+    else if (n > 0)
+        gotoNode(get_move_node(m_game.get_tree(), m_game.get_current(),
+                               static_cast<unsigned>(n)));
+}
+
+void GameModel::gotoNode(const SgfNode& node)
+{
+    if (&node == &m_game.get_current())
+        return;
+    preparePositionChange();
+    try
+    {
+        m_game.goto_node(node);
+    }
+    catch (const SgfError& error)
+    {
+        m_error = error.what();
+        emit invalidSgfFile();
+    }
+    updateProperties();
+}
+
+void GameModel::gotoNode(const SgfNode* node)
+{
+    if (node)
+        gotoNode(*node);
+}
+
+void GameModel::initGame(Variant variant)
+{
+    m_game.init(variant);
+#ifdef VERSION
+    m_game.set_application("Pentobi", VERSION);
+#else
+    m_game.set_application("Pentobi");
+#endif
+    m_game.set_date_today();
+    setUtf8();
+    updateGameInfo();
+}
+
+void GameModel::initGameVariant(Variant variant)
+{
+    if (m_game.get_variant() != variant)
+        initGame(variant);
+    auto& bd = getBoard();
+    set(m_nuColors, static_cast<unsigned>(bd.get_nu_colors()),
+        &GameModel::nuColorsChanged);
+    set(m_nuPlayers, bd.get_nu_players(), &GameModel::nuPlayersChanged);
+    m_lastMovePieceModel = nullptr;
+    createPieceModels();
+    m_gameVariant = to_string_id(variant);
+    emit gameVariantChanged();
+    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()));
+    return getBoard().is_legal(c, mv);
+}
+
+bool GameModel::isLegalSetupPos(PieceModel* pieceModel, const QString& state,
+                                QPointF coord) const
+{
+    Move mv;
+    if (! findMove(*pieceModel, state, coord, mv))
+        return false;
+    auto& bd = getBoard();
+    for (auto p : bd.get_move_points(mv))
+        if (! bd.get_point_state(p).is_empty())
+            return false;
+    return true;
+}
+
+void GameModel::keepOnlyPosition()
+{
+    m_game.keep_only_position();
+    updateProperties();
+}
+
+void GameModel::keepOnlySubtree()
+{
+    m_game.keep_only_subtree();
+    updateProperties();
+}
+
+bool GameModel::loadAutoSave()
+{
+    QSettings settings;
+    auto file = settings.value(QStringLiteral("file")).toString();
+    auto isModified = settings.value(QStringLiteral("isModified")).toBool();
+    if (! file.isEmpty() && ! isModified)
+    {
+        if (! checkFileExists(file) || ! openFile(file))
+            return false;
+        updateFileInfo(file);
+        m_autosaveDate = m_fileDate;
+        settings.setValue(QStringLiteral("autosaveDate"), m_autosaveDate);
+    }
+    else
+    {
+        if (! openByteArray(
+                    settings.value(QStringLiteral("autosave")).toByteArray()))
+            return false;
+        m_fileDate = settings.value(QStringLiteral("fileDate")).toDateTime();
+        m_autosaveDate =
+                settings.value(QStringLiteral("autosaveDate")).toDateTime();
+        setFile(file);
+    }
+    setIsModified(isModified);
+    restoreAutoSaveLocation();
+    updateProperties();
+    return true;
+}
+
+void GameModel::loadRecentFiles()
+{
+    QSettings settings;
+    m_recentFiles =
+            settings.value(QStringLiteral("recentFiles")).toStringList();
+    QMutableListIterator<QString> i(m_recentFiles);
+    while (i.hasNext()) {
+        auto file = i.next();
+        if (file.isEmpty() || ! QFileInfo::exists(file))
+            i.remove();
+    }
+    while (m_recentFiles.length() > maxRecentFiles)
+        m_recentFiles.removeLast();
+    emit recentFilesChanged();
+}
+
+bool GameModel::openByteArray(const QByteArray& byteArray)
+{
+    istringstream in(byteArray.constData());
+    clearFile();
+    if (! openStream(in))
+        return false;
+    goEnd();
+    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()
+{
+    preparePositionChange();
+    auto& bd = getBoard();
+    m_game.set_to_play(bd.get_next(bd.get_to_play()));
+    setSetupPlayer();
+    updateProperties();
+}
+
+PieceModel* GameModel::nextPiece(PieceModel* currentPickedPiece)
+{
+    auto& bd = getBoard();
+    auto c = bd.get_to_play();
+    if (bd.get_pieces_left(c).empty())
+        return nullptr;
+    auto nuUniqPieces = bd.get_nu_uniq_pieces();
+    Piece::IntType i;
+    if (currentPickedPiece != nullptr)
+        i = static_cast<Piece::IntType>(
+                    currentPickedPiece->getPiece().to_int() + 1);
+    else
+        i = 0;
+    while (true)
+    {
+        if (i >= nuUniqPieces)
+            i = 0;
+        if (bd.is_piece_left(c, Piece(i)))
+            break;
+        ++i;
+    }
+    return findUnplayedPieceModel(c, Piece(i));
+}
+
+void GameModel::newGame()
+{
+    preparePositionChange();
+    initGame(m_game.get_variant());
+    setIsModified(false);
+    clearFile();
+    for (Color c : Color::Range(Color::range))
+        for (auto& variant : qAsConst(m_pieceModels[c]))
+        {
+            auto pieceModel = qvariant_cast<PieceModel*>(variant);
+            pieceModel->setDefaultState();
+        }
+    updateProperties();
+}
+
+bool GameModel::openStream(istream& in)
+{
+    bool result = true;
+    try
+    {
+        preparePositionChange();
+        TreeReader reader;
+        reader.read(in);
+        auto root = reader.get_tree_transfer_ownership();
+        m_game.init(root);
+    }
+    catch (const runtime_error& e)
+    {
+        m_error =
+                tr("Invalid Blokus SGF file. (%1)")
+                .arg(QString::fromLocal8Bit(e.what()));
+        result = false;
+    }
+    auto charSet = m_game.get_charset();
+    if (charSet.empty())
+        m_textCodec = QTextCodec::codecForName("ISO 8859-1");
+    else
+        m_textCodec = QTextCodec::codecForName(m_game.get_charset().c_str());
+    if (! m_textCodec)
+    {
+        m_textCodec = QTextCodec::codecForName("ISO 8859-1");
+        m_error = tr("Unsupported character set");
+        result = false;
+    }
+    if (! result)
+        m_game.init();
+    auto variant = to_string_id(m_game.get_variant());
+    if (variant != m_gameVariant)
+        initGameVariant(m_game.get_variant());
+    setIsModified(false);
+    updateGameInfo();
+    updateProperties();
+    return result;
+}
+
+bool GameModel::openFile(const QString& file)
+{
+    auto absoluteFile = QFileInfo(file).absoluteFilePath();
+    ifstream in(absoluteFile.toLocal8Bit().constData());
+    if (! in)
+    {
+        m_error = QString::fromLocal8Bit(strerror(errno));
+        return false;
+    }
+    if (openStream(in))
+    {
+        updateFileInfo(absoluteFile);
+        addRecentFile(absoluteFile);
+        auto& root = m_game.get_root();
+        // Show end of game position by default unless the root node has
+        // setup stones or comments, because then it might be a puzzle and
+        // we don't want to show the solution.
+        if (! has_setup(root) && ! has_comment(root) && root.has_children())
+            goEnd();
+        return true;
+    }
+    clearFile();
+    return false;
+}
+
+bool GameModel::openClipboard()
+{
+    auto text = QGuiApplication::clipboard()->text();
+    if (text.isEmpty())
+    {
+        m_error = tr("Clipboard is empty.");
+        return false;
+    }
+    istringstream in(text.toLocal8Bit().constData());
+    if (openStream(in))
+    {
+        auto& root = m_game.get_root();
+        if (! has_setup(root) && root.has_children())
+            goEnd();
+        return true;
+    }
+    clearFile();
+    return false;
+}
+
+PieceModel* GameModel::pickNamedPiece(const QString& name,
+                                      PieceModel* currentPickedPiece)
+{
+    string nameStr(name.toLocal8Bit().constData());
+    auto& bd = getBoard();
+    auto c = bd.get_to_play();
+    Board::PiecesLeftList pieces;
+    for (Piece::IntType i = 0; i < bd.get_nu_uniq_pieces(); ++i)
+    {
+        Piece piece(i);
+        if (bd.is_piece_left(c, piece)
+                && bd.get_piece_info(piece).get_name().find(nameStr) == 0)
+            pieces.push_back(piece);
+    }
+    if (pieces.empty())
+        return nullptr;
+    Piece piece;
+    if (currentPickedPiece == nullptr)
+        piece = pieces[0];
+    else
+    {
+        piece = currentPickedPiece->getPiece();
+        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;
+        }
+    }
+    return findUnplayedPieceModel(c, piece);
+}
+
+void GameModel::playMove(GameMove* move)
+{
+    auto mv = move->get();
+    if (mv.is_null())
+        return;
+    preparePositionChange();
+    m_game.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;
+    }
+    preparePositionChange();
+    preparePieceGameCoord(pieceModel, mv);
+    pieceModel->setIsPlayed(true);
+    preparePieceTransform(pieceModel, mv);
+    m_game.play(c, mv, false);
+    updateProperties();
+}
+
+void GameModel::prepareFindMove()
+{
+    auto& bd = getBoard();
+    auto c = bd.get_to_play();
+    if (! m_legalMoves)
+        m_legalMoves = make_unique<MoveList>();
+    if (m_legalMoves->empty())
+    {
+        if (! m_marker)
+            m_marker = make_unique<MoveMarker>();
+        bd.gen_moves(c, *m_marker, *m_legalMoves);
+        m_marker->clear(*m_legalMoves);
+        sort(m_legalMoves->begin(), m_legalMoves->end(),
+             [&](Move mv1, Move mv2) {
+                 return getHeuristic(bd, mv1) > getHeuristic(bd, mv2);
+             });
+        m_legalMoveIndex = 0;
+    }
+}
+
+PieceModel* GameModel::preparePiece(GameMove* move)
+{
+    if (move == nullptr || move->get().is_null())
+        return nullptr;
+    auto c = move->get().color;
+    auto mv = move->get().move;
+    auto piece = getBoard().get_move_piece(mv);
+    for (auto& variant : qAsConst(m_pieceModels[c]))
+    {
+        auto pieceModel = qvariant_cast<PieceModel*>(variant);
+        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);
+}
+
+void GameModel::preparePositionChange()
+{
+    if (m_legalMoves)
+    {
+        m_legalMoves->clear();
+        m_legalMoveIndex = 0;
+    }
+    emit positionAboutToChange();
+}
+
+PieceModel* GameModel::previousPiece(PieceModel* currentPickedPiece)
+{
+    auto& bd = getBoard();
+    auto c = bd.get_to_play();
+    if (bd.get_pieces_left(c).empty())
+        return nullptr;
+    auto nuUniqPieces = bd.get_nu_uniq_pieces();
+    Piece::IntType i;
+    if (currentPickedPiece != nullptr)
+        i = static_cast<Piece::IntType>(currentPickedPiece->getPiece().to_int());
+    else
+        i = 0;
+    while (true)
+    {
+        if (i == 0)
+            i = static_cast<Piece::IntType>(nuUniqPieces - 1);
+        else
+            --i;
+        if (bd.is_piece_left(c, Piece(i)))
+            break;
+    }
+    return findUnplayedPieceModel(c, Piece(i));
+}
+
+void GameModel::restoreAutoSaveLocation()
+{
+    QSettings settings;
+    auto location =
+            settings.value(QStringLiteral("autosaveLocation")).value<QVariantList>();
+    if (location.empty())
+        return;
+    int index = 0;
+    bool ok;
+    auto depth = location[index++].toUInt(&ok);
+    if (! ok)
+        return;
+    auto node = &m_game.get_root();
+    while (depth > 0)
+    {
+        auto nuChildren = node->get_nu_children();
+        if (nuChildren == 0)
+            break;
+        if (nuChildren == 1)
+            node = &node->get_first_child();
+        else
+        {
+            if (index >= location.size())
+                break;
+            auto child = location[index++].toUInt(&ok);
+            if (! ok || child >= nuChildren)
+                break;
+            node = &node->get_child(child);
+        }
+        --depth;
+    }
+    gotoNode(*node);
+}
+
+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_error = QString::fromLocal8Bit(strerror(errno));
+            return false;
+        }
+    }
+    AndroidUtils::scanFile(file);
+    updateFileInfo(file);
+    setIsModified(false);
+    addRecentFile(file);
+    return true;
+}
+
+bool GameModel::saveAsciiArt(const QString& file)
+{
+    ofstream out(file.toLocal8Bit().constData());
+    getBoard().write(out, false);
+    if (! out)
+    {
+        m_error = QString::fromLocal8Bit(strerror(errno));
+        return false;
+    }
+    AndroidUtils::scanFile(file);
+    return true;
+}
+
+template<typename T>
+bool GameModel::set(T& target, const T& value,
+                    void (GameModel::*changedSignal)())
+{
+    if (target != value)
+    {
+        target = value;
+        emit (this->*changedSignal)();
+        return true;
+    }
+    return false;
+}
+
+void GameModel::setComment(const QString& comment)
+{
+    if (comment == m_comment)
+        return;
+    m_game.set_comment(encode(comment).constData());
+    m_comment = comment;
+    emit commentChanged();
+    updateIsModified();
+}
+
+void GameModel::setDate(const QString& date)
+{
+    if (date == m_date)
+        return;
+    m_date = date;
+    m_game.set_date(encode(date).constData());
+    emit dateChanged();
+    updateIsModified();
+}
+
+void GameModel::setEvent(const QString& event)
+{
+    if (event == m_event)
+        return;
+    m_event = event;
+    m_game.set_event(encode(event).constData());
+    emit eventChanged();
+    updateIsModified();
+}
+
+void GameModel::setFile(const QString& file)
+{
+    if (file == m_file)
+        return;
+    m_file = file;
+    emit fileChanged();
+}
+
+void GameModel::setIsModified(bool isModified)
+{
+    m_game.set_modified(isModified);
+    updateIsModified();
+}
+
+void GameModel::setMoveAnnotationAtNode(const SgfNode& node,
+                                        const QString& annotation)
+{
+    m_game.remove_move_annotation(node);
+    if (annotation == QStringLiteral("!"))
+        m_game.set_good_move(node);
+    else if (annotation == QStringLiteral("‼"))
+        m_game.set_good_move(node, 2);
+    else if (annotation == QStringLiteral("?"))
+        m_game.set_bad_move(node);
+    else if (annotation == QStringLiteral("⁇"))
+        m_game.set_bad_move(node, 2);
+    else if (annotation == QStringLiteral("⁉"))
+        m_game.set_interesting_move(node);
+    else if (annotation == QStringLiteral("⁈"))
+        m_game.set_doubtful_move(node);
+    updateIsModified();
+    updatePositionInfo();
+    updatePieces();
+}
+
+void GameModel::setMoveAnnotation(int moveNumber, const QString& annotation)
+{
+    if (moveNumber <= 0)
+        return;
+    auto node = get_move_node(m_game.get_tree(), m_game.get_current(),
+                              static_cast<unsigned>(moveNumber));
+    if (node == nullptr)
+        return;
+    setMoveAnnotationAtNode(*node, annotation);
+}
+
+void GameModel::setPlayerName0(const QString& name)
+{
+    if (name == m_playerName0)
+        return;
+    m_playerName0 = name;
+    m_game.set_player_name(Color(0), encode(name).constData());
+    emit playerName0Changed();
+    updateIsModified();
+}
+
+void GameModel::setPlayerName1(const QString& name)
+{
+    if (name == m_playerName1)
+        return;
+    m_playerName1 = name;
+    m_game.set_player_name(Color(1), encode(name).constData());
+    emit playerName1Changed();
+    updateIsModified();
+}
+
+void GameModel::setPlayerName2(const QString& name)
+{
+    if (name == m_playerName2)
+        return;
+    m_playerName2 = name;
+    m_game.set_player_name(Color(2), encode(name).constData());
+    emit playerName2Changed();
+    updateIsModified();
+}
+
+void GameModel::setPlayerName3(const QString& name)
+{
+    if (name == m_playerName3)
+        return;
+    m_playerName3 = name;
+    m_game.set_player_name(Color(3), encode(name).constData());
+    emit playerName3Changed();
+    updateIsModified();
+}
+
+void GameModel::setRound(const QString& round)
+{
+    if (round == m_round)
+        return;
+    m_round = round;
+    m_game.set_round(encode(round).constData());
+    emit roundChanged();
+    updateIsModified();
+}
+
+void GameModel::setShowVariations(bool showVariations)
+{
+    if (set(m_showVariations, showVariations, &GameModel::showVariationsChanged))
+        updatePieces();
+}
+
+void GameModel::setSetupPlayer()
+{
+    if (! m_game.has_setup())
+        m_game.remove_player();
+    else
+        m_game.set_player(getBoard().get_to_play());
+}
+
+void GameModel::setTime(const QString& time)
+{
+    if (time == m_time)
+        return;
+    m_time = time;
+    m_game.set_time(encode(time).constData());
+    emit playerName3Changed();
+    updateIsModified();
+}
+
+void GameModel::setUtf8()
+{
+    m_game.set_charset("UTF-8");
+    m_textCodec = QTextCodec::codecForName("UTF-8");
+}
+
+QString GameModel::suggestFileName(const QUrl& folder,
+                                   const QString& fileEnding)
+{
+    QString suffix =
+            ! fileEnding.isEmpty()
+            && ! fileEnding.startsWith(QStringLiteral(".")) ?
+                QStringLiteral(".") + fileEnding : fileEnding;
+    auto localFolder = folder.toLocalFile();
+    QString file = localFolder + '/' + tr("Untitled") + suffix;
+    if (QFileInfo::exists(file))
+        for (unsigned i = 1; ; ++i)
+        {
+            //: The argument is a number, which will be increased if a
+            //: file with the same name already exists
+            file = localFolder + '/' + tr("Untitled %1").arg(i)
+                    + suffix;
+            if (! QFileInfo::exists(file))
+                break;
+        }
+    return QUrl::fromLocalFile(file).fileName();
+}
+
+QString GameModel::suggestGameFileName(const QUrl& folder)
+{
+    if (! m_file.isEmpty())
+        return QUrl::fromLocalFile(m_file).fileName();
+    return suggestFileName(folder, QStringLiteral("blksgf"));
+}
+
+QString GameModel::suggestNewFolderName(const QUrl& folder)
+{
+    auto localFolder = folder.toLocalFile();
+    QString file = localFolder;
+    if (! file.endsWith('/'))
+        file.append('/');
+    file.append(tr("New Folder"));
+    if (QFileInfo::exists(file))
+        for (unsigned i = 1; ; ++i)
+        {
+            //: The argument is a number, which will be increased if a
+            //: folder with the same name already exists
+            file = localFolder + '/' + tr("New Folder %1").arg(i);
+            if (! QFileInfo::exists(file))
+                break;
+        }
+    return QUrl::fromLocalFile(file).fileName();
+}
+
+void GameModel::truncate()
+{
+    if (! m_game.get_current().has_parent())
+        return;
+    preparePositionChange();
+    m_game.truncate();
+    updateProperties();
+}
+
+void GameModel::truncateChildren()
+{
+    m_game.truncate_children();
+    updateProperties();
+}
+
+void GameModel::undo()
+{
+    if (! m_canUndo)
+        return;
+    preparePositionChange();
+    m_game.undo();
+    updateProperties();
+}
+
+void GameModel::updateFileInfo(const QString& file)
+{
+    setFile(file);
+    m_fileDate = QFileInfo(file).lastModified();
+}
+
+void GameModel::updateGameInfo()
+{
+    static_assert(Color::range == 4, "");
+    setPlayerName0(decode(m_game.get_player_name(Color(0))));
+    setPlayerName1(decode(m_game.get_player_name(Color(1))));
+    if (m_nuPlayers > 2)
+        setPlayerName2(decode(m_game.get_player_name(Color(2))));
+    if (m_nuPlayers > 3)
+        setPlayerName3(decode(m_game.get_player_name(Color(3))));
+    setDate(decode(m_game.get_date()));
+    setTime(decode(m_game.get_time()));
+    setEvent(decode(m_game.get_event()));
+    setRound(decode(m_game.get_round()));
+}
+
+void GameModel::updateIsModified()
+{
+    // Don't consider modified game tree as modified if it is empty and no
+    // file is associated.
+    bool isModified =
+            m_game.is_modified()
+            && (! libboardgame_sgf::is_empty(m_game.get_tree())
+                || ! m_file.isEmpty());
+    set(m_isModified, isModified, &GameModel::isModifiedChanged);
+}
+
+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 = m_pieceModels[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)
+    {
+        auto pieceModel = qvariant_cast<PieceModel*>(pieceModels[i]);
+        if (pieceModel->getPiece() == piece
+                && pieceModel->isPlayed()
+                && compareGameCoord(pieceModel->gameCoord(), gameCoord)
+                && compareTransform(pieceInfo, pieceModel->getTransform(),
+                                    transform))
+        {
+            isPlayed[i] = true;
+            return pieceModel;
+        }
+    }
+    for (int i = 0; i < pieceModels.length(); ++i)
+    {
+        auto pieceModel = qvariant_cast<PieceModel*>(pieceModels[i]);
+        if (pieceModel->getPiece() == piece && ! isPlayed[i])
+        {
+            isPlayed[i] = true;
+            // Set PieceModel.isPlayed temporarily to false, such that there is
+            // always a state transition animation (e.g. if the piece stays
+            // on the board but changes its coordinates when navigating through
+            // move variations).
+            pieceModel->setIsPlayed(false);
+            // Set gameCoord before isPlayed because the animation needs it.
+            pieceModel->setGameCoord(gameCoord);
+            pieceModel->setIsPlayed(true);
+            pieceModel->setTransform(transform);
+            return pieceModel;
+        }
+    }
+    LIBBOARDGAME_ASSERT(false);
+    return nullptr;
+}
+
+void GameModel::updatePieces()
+{
+    auto& bd = getBoard();
+    ColorMap<array<bool, Board::max_pieces>> isPlayed;
+
+    // Update pieces of setup
+    for (Color c : bd.get_colors())
+    {
+        isPlayed[c].fill(false);
+        for (Move mv : bd.get_setup().placements[c])
+        {
+            auto pieceModel = updatePiece(c, mv, isPlayed[c]);
+            pieceModel->setMoveLabel(QString());
+        }
+    }
+
+    // Update pieces of moves played after last setup or root
+    auto& tree = m_game.get_tree();
+    // We need to loop forward through the moves to ensure the persistence of
+    // the GUI pieces, see comment in updatePiece()
+    ArrayList<const SgfNode*, Board::max_moves> nodes;
+    auto node = &m_game.get_current();
+    do
+    {
+        if (tree.has_move(*node))
+            nodes.push_back(node);
+        if (has_setup(*node) || nodes.size() == decltype(nodes)::max_size)
+            break;
+        node = node->get_parent_or_null();
+    }
+    while (node);
+    PieceModel* pieceModel = nullptr;
+    int moveNumber = 1;
+    for (auto i = nodes.size(); i > 0; --i)
+    {
+        node = nodes[i - 1];
+        auto mv = tree.get_move(*node);
+        auto c = mv.color;
+        pieceModel = updatePiece(c, mv.move, isPlayed[c]);
+        QString label = QString::number(moveNumber);
+        ++moveNumber;
+        unsigned moveIndex;
+        if (m_showVariations && getVariationIndex(tree, *node, moveIndex))
+            label.append(get_letter_coord(moveIndex).c_str());
+        label.append(getMoveAnnotationAtNode(*node));
+        pieceModel->setMoveLabel(label);
+    }
+    if (pieceModel != m_lastMovePieceModel)
+    {
+        if (m_lastMovePieceModel != nullptr)
+            m_lastMovePieceModel->setIsLastMove(false);
+        if (pieceModel != nullptr)
+            pieceModel->setIsLastMove(true);
+        m_lastMovePieceModel = pieceModel;
+    }
+
+    // Update pieces not on board
+    for (Color c : bd.get_colors())
+        for (int i = 0; i < m_pieceModels[c].length(); ++i)
+        {
+            auto pieceModel = qvariant_cast<PieceModel*>(m_pieceModels[c][i]);
+            if (! isPlayed[c][i] && pieceModel->isPlayed())
+            {
+                pieceModel->setDefaultState();
+                pieceModel->setIsPlayed(false);
+                pieceModel->setMoveLabel(QString());
+            }
+        }
+}
+
+void GameModel::updatePositionInfo()
+{
+    auto& tree = m_game.get_tree();
+    auto& current = m_game.get_current();
+    auto& bd = m_game.get_board();
+    auto move = get_move_number(tree, current);
+    auto left = get_moves_left(tree, current);
+    auto total = move + left;
+    auto variation = get_variation_string(current);
+    QString positionInfo = QString::number(move);
+    if (left > 0 || move > 0)
+        positionInfo.append(getMoveAnnotationAtNode(current));
+    if (left > 0)
+    {
+        positionInfo.append('/');
+        positionInfo.append(QString::number(total));
+    }
+    if (! variation.empty())
+    {
+        positionInfo.append(" (");
+        positionInfo.append(QString::fromLocal8Bit(variation.c_str()));
+        positionInfo.append(')');
+    }
+    auto positionInfoShort = positionInfo;
+    if (positionInfo.isEmpty())
+    {
+        positionInfo = bd.has_setup() ? tr("(Setup)") : tr("(No moves)");
+        positionInfoShort = bd.has_setup() ? tr("(Setup)") : QString();
+    }
+    else
+    {
+        //: The argument is the current move number.
+        positionInfo = tr("Move %1").arg(positionInfo);
+        if (bd.get_nu_moves() == 0 && bd.has_setup())
+        {
+            positionInfo.append(' ');
+            positionInfo.append(tr("(Setup)"));
+            positionInfoShort.append(' ');
+            positionInfoShort.append(tr("(Setup)"));
+        }
+    }
+    set(m_positionInfo, positionInfo, &GameModel::positionInfoChanged);
+    set(m_positionInfoShort, positionInfoShort,
+        &GameModel::positionInfoShortChanged);
+}
+
+/** Update all properties that might change when changing the current
+    position in the game tree. */
+void GameModel::updateProperties()
+{
+    auto& bd = getBoard();
+    auto& geo = bd.get_geometry();
+    auto& tree = m_game.get_tree();
+    bool isGembloQ = (bd.get_piece_set() == PieceSet::gembloq);
+    bool isTrigon = (bd.get_piece_set() == PieceSet::trigon);
+    bool isNexos = (bd.get_board_type() == BoardType::nexos);
+    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 (isNexos)
+            m_tmpPoints.append(QPointF(4, 4));
+        else if (isGembloQ)
+            m_tmpPoints.append(getGembloQStartingPoint(bd, Color(0)));
+        else 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 (isNexos)
+            m_tmpPoints.append(QPointF(20, 4));
+        else if (isGembloQ)
+            m_tmpPoints.append(getGembloQStartingPoint(bd, Color(1)));
+        else 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 (isNexos)
+            m_tmpPoints.append(QPointF(20, 20));
+        else if (isGembloQ)
+            m_tmpPoints.append(getGembloQStartingPoint(bd, Color(2)));
+        else 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 (isNexos)
+            m_tmpPoints.append(QPointF(4, 20));
+        else if (isGembloQ)
+            m_tmpPoints.append(getGembloQStartingPoint(bd, Color(3)));
+        else 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_startingPointsAny, m_tmpPoints,
+        &GameModel::startingPointsAnyChanged);
+    auto& current = m_game.get_current();
+    set(m_canUndo,
+           ! current.has_children() && tree.has_move(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_hasEarlierVar, has_earlier_variation(current),
+        &GameModel::hasEarlierVarChanged);
+    set(m_isMainVar, is_main_variation(current), &GameModel::isMainVarChanged);
+    set(m_moveNumber, static_cast<int>(get_move_number(tree, current)),
+        &GameModel::moveNumberChanged);
+    set(m_movesLeft, static_cast<int>(get_moves_left(tree, current)),
+        &GameModel::movesLeftChanged);
+    updatePositionInfo();
+    bool isGameOver = true;
+    for (Color c : bd.get_colors())
+        if (bd.has_moves(c))
+        {
+            isGameOver = false;
+            break;
+        }
+    set(m_isBoardEmpty, bd.get_nu_onboard_pieces() == 0,
+        &GameModel::isBoardEmptyChanged);
+    set(m_isGameOver, isGameOver, &GameModel::isGameOverChanged);
+    updateIsModified();
+    updatePieces();
+    set(m_comment, decode(m_game.get_comment()), &GameModel::commentChanged);
+    set(m_toPlay, m_isGameOver ? 0u : bd.get_effective_to_play().to_int(),
+        &GameModel::toPlayChanged);
+    set(m_altPlayer,
+        bd.get_variant() == Variant::classic_3 ? bd.get_alt_player() : 0u,
+        &GameModel::altPlayerChanged);
+
+    emit positionChanged();
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/pentobi/GameModel.h b/src/pentobi/GameModel.h
new file mode 100644 (file)
index 0000000..388f9bb
--- /dev/null
@@ -0,0 +1,695 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/GameModel.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_GAME_MODEL_H
+#define PENTOBI_GAME_MODEL_H
+
+#include <QDateTime>
+#include <QUrl>
+#include "PieceModel.h"
+#include "libpentobi_base/Game.h"
+
+class QTextCodec;
+
+using namespace std;
+using libboardgame_sgf::SgfNode;
+using libpentobi_base::ColorMap;
+using libpentobi_base::ColorMove;
+using libpentobi_base::Board;
+using libpentobi_base::Game;
+using libpentobi_base::Move;
+using libpentobi_base::MoveList;
+using libpentobi_base::MoveMarker;
+using libpentobi_base::ScoreType;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+class GameMove
+    : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(int color READ color CONSTANT)
+
+public:
+    GameMove(QObject* parent, ColorMove mv)
+        : QObject(parent),
+          m_move(mv)
+    { }
+
+
+    Q_INVOKABLE bool isNull() const { return m_move.is_null(); }
+
+
+    int color() const { return static_cast<int>(m_move.color.to_int()); }
+
+    ColorMove get() const { return m_move; }
+
+private:
+    ColorMove m_move;
+};
+
+//-----------------------------------------------------------------------------
+
+class GameModel
+    : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QString gameVariant READ gameVariant NOTIFY gameVariantChanged)
+    Q_PROPERTY(QString positionInfo READ positionInfo NOTIFY positionInfoChanged)
+    Q_PROPERTY(QString positionInfoShort READ positionInfoShort NOTIFY positionInfoShortChanged)
+    Q_PROPERTY(QString comment READ comment WRITE setComment NOTIFY commentChanged)
+    Q_PROPERTY(QString file READ file NOTIFY fileChanged)
+    Q_PROPERTY(QStringList recentFiles READ recentFiles NOTIFY recentFilesChanged)
+    Q_PROPERTY(unsigned nuColors READ nuColors NOTIFY nuColorsChanged)
+    Q_PROPERTY(unsigned nuPlayers READ nuPlayers NOTIFY nuPlayersChanged)
+    Q_PROPERTY(unsigned toPlay READ toPlay NOTIFY toPlayChanged)
+    Q_PROPERTY(unsigned altPlayer READ altPlayer NOTIFY altPlayerChanged)
+    Q_PROPERTY(int moveNumber READ moveNumber NOTIFY moveNumberChanged)
+    Q_PROPERTY(int movesLeft READ movesLeft NOTIFY movesLeftChanged)
+    Q_PROPERTY(float points0 READ points0 NOTIFY points0Changed)
+    Q_PROPERTY(float points1 READ points1 NOTIFY points1Changed)
+    Q_PROPERTY(float points2 READ points2 NOTIFY points2Changed)
+    Q_PROPERTY(float points3 READ points3 NOTIFY points3Changed)
+    Q_PROPERTY(float bonus0 READ bonus0 NOTIFY bonus0Changed)
+    Q_PROPERTY(float bonus1 READ bonus1 NOTIFY bonus1Changed)
+    Q_PROPERTY(float bonus2 READ bonus2 NOTIFY bonus2Changed)
+    Q_PROPERTY(float bonus3 READ bonus3 NOTIFY bonus3Changed)
+    Q_PROPERTY(bool hasMoves0 READ hasMoves0 NOTIFY hasMoves0Changed)
+    Q_PROPERTY(bool hasMoves1 READ hasMoves1 NOTIFY hasMoves1Changed)
+    Q_PROPERTY(bool hasMoves2 READ hasMoves2 NOTIFY hasMoves2Changed)
+    Q_PROPERTY(bool hasMoves3 READ hasMoves3 NOTIFY hasMoves3Changed)
+    Q_PROPERTY(bool isBoardEmpty READ isBoardEmpty NOTIFY isBoardEmptyChanged)
+    Q_PROPERTY(bool isGameOver READ isGameOver NOTIFY isGameOverChanged)
+    Q_PROPERTY(bool isModified READ isModified WRITE setIsModified NOTIFY isModifiedChanged)
+    Q_PROPERTY(bool canUndo READ canUndo NOTIFY canUndoChanged)
+    Q_PROPERTY(bool canGoBackward READ canGoBackward NOTIFY canGoBackwardChanged)
+    Q_PROPERTY(bool canGoForward READ canGoForward NOTIFY canGoForwardChanged)
+    Q_PROPERTY(bool hasPrevVar READ hasPrevVar NOTIFY hasPrevVarChanged)
+    Q_PROPERTY(bool hasNextVar READ hasNextVar NOTIFY hasNextVarChanged)
+    Q_PROPERTY(bool hasVariations READ hasVariations NOTIFY hasVariationsChanged)
+    Q_PROPERTY(bool hasEarlierVar READ hasEarlierVar NOTIFY hasEarlierVarChanged)
+    Q_PROPERTY(bool isMainVar READ isMainVar NOTIFY isMainVarChanged)
+    Q_PROPERTY(bool showVariations MEMBER m_showVariations WRITE setShowVariations NOTIFY showVariationsChanged)
+    Q_PROPERTY(QVariantList startingPoints0 READ startingPoints0 NOTIFY startingPoints0Changed)
+    Q_PROPERTY(QVariantList startingPoints1 READ startingPoints1 NOTIFY startingPoints1Changed)
+    Q_PROPERTY(QVariantList startingPoints2 READ startingPoints2 NOTIFY startingPoints2Changed)
+    Q_PROPERTY(QVariantList startingPoints3 READ startingPoints3 NOTIFY startingPoints3Changed)
+    Q_PROPERTY(QVariantList startingPointsAny READ startingPointsAny NOTIFY startingPointsAnyChanged)
+    Q_PROPERTY(QString playerName0 READ playerName0 WRITE setPlayerName0 NOTIFY playerName0Changed)
+    Q_PROPERTY(QString playerName1 READ playerName1 WRITE setPlayerName1 NOTIFY playerName1Changed)
+    Q_PROPERTY(QString playerName2 READ playerName2 WRITE setPlayerName2 NOTIFY playerName2Changed)
+    Q_PROPERTY(QString playerName3 READ playerName3 WRITE setPlayerName3 NOTIFY playerName3Changed)
+    Q_PROPERTY(QString date READ date WRITE setDate NOTIFY dateChanged)
+    Q_PROPERTY(QString time READ time WRITE setTime NOTIFY timeChanged)
+    Q_PROPERTY(QString event READ getEvent WRITE setEvent NOTIFY eventChanged)
+    Q_PROPERTY(QString round READ getRound WRITE setRound NOTIFY roundChanged)
+
+public:
+    static const int maxRecentFiles = 9;
+
+    static Variant getInitialGameVariant();
+
+
+    explicit GameModel(QObject* parent = nullptr);
+
+    ~GameModel() override;
+
+
+    Q_INVOKABLE void addSetup(PieceModel* pieceModel, QPointF coord);
+
+    /** Remove a piece from the board.
+        Updates setup properties in the current node.
+        @param pos The point on the board in game coordinates.
+        @return The PieceModel corresponding to the removed piece or null if
+        there is no piece at this location. */
+    Q_INVOKABLE PieceModel* addEmpty(const QPoint& pos);
+
+    Q_INVOKABLE void clearRecentFiles();
+
+    Q_INVOKABLE bool createFolder(const QUrl& folder);
+
+    Q_INVOKABLE void deleteAllVar();
+
+    Q_INVOKABLE bool findNextComment();
+
+    Q_INVOKABLE bool findNextCommentContinueFromRoot();
+
+    Q_INVOKABLE int getMoveNumberAt(const QPoint& pos);
+
+    Q_INVOKABLE QString getPlayerString(int player);
+
+    Q_INVOKABLE QString getVariationInfo() const;
+
+    Q_INVOKABLE bool isLegalPos(PieceModel* pieceModel, const QString& state,
+                                QPointF coord) const;
+
+    Q_INVOKABLE bool isLegalSetupPos(PieceModel* pieceModel,
+                                     const QString& state,
+                                     QPointF coord) const;
+
+    Q_INVOKABLE void keepOnlyPosition();
+
+    Q_INVOKABLE void keepOnlySubtree();
+
+    Q_INVOKABLE void nextColor();
+
+    Q_INVOKABLE bool openByteArray(const QByteArray& byteArray);
+
+    Q_INVOKABLE bool openClipboard();
+
+    Q_INVOKABLE bool openFile(const QString& file);
+
+    Q_INVOKABLE PieceModel* preparePiece(GameMove* move);
+
+    Q_INVOKABLE void playPiece(PieceModel* pieceModel, QPointF coord);
+
+    Q_INVOKABLE void playMove(GameMove* move);
+
+    Q_INVOKABLE void newGame();
+
+    Q_INVOKABLE void undo();
+
+    Q_INVOKABLE QString getMoveAnnotation(int moveNumber);
+
+    Q_INVOKABLE void goBeginning();
+
+    Q_INVOKABLE void goBackward();
+
+    Q_INVOKABLE void goBackward10();
+
+    Q_INVOKABLE void goForward();
+
+    Q_INVOKABLE void goForward10();
+
+    Q_INVOKABLE void goEnd();
+
+    Q_INVOKABLE void goNextVar();
+
+    Q_INVOKABLE void goPrevVar();
+
+    Q_INVOKABLE void backToMainVar();
+
+    Q_INVOKABLE void gotoBeginningOfBranch();
+
+    Q_INVOKABLE void gotoMove(int n);
+
+    Q_INVOKABLE void changeGameVariant(const QString& gameVariant);
+
+    Q_INVOKABLE void autoSave();
+
+    Q_INVOKABLE bool loadAutoSave();
+
+    Q_INVOKABLE bool save(const QString& file);
+
+    Q_INVOKABLE bool saveAsciiArt(const QString& file);
+
+    Q_INVOKABLE void setMoveAnnotation(int moveNumber,
+                                       const QString& annotation);
+
+    Q_INVOKABLE void makeMainVar();
+
+    Q_INVOKABLE void moveDownVar();
+
+    Q_INVOKABLE void moveUpVar();
+
+    Q_INVOKABLE void truncate();
+
+    Q_INVOKABLE void truncateChildren();
+
+    Q_INVOKABLE QString getResultMessage();
+
+    Q_INVOKABLE bool checkFileExists(const QString& file);
+
+    Q_INVOKABLE bool checkFileModifiedOutside();
+
+    Q_INVOKABLE bool checkAutosaveModifiedOutside();
+
+    Q_INVOKABLE GameMove* findMoveNext();
+
+    Q_INVOKABLE GameMove* findMovePrevious();
+
+    Q_INVOKABLE PieceModel* pickNamedPiece(const QString& name,
+                                           PieceModel* currentPickedPiece);
+
+    Q_INVOKABLE PieceModel* nextPiece(PieceModel* currentPickedPiece);
+
+    Q_INVOKABLE PieceModel* previousPiece(PieceModel* currentPickedPiece);
+
+    Q_INVOKABLE QString suggestFileName(const QUrl& folder,
+                                        const QString& fileEnding);
+
+    Q_INVOKABLE QString suggestGameFileName(const QUrl& folder);
+
+    Q_INVOKABLE QString suggestNewFolderName(const QUrl& folder);
+
+    Q_INVOKABLE QString getError() const { return m_error; }
+
+    Q_INVOKABLE QVariantList getPieceModels(int color);
+
+
+    QByteArray getSgf() const;
+
+    void setComment(const QString& comment);
+
+    const QString& gameVariant() const { return m_gameVariant; }
+
+    const QString& positionInfo() const { return m_positionInfo; }
+
+    const QString& positionInfoShort() const { return m_positionInfoShort; }
+
+    const QString& file() const { return m_file; }
+
+    const QString& comment() const { return m_comment; }
+
+    unsigned nuColors() const { return m_nuColors; }
+
+    unsigned nuPlayers() const { return m_nuPlayers; }
+
+    unsigned toPlay() const { return m_toPlay; }
+
+    unsigned altPlayer() const { return m_altPlayer; }
+
+    int moveNumber() const { return m_moveNumber; }
+
+    int movesLeft() const { return m_movesLeft; }
+
+    float points0() const { return m_points0; }
+
+    float points1() const { return m_points1; }
+
+    float points2() const { return m_points2; }
+
+    float points3() const { return m_points3; }
+
+    float bonus0() const { return m_bonus0; }
+
+    float bonus1() const { return m_bonus1; }
+
+    float bonus2() const { return m_bonus2; }
+
+    float bonus3() const { return m_bonus3; }
+
+    bool hasMoves0() const { return m_hasMoves0; }
+
+    bool hasMoves1() const { return m_hasMoves1; }
+
+    bool hasMoves2() const { return m_hasMoves2; }
+
+    bool hasMoves3() const { return m_hasMoves3; }
+
+    bool isBoardEmpty() const { return m_isBoardEmpty; }
+
+    bool isGameOver() const { return m_isGameOver; }
+
+    bool isModified() const { return m_isModified; }
+
+    bool canUndo() const { return m_canUndo; }
+
+    bool canGoBackward() const { return m_canGoBackward; }
+
+    bool canGoForward() const { return m_canGoForward; }
+
+    bool hasEarlierVar() const { return m_hasEarlierVar; }
+
+    bool hasPrevVar() const { return m_hasPrevVar; }
+
+    bool hasNextVar() const { return m_hasNextVar; }
+
+    bool hasVariations() const { return m_hasVariations; }
+
+    bool isMainVar() const { return m_isMainVar; }
+
+    const QStringList& recentFiles() const { return m_recentFiles; }
+
+    const QVariantList& startingPoints0() const { return m_startingPoints0; }
+
+    const QVariantList& startingPoints1() const { return m_startingPoints1; }
+
+    const QVariantList& startingPoints2() const { return m_startingPoints2; }
+
+    const QVariantList& startingPoints3() const { return m_startingPoints3; }
+
+    const QVariantList& startingPointsAny() const { return m_startingPointsAny; }
+
+    const QString& playerName0() const { return m_playerName0; }
+
+    const QString& playerName1() const { return m_playerName1; }
+
+    const QString& playerName2() const { return m_playerName2; }
+
+    const QString& playerName3() const { return m_playerName3; }
+
+    const QString& date() const { return m_date; }
+
+    const QString& time() const { return m_time; }
+
+    // Avoid conflict with QObject::event()
+    const QString& getEvent() const { return m_event; }
+
+    // Avoid conflict with round(), which cannot be resolved by using fully
+    // qualified std::round(), because with many GCC versions it's in the
+    // global namespace (http://stackoverflow.com/questions/1882689)
+    const QString& getRound() const { return m_round; }
+
+    void setIsModified(bool isModified);
+
+    void setMoveAnnotationAtNode(const SgfNode& node,
+                                 const QString& annotation);
+
+    void setPlayerName0(const QString& name);
+
+    void setPlayerName1(const QString& name);
+
+    void setPlayerName2(const QString& name);
+
+    void setPlayerName3(const QString& name);
+
+    void setDate(const QString& date);
+
+    void setShowVariations(bool showVariations);
+
+    void setTime(const QString& time);
+
+    void setEvent(const QString& event);
+
+    void setRound(const QString& round);
+
+    const Game& getGame() const { return m_game; }
+
+    const Board& getBoard() const { return m_game.get_board(); }
+
+    void gotoNode(const SgfNode& node);
+
+    void gotoNode(const SgfNode* node);
+
+signals:
+    /** Loaded Blokus SGF file has invalid syntax.
+        Triggered when a loaded SGF file causes a problem later than at load
+        time (e.g. invalid move property value in a side variation). The
+        reason can be retrieved with in getError().*/
+    void invalidSgfFile();
+
+    /** 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();
+
+    void altPlayerChanged();
+
+    void fileChanged();
+
+    void points0Changed();
+
+    void points1Changed();
+
+    void points2Changed();
+
+    void points3Changed();
+
+    void bonus0Changed();
+
+    void bonus1Changed();
+
+    void bonus2Changed();
+
+    void bonus3Changed();
+
+    void hasEarlierVarChanged();
+
+    void hasMoves0Changed();
+
+    void hasMoves1Changed();
+
+    void hasMoves2Changed();
+
+    void hasMoves3Changed();
+
+    void hasVariationsChanged();
+
+    void isBoardEmptyChanged();
+
+    void isGameOverChanged();
+
+    void isModifiedChanged();
+
+    void isMainVarChanged();
+
+    void canUndoChanged();
+
+    void canGoBackwardChanged();
+
+    void canGoForwardChanged();
+
+    void hasPrevVarChanged();
+
+    void hasNextVarChanged();
+
+    void gameVariantChanged();
+
+    void positionInfoChanged();
+
+    void positionInfoShortChanged();
+
+    void commentChanged();
+
+    void moveNumberChanged();
+
+    void movesLeftChanged();
+
+    void nuColorsChanged();
+
+    void nuPlayersChanged();
+
+    void recentFilesChanged();
+
+    void startingPoints0Changed();
+
+    void startingPoints1Changed();
+
+    void startingPoints2Changed();
+
+    void startingPoints3Changed();
+
+    void startingPointsAnyChanged();
+
+    void playerName0Changed();
+
+    void playerName1Changed();
+
+    void playerName2Changed();
+
+    void playerName3Changed();
+
+    void dateChanged();
+
+    void showVariationsChanged();
+
+    void timeChanged();
+
+    void eventChanged();
+
+    void roundChanged();
+
+private:
+    Game m_game;
+
+    QString m_gameVariant;
+
+    QString m_positionInfo;
+
+    QString m_positionInfoShort;
+
+    QString m_comment;
+
+    QString m_error;
+
+    QString m_file;
+
+    QString m_playerName0;
+
+    QString m_playerName1;
+
+    QString m_playerName2;
+
+    QString m_playerName3;
+
+    QString m_date;
+
+    QString m_time;
+
+    QString m_event;
+
+    QString m_round;
+
+    QStringList m_recentFiles;
+
+    QDateTime m_fileDate;
+
+    QDateTime m_autosaveDate;
+
+    unsigned m_nuColors;
+
+    unsigned m_nuPlayers;
+
+    unsigned m_toPlay = 0;
+
+    unsigned m_altPlayer = 0;
+
+    int m_moveNumber = 0;
+
+    int m_movesLeft = 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_isBoardEmpty = true;
+
+    bool m_isGameOver = false;
+
+    bool m_isModified = false;
+
+    bool m_canUndo = false;
+
+    bool m_canGoForward = false;
+
+    bool m_canGoBackward = false;
+
+    bool m_hasEarlierVar = false;
+
+    bool m_hasPrevVar = false;
+
+    bool m_hasNextVar = false;
+
+    bool m_isMainVar = true;
+
+    bool m_showVariations = true;
+
+    ColorMap<QVariantList> m_pieceModels;
+
+    PieceModel* m_lastMovePieceModel = nullptr;
+
+    QVariantList m_startingPoints0;
+
+    QVariantList m_startingPoints1;
+
+    QVariantList m_startingPoints2;
+
+    QVariantList m_startingPoints3;
+
+    QVariantList m_startingPointsAny;
+
+    QVariantList m_tmpPoints;
+
+    QTextCodec* m_textCodec;
+
+    unique_ptr<MoveList> m_legalMoves;
+
+    unsigned m_legalMoveIndex;
+
+    /** Local variable reused for efficiency. */
+    unique_ptr<MoveMarker> m_marker;
+
+
+    void addRecentFile(const QString& file);
+
+    bool checkSetupAllowed() const;
+
+    void clearFile();
+
+    void createPieceModels();
+
+    void createPieceModels(Color c);
+
+    QString decode(const string& s) const;
+
+    QByteArray encode(const QString& s) const;
+
+    bool findMove(const PieceModel& pieceModel, const QString& state,
+                  QPointF coord, Move& mv) const;
+
+    PieceModel* findUnplayedPieceModel(Color c, Piece piece);
+
+    QString getMoveAnnotationAtNode(const SgfNode& node) const;
+
+    ColorMove getMoveAt(const QPoint& pos) const;
+
+    void initGame(Variant variant);
+
+    void initGameVariant(Variant variant);
+
+    void loadRecentFiles();
+
+    bool openStream(istream& in);
+
+    void prepareFindMove();
+
+    void preparePieceGameCoord(PieceModel* pieceModel, Move mv);
+
+    void preparePieceTransform(PieceModel* pieceModel, Move mv);
+
+    void preparePositionChange();
+
+    void restoreAutoSaveLocation();
+
+    template<typename T>
+    bool set(T& target, const T& value, void (GameModel::*changedSignal)());
+
+    void setFile(const QString& file);
+
+    void setSetupPlayer();
+
+    void setUtf8();
+
+    void updateFileInfo(const QString& file);
+
+    void updateGameInfo();
+
+    void updateIsModified();
+
+    PieceModel* updatePiece(Color c, Move mv,
+                            array<bool, Board::max_pieces>& isPlayed);
+
+    void updatePieces();
+
+    void updatePositionInfo();
+
+    void updateProperties();
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_GAME_MODEL_H
diff --git a/src/pentobi/ImageProvider.cpp b/src/pentobi/ImageProvider.cpp
new file mode 100644 (file)
index 0000000..74e85d3
--- /dev/null
@@ -0,0 +1,89 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/ImageProvider.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "ImageProvider.h"
+
+#include <QPainter>
+#include "libpentobi_paint/Paint.h"
+
+using namespace std;
+using namespace libpentobi_paint;
+
+//-----------------------------------------------------------------------------
+
+ImageProvider::ImageProvider()
+    : QQuickImageProvider(QQuickImageProvider::Pixmap)
+{ }
+
+QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size,
+                                     const QSize& requestedSize)
+{
+    // Piece element images are always created with a user-defined sourceSize,
+    // requestedSize can only become 0 temporarily when changing the game
+    // variant (when scaleUnplayed of a piece becomes 0). In this case, we
+    // return a 1x1 pixmap (0x0 would cause a QQuickImageProvider warning).
+    int width = max(requestedSize.width(), 1);
+    int height = max(requestedSize.height(), 1);
+    *size = QSize(width, height);
+    QPixmap pixmap(width, height);
+    if (requestedSize.width() == 0 || requestedSize.height() == 0)
+        return pixmap;
+    pixmap.fill(Qt::transparent);
+    QPainter painter(&pixmap);
+    painter.setRenderHint(QPainter::Antialiasing);
+    auto splitRef = id.splitRef(QStringLiteral("/"));
+    if (splitRef.empty())
+        return pixmap;
+    auto name = splitRef[0];
+    if (name == "board" && splitRef.size() == 8)
+    {
+        auto gameVariant = splitRef[1].toLocal8Bit();
+        QColor base(splitRef[2]);
+        QColor dark(splitRef[3]);
+        QColor light(splitRef[4]);
+        QColor centerBase(splitRef[5]);
+        QColor centerDark(splitRef[6]);
+        QColor centerLight(splitRef[7]);
+        Variant variant;
+        if (parse_variant_id(gameVariant.constData(), variant))
+            paintBoard(painter, width, height, variant, base, light, dark,
+                       centerBase, centerLight, centerDark);
+    }
+    else if (splitRef.size() == 2)
+    {
+        QColor base(splitRef[1]);
+        if (name == "junction-all")
+            paintJunctionAll(painter, 0, 0, width, height, base);
+        else if (name == "junction-right")
+            paintJunctionRight(painter, 0, 0, width, height, base);
+        else if (name == "junction-straight")
+            paintJunctionStraight(painter, 0, 0, width, height, base);
+        else if (name == "junction-t")
+            paintJunctionT(painter, 0, 0, width, height, base);
+    }
+    else if (splitRef.size() == 4)
+    {
+        QColor base(splitRef[1]);
+        QColor dark(splitRef[2]);
+        QColor light(splitRef[3]);
+        if (name == "frame")
+            paintCallistoOnePiece(painter, 0, 0, width, height, base, light,
+                                  dark);
+        else if (name == "quarter-square")
+            paintQuarterSquare(painter, 0, 0, width, height, base, light);
+        else if (name == "quarter-square-bottom")
+            paintQuarterSquare(painter, 0, 0, width, height, base, dark);
+        else if (name == "square")
+            paintSquare(painter, 0, 0, width, height, base, light, dark);
+        else if (name == "triangle")
+            paintTriangleUp(painter, 0, 0, width, height, base, light, dark);
+        else if (name == "triangle-down")
+            paintTriangleDown(painter, 0, 0, width, height, base, light, dark);
+    }
+    return pixmap;
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/pentobi/ImageProvider.h b/src/pentobi/ImageProvider.h
new file mode 100644 (file)
index 0000000..c76b377
--- /dev/null
@@ -0,0 +1,26 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/ImageProvider.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_IMAGE_PROVIDER_H
+#define PENTOBI_IMAGE_PROVIDER_H
+
+#include <QQuickImageProvider>
+
+//-----------------------------------------------------------------------------
+
+class ImageProvider
+    : public QQuickImageProvider
+{
+public:
+    ImageProvider();
+
+    QPixmap requestPixmap(const QString& id, QSize* size,
+                          const QSize& requestedSize) override;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_IMAGE_PROVIDER_H
diff --git a/src/pentobi/Main.cpp b/src/pentobi/Main.cpp
new file mode 100644 (file)
index 0000000..a9429cd
--- /dev/null
@@ -0,0 +1,225 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/Main.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include <QApplication>
+#include <QIcon>
+#include <QQuickStyle>
+#include <QtQml>
+#include <QTranslator>
+#include "AnalyzeGameModel.h"
+#include "AndroidUtils.h"
+#include "GameModel.h"
+#include "ImageProvider.h"
+#include "PlayerModel.h"
+#include "RatingModel.h"
+#include "SyncSettings.h"
+#include "libboardgame_util/Log.h"
+
+#ifndef Q_OS_ANDROID
+#include <QCommandLineParser>
+#endif
+
+#ifndef PENTOBI_OPEN_HELP_EXTERNALLY
+#include <QtWebView>
+#endif
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+#ifdef Q_OS_ANDROID
+
+int mainAndroid()
+{
+    QQmlApplicationEngine engine;
+    engine.addImageProvider(QStringLiteral("pentobi"), new ImageProvider);
+    auto ctx = engine.rootContext();
+    ctx->setContextProperty(QStringLiteral("initialFile"), QString());
+    ctx->setContextProperty(QStringLiteral("isDesktop"), false);
+#ifdef QT_DEBUG
+    ctx->setContextProperty(QStringLiteral("isDebug"), true);
+#else
+    ctx->setContextProperty(QStringLiteral("isDebug"), false);
+#endif
+    ctx->setContextProperty(QStringLiteral("openHelpExternally"), false);
+    engine.load(QStringLiteral("qrc:///qml/Main.qml"));
+    if (engine.rootObjects().empty())
+        return 1;
+    return QGuiApplication::exec();
+}
+
+#else // ! defined(Q_OS_ANDROID)
+
+int mainDesktop()
+{
+    QIcon icon;
+    icon.addFile(QStringLiteral(":/pentobi_icon/pentobi.svg"));
+    icon.addFile(QStringLiteral(":/pentobi_icon/pentobi-16.svg"));
+    icon.addFile(QStringLiteral(":/pentobi_icon/pentobi-32.svg"));
+    icon.addFile(QStringLiteral(":/pentobi_icon/pentobi-64.svg"));
+    QGuiApplication::setWindowIcon(icon);
+    QGuiApplication::setDesktopFileName(
+                QStringLiteral("io.sourceforge.pentobi"));
+    QCommandLineParser parser;
+    auto maxSupportedLevel = Player::max_supported_level;
+    QCommandLineOption optionMaxLevel(
+                QStringLiteral("maxlevel"),
+                QStringLiteral("Set maximum level to <n>."),
+                QStringLiteral("n"),
+                QString::number(PlayerModel::maxLevel));
+    parser.addOption(optionMaxLevel);
+    QCommandLineOption optionNoBook(
+                QStringLiteral("nobook"),
+                QStringLiteral("Do not use opening books."));
+    QCommandLineOption optionMobile(
+                QStringLiteral("mobile"),
+                QStringLiteral("Use layout optimized for smartphones."));
+    parser.addOption(optionMobile);
+    parser.addOption(optionNoBook);
+    QCommandLineOption optionNoDelay(
+                QStringLiteral("nodelay"),
+                QStringLiteral("Do not delay fast computer moves."));
+    parser.addOption(optionNoDelay);
+    QCommandLineOption optionSeed(
+                QStringLiteral("seed"),
+                QStringLiteral("Set random seed to <n>."),
+                QStringLiteral("n"));
+    parser.addOption(optionSeed);
+    QCommandLineOption optionThreads(
+                QStringLiteral("threads"),
+                QStringLiteral("Use <n> threads (0=auto)."),
+                QStringLiteral("n"));
+    parser.addOption(optionThreads);
+#ifndef LIBBOARDGAME_DISABLE_LOG
+    QCommandLineOption optionVerbose(
+                QStringLiteral("verbose"),
+                QStringLiteral("Print logging information to standard error."));
+    parser.addOption(optionVerbose);
+#endif
+    parser.addPositionalArgument(
+                QStringLiteral("file.blksgf"),
+                QStringLiteral("Blokus SGF file to open (optional)."));
+    parser.addHelpOption();
+    parser.process(*QCoreApplication::instance());
+    try
+    {
+#ifndef LIBBOARDGAME_DISABLE_LOG
+        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;
+        auto maxLevel = parser.value(optionMaxLevel).toUInt(&ok);
+        if (! ok || maxLevel < 1 || maxLevel > maxSupportedLevel)
+            throw runtime_error("--maxlevel must be between 1 and "
+                                + libboardgame_util::to_string(maxSupportedLevel));
+        PlayerModel::maxLevel = maxLevel;
+        if (parser.isSet(optionSeed))
+        {
+            auto seed = parser.value(optionSeed).toUInt(&ok);
+            if (! ok)
+                throw runtime_error("--seed must be a positive number");
+            libboardgame_util::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;
+        }
+        bool isDesktop = ! parser.isSet(optionMobile);
+        QString initialFile;
+        auto args = parser.positionalArguments();
+        if (args.size() > 1)
+            throw runtime_error("Too many arguments");
+        if (! args.empty())
+            initialFile = args.at(0);
+        if (QQuickStyle::name().isEmpty() && isDesktop)
+            QQuickStyle::setStyle(QStringLiteral("Fusion"));
+        QQmlApplicationEngine engine;
+        engine.addImageProvider(QStringLiteral("pentobi"), new ImageProvider);
+        auto ctx = engine.rootContext();
+        ctx->setContextProperty(QStringLiteral("initialFile"), initialFile);
+        ctx->setContextProperty(QStringLiteral("isDesktop"), isDesktop);
+#ifdef QT_DEBUG
+        ctx->setContextProperty(QStringLiteral("isDebug"), true);
+#else
+        ctx->setContextProperty(QStringLiteral("isDebug"), false);
+#endif
+#ifdef PENTOBI_HELP_DIR
+        ctx->setContextProperty(QStringLiteral("helpDir"),
+                                QString::fromLocal8Bit(PENTOBI_HELP_DIR));
+#else
+        ctx->setContextProperty(QStringLiteral("helpDir"), QString());
+#endif
+#ifdef PENTOBI_OPEN_HELP_EXTERNALLY
+        ctx->setContextProperty(QStringLiteral("openHelpExternally"), true);
+#else
+        ctx->setContextProperty(QStringLiteral("openHelpExternally"), false);
+#endif
+        engine.load(QStringLiteral("qrc:///qml/Main.qml"));
+        if (engine.rootObjects().empty())
+            return 1;
+        return QGuiApplication::exec();
+    }
+    catch (const exception& e)
+    {
+        LIBBOARDGAME_LOG("Error: ", e.what());
+        return 1;
+    }
+}
+
+#endif // Q_OS_ANDROID
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char *argv[])
+{
+    libboardgame_util::LogInitializer log_initializer;
+#ifdef Q_OS_ANDROID
+    // We don't use HighDpiScaling on low-DPI Android devices because of
+    // QTBUG-69102 and other bugs
+    auto density = AndroidUtils::getDensity();
+    if (density == 0 || density > 1)
+        QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+#else
+    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+#endif
+    QCoreApplication::setOrganizationName(QStringLiteral("Pentobi"));
+    QCoreApplication::setApplicationName(QStringLiteral("Pentobi"));
+#ifdef VERSION
+    QCoreApplication::setApplicationVersion(QStringLiteral(VERSION));
+#endif
+    QGuiApplication app(argc, argv);
+#ifndef PENTOBI_OPEN_HELP_EXTERNALLY
+    QtWebView::initialize();
+#endif
+    qmlRegisterType<AnalyzeGameModel>("pentobi", 1, 0, "AnalyzeGameModel");
+    qmlRegisterType<AndroidUtils>("pentobi", 1, 0, "AndroidUtils");
+    qmlRegisterType<GameModel>("pentobi", 1, 0, "GameModel");
+    qmlRegisterType<PlayerModel>("pentobi", 1, 0, "PlayerModel");
+    qmlRegisterType<RatingModel>("pentobi", 1, 0, "RatingModel");
+    qmlRegisterType<SyncSettings>("pentobi", 1, 0, "SyncSettings");
+    qmlRegisterInterface<AnalyzeGameElement>("AnalyzeGameElement");
+    qmlRegisterInterface<GameMove>("GameModelMove");
+    qmlRegisterInterface<PieceModel>("PieceModel");
+    QTranslator translator;
+    translator.load(":qml/i18n/qml_" + QLocale::system().name());
+    QCoreApplication::installTranslator(&translator);
+#ifdef Q_OS_ANDROID
+    return mainAndroid();
+#else
+    return mainDesktop();
+#endif
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/pentobi/Pentobi.pro b/src/pentobi/Pentobi.pro
new file mode 100644 (file)
index 0000000..2529463
--- /dev/null
@@ -0,0 +1,263 @@
+#############################################################################
+# The preferred way of building Pentobi is using CMake. This project file
+# exists only because building, deploying and debugging for Android is not
+# yet functional for CMake projects in QtCreator.
+#############################################################################
+
+lessThan(QT_MAJOR_VERSION, 5) {
+    error("Qt >=5.11 required")
+}
+equals(QT_MAJOR_VERSION, 5):lessThan(QT_MINOR_VERSION, 11) {
+    error("Qt >=5.11 required")
+}
+
+TEMPLATE = app
+
+QT += concurrent quickcontrols2 svg webview
+android {
+    QT += androidextras
+}
+
+INCLUDEPATH += ..
+CONFIG += c++14 qtquickcompiler
+DEFINES += QT_DEPRECATED_WARNINGS
+DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x051100
+DEFINES += QT_NO_NARROWING_CONVERSIONS_IN_CONNECT
+DEFINES += VERSION=\"\\\"16.2\\\"\"
+android {
+    DEFINES += PENTOBI_LOW_RESOURCES
+    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 += \
+    AnalyzeGameModel.cpp \
+    AndroidUtils.cpp \
+    GameModel.cpp \
+    ImageProvider.cpp \
+    Main.cpp \
+    PieceModel.cpp \
+    PlayerModel.cpp \
+    RatingModel.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/Reader.cpp \
+    ../libboardgame_sgf/SgfError.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/Game.cpp \
+    ../libpentobi_base/GembloQGeometry.cpp \
+    ../libpentobi_base/GembloQTransform.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/PieceTransformsGembloQ.cpp \
+    ../libpentobi_base/PieceTransformsTrigon.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/AnalyzeGame.cpp \
+    ../libpentobi_mcts/History.cpp \
+    ../libpentobi_mcts/LocalPoints.cpp \
+    ../libpentobi_mcts/Player.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 \
+    ../libpentobi_paint/Paint.cpp
+
+RESOURCES += \
+    ../books/pentobi_books.qrc \
+    ../icon/pentobi_icon.qrc \
+    ../../doc/help.qrc \
+    qml/themes/themes.qrc \
+    qml/i18n/translations.qrc \
+    resources.qrc
+
+!android {
+    RESOURCES += \
+        ../icon/pentobi_icon_desktop.qrc \
+        resources_desktop.qrc
+}
+
+HEADERS += \
+    AnalyzeGameModel.h \
+    AndroidUtils.h \
+    GameModel.h \
+    ImageProvider.h \
+    PieceModel.h \
+    PlayerModel.h \
+    RatingModel.h \
+    SyncSettings.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/Reader.h \
+    ../libboardgame_sgf/SgfError.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/CallistoGeometry.h \
+    ../libpentobi_base/Color.h \
+    ../libpentobi_base/ColorMap.h \
+    ../libpentobi_base/ColorMove.h \
+    ../libpentobi_base/Game.h \
+    ../libpentobi_base/GembloQGeometry.h \
+    ../libpentobi_base/GembloQTransform.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/PieceTransformsGembloQ.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/AnalyzeGame.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 \
+    ../libpentobi_paint/Paint.h
+
+lupdate_only {
+SOURCES += \
+    qml/*.qml \
+    qml/*.js
+}
+
+TRANSLATIONS = \
+    qml/i18n/qml_de.ts \
+    qml/i18n/qml_fr.ts \
+    qml/i18n/qml_nb_NO.ts
+
+qtPrepareTool(LRELEASE, lrelease)
+updateqm.input = TRANSLATIONS
+updateqm.output = ${QMAKE_FILE_PATH}/${QMAKE_FILE_BASE}.qm
+updateqm.commands = $$LRELEASE -removeidentical -nounfinished ${QMAKE_FILE_IN} -qm ${QMAKE_FILE_OUT}
+updateqm.CONFIG += no_link target_predeps
+QMAKE_EXTRA_COMPILERS += updateqm
+
+OTHER_FILES += \
+    android/AndroidManifest.xml
+
+ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
diff --git a/src/pentobi/PieceModel.cpp b/src/pentobi/PieceModel.cpp
new file mode 100644 (file)
index 0000000..67519a0
--- /dev/null
@@ -0,0 +1,425 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/PieceModel.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PieceModel.h"
+
+#include "libboardgame_base/RectTransform.h"
+#include "libpentobi_base/Board.h"
+#include "libpentobi_base/GembloQTransform.h"
+#include "libpentobi_base/TrigonTransform.h"
+
+using namespace std;
+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 libboardgame_util::ArrayList;
+using libpentobi_base::BoardType;
+using libpentobi_base::GeometryType;
+using libpentobi_base::PieceInfo;
+using libpentobi_base::PieceSet;
+using libpentobi_base::TransfGembloQIdentity;
+using libpentobi_base::TransfGembloQRot90;
+using libpentobi_base::TransfGembloQRot180;
+using libpentobi_base::TransfGembloQRot270;
+using libpentobi_base::TransfGembloQRefl;
+using libpentobi_base::TransfGembloQRot90Refl;
+using libpentobi_base::TransfGembloQRot180Refl;
+using libpentobi_base::TransfGembloQRot270Refl;
+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;
+
+//-----------------------------------------------------------------------------
+
+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();
+    auto geoType = bd.get_geometry_type();
+    bool isCallisto = (geoType == GeometryType::callisto);
+    bool isGembloQ = (geoType == GeometryType::gembloq);
+    bool isNexos = (geoType == GeometryType::nexos);
+    bool isTrigon = (geoType == GeometryType::trigon);
+    auto& info = bd.get_piece_info(piece);
+    auto& points = info.get_points();
+    m_elements.reserve(static_cast<int>(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, int> 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);
+        }
+    m_center = findCenter(bd, points, true);
+    auto& labelPos = info.get_label_pos();
+    qreal labelX = labelPos.x - m_center.x();
+    qreal labelY = labelPos.y - m_center.y();
+    if (isGembloQ)
+    {
+        if (labelPos.x % 2 != 0)
+            labelX += 1;
+        if (labelPos.y % 2 != 0)
+            labelY += 1;
+    }
+    else if (isTrigon)
+    {
+        labelX += 0.5;
+        if ((labelPos.x % 2 == 0) != (labelPos.y % 2 == 0))
+            // Downward
+            labelY += 1. / 3;
+        else
+            labelY += 2. / 3;
+    }
+    else
+    {
+        labelX += 0.5;
+        labelY += 0.5;
+    }
+    m_labelPos = QPointF(labelX, labelY);
+}
+
+void PieceModel::flipAcrossX()
+{
+    setTransform(m_bd.get_transforms().get_mirrored_vertically(getTransform()));
+}
+
+void PieceModel::flipAcrossY()
+{
+    setTransform(m_bd.get_transforms().get_mirrored_horizontally(getTransform()));
+}
+
+const Transform* PieceModel::getTransform(const QString& state) const
+{
+    // See comment in getTransform() about the mapping between states and
+    // transform classes.
+    auto& transforms = m_bd.get_transforms();
+    auto pieceSet = m_bd.get_piece_set();
+    if (pieceSet == PieceSet::trigon)
+    {
+        if (state == QStringLiteral("rot60"))
+            return transforms.find<TransfTrigonRot60>();
+        if (state == QStringLiteral("rot120"))
+            return transforms.find<TransfTrigonRot120>();
+        if (state == QStringLiteral("rot180"))
+            return transforms.find<TransfTrigonRot180>();
+        if (state == QStringLiteral("rot240"))
+            return transforms.find<TransfTrigonRot240>();
+        if (state == QStringLiteral("rot300"))
+            return transforms.find<TransfTrigonRot300>();
+        if (state == QStringLiteral("flip"))
+            return transforms.find<TransfTrigonReflRot180>();
+        if (state == QStringLiteral("rot60Flip"))
+            return transforms.find<TransfTrigonReflRot120>();
+        if (state == QStringLiteral("rot120Flip"))
+            return transforms.find<TransfTrigonReflRot60>();
+        if (state == QStringLiteral("rot180Flip"))
+            return transforms.find<TransfTrigonRefl>();
+        if (state == QStringLiteral("rot240Flip"))
+            return transforms.find<TransfTrigonReflRot300>();
+        if (state == QStringLiteral("rot300Flip"))
+            return transforms.find<TransfTrigonReflRot240>();
+        return transforms.find<TransfTrigonIdentity>();
+    }
+    if (pieceSet == PieceSet::gembloq)
+    {
+        if (state == QStringLiteral("rot90"))
+            return transforms.find<TransfGembloQRot90>();
+        if (state == QStringLiteral("rot180"))
+            return transforms.find<TransfGembloQRot180>();
+        if (state == QStringLiteral("rot270"))
+            return transforms.find<TransfGembloQRot270>();
+        if (state == QStringLiteral("flip"))
+            return transforms.find<TransfGembloQRot180Refl>();
+        if (state == QStringLiteral("rot90Flip"))
+            return transforms.find<TransfGembloQRot90Refl>();
+        if (state == QStringLiteral("rot180Flip"))
+            return transforms.find<TransfGembloQRefl>();
+        if (state == QStringLiteral("rot270Flip"))
+            return transforms.find<TransfGembloQRot270Refl>();
+        return transforms.find<TransfGembloQIdentity>();
+    }
+    if (state == QStringLiteral("rot90"))
+        return transforms.find<TransfRectRot90>();
+    if (state == QStringLiteral("rot180"))
+        return transforms.find<TransfRectRot180>();
+    if (state == QStringLiteral("rot270"))
+        return transforms.find<TransfRectRot270>();
+    if (state == QStringLiteral("flip"))
+        return transforms.find<TransfRectRot180Refl>();
+    if (state == QStringLiteral("rot90Flip"))
+        return transforms.find<TransfRectRot90Refl>();
+    if (state == QStringLiteral("rot180Flip"))
+        return transforms.find<TransfRectRefl>();
+    if (state == QStringLiteral("rot270Flip"))
+        return transforms.find<TransfRectRot270Refl>();
+    return transforms.find<TransfIdentity>();
+}
+
+QPointF PieceModel::findCenter(const Board& bd, const PiecePoints& points,
+                               bool usePieceInfoPointTypes)
+{
+    auto geoType = bd.get_geometry_type();
+    bool isTrigon = (geoType == GeometryType::trigon);
+    bool isGembloQ = (geoType == GeometryType::gembloq);
+    bool isNexos = (geoType == GeometryType::nexos);
+    bool isOriginDownward = (usePieceInfoPointTypes
+                             && bd.get_board_type() == BoardType::trigon_3);
+    auto& geo = bd.get_geometry();
+    qreal sumX = 0;
+    qreal sumY = 0;
+    qreal n = 0;
+    for (auto& p : points)
+    {
+        auto pointType = geo.get_point_type(p);
+        if (isNexos && pointType == 0)
+            continue;
+        qreal centerX, centerY;
+        if (isTrigon)
+        {
+            bool isDownward = (pointType == (isOriginDownward ? 0 : 1));
+            centerX = 0.5;
+            centerY = isDownward ?
+                        static_cast<qreal>(1) / 3 : static_cast<qreal>(2) / 3;
+        }
+        else if (isGembloQ)
+        {
+            centerX = (pointType == 1 || pointType == 3) ?
+                        static_cast<qreal>(1) / 3 : static_cast<qreal>(2) / 3;
+            centerY = (pointType == 0 || pointType == 3) ?
+                        static_cast<qreal>(1) / 3 : static_cast<qreal>(2) / 3;
+        }
+        else
+        {
+            centerX = 0.5;
+            centerY = 0.5;
+        }
+        sumX += (p.x + centerX);
+        sumY += (p.y + centerY);
+        ++n;
+    }
+    return {sumX / n, sumY / n};
+}
+
+void PieceModel::nextOrientation()
+{
+    setTransform(m_bd.get_piece_info(m_piece).get_next_transform(getTransform()));
+}
+
+void PieceModel::previousOrientation()
+{
+    setTransform(m_bd.get_piece_info(m_piece).get_previous_transform(getTransform()));
+}
+
+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::setDefaultState()
+{
+    if (m_state.isEmpty())
+        return;
+    m_state.clear();
+    emit stateChanged();
+}
+
+void PieceModel::setGameCoord(QPointF gameCoord)
+{
+    if (m_gameCoord == gameCoord)
+        return;
+    m_gameCoord = gameCoord;
+    emit gameCoordChanged();
+}
+
+void PieceModel::setIsLastMove(bool isLastMove)
+{
+    if (m_isLastMove == isLastMove)
+        return;
+    m_isLastMove = isLastMove;
+    emit isLastMoveChanged();
+}
+
+void PieceModel::setIsPlayed(bool isPlayed)
+{
+    if (m_isPlayed == isPlayed)
+        return;
+    m_isPlayed = isPlayed;
+    emit isPlayedChanged();
+}
+
+void PieceModel::setMoveLabel(const QString& moveLabel)
+{
+    if (m_moveLabel == moveLabel)
+        return;
+    m_moveLabel = moveLabel;
+    emit moveLabelChanged();
+}
+
+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.
+    auto pieceSet = m_bd.get_piece_set();
+    if (pieceSet == PieceSet::trigon)
+    {
+        if (dynamic_cast<const TransfTrigonRot60*>(transform))
+            state = QStringLiteral("rot60");
+        else if (dynamic_cast<const TransfTrigonRot120*>(transform))
+            state = QStringLiteral("rot120");
+        else if (dynamic_cast<const TransfTrigonRot180*>(transform))
+            state = QStringLiteral("rot180");
+        else if (dynamic_cast<const TransfTrigonRot240*>(transform))
+            state = QStringLiteral("rot240");
+        else if (dynamic_cast<const TransfTrigonRot300*>(transform))
+            state = QStringLiteral("rot300");
+        else if (dynamic_cast<const TransfTrigonReflRot180*>(transform))
+            state = QStringLiteral("flip");
+        else if (dynamic_cast<const TransfTrigonReflRot120*>(transform))
+            state = QStringLiteral("rot60Flip");
+        else if (dynamic_cast<const TransfTrigonReflRot60*>(transform))
+            state = QStringLiteral("rot120Flip");
+        else if (dynamic_cast<const TransfTrigonRefl*>(transform))
+            state = QStringLiteral("rot180Flip");
+        else if (dynamic_cast<const TransfTrigonReflRot300*>(transform))
+            state = QStringLiteral("rot240Flip");
+        else if (dynamic_cast<const TransfTrigonReflRot240*>(transform))
+            state = QStringLiteral("rot300Flip");
+    }
+    else if (pieceSet == PieceSet::gembloq)
+    {
+        if (dynamic_cast<const TransfGembloQRot90*>(transform))
+            state = QStringLiteral("rot90");
+        else if (dynamic_cast<const TransfGembloQRot180*>(transform))
+            state = QStringLiteral("rot180");
+        else if (dynamic_cast<const TransfGembloQRot270*>(transform))
+            state = QStringLiteral("rot270");
+        else if (dynamic_cast<const TransfGembloQRot180Refl*>(transform))
+            state = QStringLiteral("flip");
+        else if (dynamic_cast<const TransfGembloQRot90Refl*>(transform))
+            state = QStringLiteral("rot90Flip");
+        else if (dynamic_cast<const TransfGembloQRefl*>(transform))
+            state = QStringLiteral("rot180Flip");
+        else if (dynamic_cast<const TransfGembloQRot270Refl*>(transform))
+            state = QStringLiteral("rot270Flip");
+    }
+    else
+    {
+        if (dynamic_cast<const TransfRectRot90*>(transform))
+            state = QStringLiteral("rot90");
+        else if (dynamic_cast<const TransfRectRot180*>(transform))
+            state = QStringLiteral("rot180");
+        else if (dynamic_cast<const TransfRectRot270*>(transform))
+            state = QStringLiteral("rot270");
+        else if (dynamic_cast<const TransfRectRot180Refl*>(transform))
+            state = QStringLiteral("flip");
+        else if (dynamic_cast<const TransfRectRot90Refl*>(transform))
+            state = QStringLiteral("rot90Flip");
+        else if (dynamic_cast<const TransfRectRefl*>(transform))
+            state = QStringLiteral("rot180Flip");
+        else if (dynamic_cast<const TransfRectRot270Refl*>(transform))
+            state = QStringLiteral("rot270Flip");
+    }
+    if (m_state == state)
+        return;
+    m_state = state;
+    emit stateChanged();
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/pentobi/PieceModel.h b/src/pentobi/PieceModel.h
new file mode 100644 (file)
index 0000000..0a2064b
--- /dev/null
@@ -0,0 +1,149 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/PieceModel.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_PIECE_MODEL_H
+#define PENTOBI_PIECE_MODEL_H
+
+#include <QObject>
+#include <QPointF>
+#include <QVariant>
+#include <QVector>
+#include "libpentobi_base/Color.h"
+#include "libpentobi_base/Piece.h"
+#include "libpentobi_base/PieceInfo.h"
+
+namespace libboardgame_base { class Transform; }
+namespace libpentobi_base { class Board; }
+
+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)
+
+    /** List of QPointF instances with coordinates of piece elements. */
+    Q_PROPERTY(QVariantList elements MEMBER m_elements CONSTANT)
+
+    /** List of QPointF instances with coordinates of piece junctions.
+        Only used in Nexos. */
+    Q_PROPERTY(QVariantList junctions MEMBER m_junctions CONSTANT)
+
+    /** 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. */
+    Q_PROPERTY(QVariantList junctionType MEMBER m_junctionType CONSTANT)
+
+    Q_PROPERTY(QPointF center MEMBER m_center CONSTANT)
+
+    /** Position of the label in board coordinates relative to center. */
+    Q_PROPERTY(QPointF labelPos MEMBER m_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(QString moveLabel READ moveLabel NOTIFY moveLabelChanged)
+    Q_PROPERTY(QPointF gameCoord READ gameCoord NOTIFY gameCoordChanged)
+
+public:
+    static QPointF findCenter(const Board& bd, const PiecePoints& points,
+                              bool usePieceInfoPointTypes);
+
+    PieceModel(QObject* parent, const Board& bd, Piece piece, Color c);
+
+    int color() { return m_color.to_int(); }
+
+    QString state() const { return m_state; }
+
+    bool isPlayed() const { return m_isPlayed; }
+
+    bool isLastMove() const { return m_isLastMove; }
+
+    QString moveLabel() const { return m_moveLabel; }
+
+    QPointF gameCoord() const { return m_gameCoord; }
+
+    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 setMoveLabel(const QString& moveLabel);
+
+    void setGameCoord(QPointF gameCoord);
+
+    Q_INVOKABLE void rotateLeft();
+
+    Q_INVOKABLE void rotateRight();
+
+    Q_INVOKABLE void flipAcrossX();
+
+    Q_INVOKABLE void flipAcrossY();
+
+    Q_INVOKABLE void nextOrientation();
+
+    Q_INVOKABLE void previousOrientation();
+
+signals:
+    void stateChanged();
+
+    void isPlayedChanged();
+
+    void isLastMoveChanged();
+
+    void gameCoordChanged();
+
+    void moveLabelChanged();
+
+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;
+
+    QString m_moveLabel;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_PIECE_MODEL_H
diff --git a/src/pentobi/PlayerModel.cpp b/src/pentobi/PlayerModel.cpp
new file mode 100644 (file)
index 0000000..8ee5580
--- /dev/null
@@ -0,0 +1,182 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/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>
+#include "GameModel.h"
+
+using namespace std;
+using libboardgame_util::clear_abort;
+using libboardgame_util::set_abort;
+
+//-----------------------------------------------------------------------------
+
+bool PlayerModel::noBook = false;
+
+bool PlayerModel::noDelay = false;
+
+unsigned PlayerModel::nuThreads = 0;
+
+#ifdef Q_OS_ANDROID
+unsigned PlayerModel::maxLevel = 7;
+#else
+unsigned PlayerModel::maxLevel = 9;
+#endif
+
+PlayerModel::PlayerModel(QObject* parent)
+    : QObject(parent)
+{
+    try
+    {
+        m_player = make_unique<Player>(GameModel::getInitialGameVariant(),
+                                       maxLevel, "", nuThreads);
+        m_notEnoughMemory = false;
+    }
+    catch (const bad_alloc&)
+    {
+        m_notEnoughMemory = true;
+        return;
+    }
+    if (noBook)
+        m_player->set_use_book(false);
+    m_player->get_search().set_callback(
+                [this](double elapsedSeconds, double remainingSeconds) {
+        emit searchCallback(elapsedSeconds, remainingSeconds);
+    });
+    connect(&m_watcher, &QFutureWatcher<GenMoveResult>::finished,
+            this, &PlayerModel::genMoveFinished);
+}
+
+PlayerModel::~PlayerModel()
+{
+    cancelGenMove();
+}
+
+PlayerModel::GenMoveResult PlayerModel::asyncGenMove(
+        GameModel* gm, Color c, unsigned genMoveId)
+{
+    QElapsedTimer timer;
+    timer.start();
+    auto& bd = gm->getBoard();
+    GenMoveResult result;
+    result.color = c;
+    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(static_cast<unsigned long>(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_watcher.waitForFinished();
+    setIsGenMoveRunning(false);
+}
+
+void PlayerModel::genMoveFinished()
+{
+    auto result = m_watcher.future().result();
+    if (result.genMoveId != m_genMoveId)
+        // Callback from a canceled move generation
+        return;
+    setIsGenMoveRunning(false);
+    auto& bd = result.gameModel->getBoard();
+    ColorMove mv(result.color, result.move);
+    if (mv.is_null())
+    {
+        qWarning("PlayerModel: failed to generate move");
+        return;
+    }
+    if (! bd.is_legal(mv.color, mv.move))
+    {
+        qWarning("PlayerModel: player generated illegal move");
+        return;
+    }
+    emit moveGenerated(new GameMove(this, mv));
+}
+
+void PlayerModel::loadBook(Variant variant)
+{
+    QFile file(QStringLiteral(":/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::setGameVariant(const QString& gameVariant)
+{
+    if (m_gameVariant == gameVariant)
+        return;
+    m_gameVariant = gameVariant;
+    emit gameVariantChanged();
+}
+
+void PlayerModel::setIsGenMoveRunning(bool isGenMoveRunning)
+{
+    if (m_isGenMoveRunning == isGenMoveRunning)
+        return;
+    m_isGenMoveRunning = isGenMoveRunning;
+    emit isGenMoveRunningChanged();
+}
+
+void PlayerModel::setLevel(unsigned level)
+{
+    if (m_level == level)
+        return;
+    m_level = level;
+    emit levelChanged();
+}
+
+void PlayerModel::startGenMove(GameModel* gameModel)
+{
+    auto& bd = gameModel->getBoard();
+    cancelGenMove();
+    auto level = m_level;
+    if (level < 1)
+    {
+        qDebug() << "Invalid level:" << level << "using 1";
+        level = 1;
+    }
+    else if (level > maxLevel)
+    {
+        qDebug() << "Invalid level:" << level << "using" << maxLevel;
+        level = maxLevel;
+    }
+    m_player->set_level(level);
+    auto variant = gameModel->getBoard().get_variant();
+    if (! m_player->is_book_loaded(variant))
+        loadBook(variant);
+    clear_abort();
+    ++m_genMoveId;
+    QFuture<GenMoveResult> future =
+            QtConcurrent::run(this, &PlayerModel::asyncGenMove, gameModel,
+                              bd.get_effective_to_play(), m_genMoveId);
+    m_watcher.setFuture(future);
+    setIsGenMoveRunning(true);
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/pentobi/PlayerModel.h b/src/pentobi/PlayerModel.h
new file mode 100644 (file)
index 0000000..382dbc5
--- /dev/null
@@ -0,0 +1,149 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/PlayerModel.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_PLAYER_MODEL_H
+#define PENTOBI_PLAYER_MODEL_H
+
+#include <QFutureWatcher>
+#include "libpentobi_mcts/Player.h"
+
+class GameModel;
+class GameMove;
+
+using namespace std;
+using libpentobi_base::Color;
+using libpentobi_base::Move;
+using libpentobi_base::Variant;
+using libpentobi_mcts::Player;
+using libpentobi_mcts::Search;
+
+//-----------------------------------------------------------------------------
+
+class PlayerModel
+    : public QObject
+{
+    Q_OBJECT
+
+    /** Game variant should be bound to GameModel.gameVariant.
+        This automatically updates the level property to the stored level for
+        the current game variant. The level will also be updated on
+        startGenMove() but the user interface might want to display the current
+        level immediately after changing the game variant. */
+    Q_PROPERTY(QString gameVariant READ gameVariant WRITE setGameVariant NOTIFY gameVariantChanged)
+
+    Q_PROPERTY(unsigned level READ level WRITE setLevel NOTIFY levelChanged)
+    Q_PROPERTY(bool isGenMoveRunning READ isGenMoveRunning NOTIFY isGenMoveRunningChanged)
+    Q_PROPERTY(unsigned maxLevel MEMBER maxLevel CONSTANT)
+
+public:
+    /** Global variable to disable opening books.
+        Must be set before creating any instances of PlayerModel and not be
+        changed afterwards. */
+    static bool noBook;
+
+    /** Global variable to disable the minimum thinking time.
+        Must be set before creating any instances of PlayerModel and not be
+        changed afterwards. */
+    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. Must be set before creating any instances of PlayerModel and
+        not be changed afterwards. */
+    static unsigned nuThreads;
+
+    /** Global variable to set the maximum level.
+        Must be set before creating any instances of PlayerModel and not be
+        changed afterwards. */
+    static unsigned maxLevel;
+
+
+    explicit PlayerModel(QObject* parent = nullptr);
+
+    ~PlayerModel() override;
+
+
+    /** Check if the player creation failed because of low memory.
+        This is an expected error condition because the player allocates a
+        large amount of memory. This function must be checked after object
+        creation and no functions may be called if it returns true. */
+    Q_INVOKABLE bool notEnoughMemory() const { return m_notEnoughMemory; }
+
+    /** 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);
+
+    /** Cancel the move generation in the background thread if one is
+        running. */
+    Q_INVOKABLE void cancelGenMove();
+
+    const QString& gameVariant() const { return m_gameVariant; }
+
+    void setGameVariant(const QString& gameVariant);
+
+    unsigned level() const { return m_level; }
+
+    void setLevel(unsigned level);
+
+    bool isGenMoveRunning() const { return m_isGenMoveRunning; }
+
+    Search& getSearch() { return m_player->get_search(); }
+
+signals:
+    void gameVariantChanged();
+
+    void levelChanged();
+
+    void isGenMoveRunningChanged();
+
+    void moveGenerated(GameMove* move);
+
+    void searchCallback(double elapsedSeconds, double remainingSeconds);
+
+private:
+    struct GenMoveResult
+    {
+        Color color;
+
+        Move move;
+
+        unsigned genMoveId;
+
+        GameModel* gameModel;
+    };
+
+
+    bool m_notEnoughMemory;
+
+    bool m_isGenMoveRunning = false;
+
+    QString m_gameVariant;
+
+    unsigned m_level = 1;
+
+    unsigned m_genMoveId = 0;
+
+    unique_ptr<Player> m_player;
+
+    QFutureWatcher<GenMoveResult> m_watcher;
+
+
+    GenMoveResult asyncGenMove(GameModel* gm, Color c, unsigned genMoveId);
+
+    void genMoveFinished();
+
+    void loadBook(Variant variant);
+
+    void setIsGenMoveRunning(bool isGenMoveRunning);
+
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_PLAYER_MODEL_H
diff --git a/src/pentobi/RatingModel.cpp b/src/pentobi/RatingModel.cpp
new file mode 100644 (file)
index 0000000..7a8ecb0
--- /dev/null
@@ -0,0 +1,297 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/RatingModel.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "RatingModel.h"
+
+#include <random>
+#include <QDebug>
+#include <QDir>
+#include <QFileInfo>
+#include <QSettings>
+#include <QStandardPaths>
+#include "GameModel.h"
+#include "libpentobi_base/Variant.h"
+#include "libpentobi_mcts/Player.h"
+
+using namespace std;
+using libpentobi_base::Variant;
+using libpentobi_mcts::Player;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+const int maxSavedGames = 50;
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+RatedGameInfo::RatedGameInfo(QObject* parent, int number, int color,
+                             double result, const QString& date, int level,
+                             double rating)
+    : QObject(parent),
+      m_number(number),
+      m_color(color),
+      m_level(level),
+      m_result(result),
+      m_rating(rating),
+      m_date(date)
+{
+}
+
+//-----------------------------------------------------------------------------
+
+void RatingModel::addResult(GameModel* gameModel, int level)
+{
+    Variant variant;
+    if (! parse_variant_id(m_gameVariant.toLocal8Bit().constData(), variant))
+        return;
+    unsigned place;
+    bool isPlaceShared;
+    auto color = getNextHumanPlayer();
+    gameModel->getBoard().get_place(Color(static_cast<Color::IntType>(color)),
+                                    place, isPlaceShared);
+    double gameResult;
+    if (place == 0 && ! isPlaceShared)
+        gameResult = 1;
+    else if (place == 0 && isPlaceShared)
+        gameResult = 0.5;
+    else
+        gameResult = 0;
+    auto nuOpponents = static_cast<unsigned>(get_nu_players(variant) - 1);
+    Rating opponentRating =
+            Player::get_rating(variant, static_cast<unsigned>(level));
+    double kValue = (m_numberGames < 30 ? 40 : 20);
+    Rating rating = m_rating;
+    rating.update(gameResult, opponentRating, kValue, nuOpponents);
+    setRating(rating.get());
+    auto numberGames = m_numberGames + 1;
+    if (numberGames == 1 || rating.get() > m_bestRating.get())
+        setBestRating(rating.get());
+    auto date = QDate::currentDate().toString(QStringLiteral("yyyy-MM-dd"));
+    m_history.prepend(new RatedGameInfo(this, numberGames, color, gameResult,
+                                       date, level, m_rating.get()));
+    auto file = getFile(numberGames);
+    QFileInfo(file).dir().mkpath(QStringLiteral("."));
+    gameModel->save(file);
+    emit historyChanged();
+    setNumberGames(numberGames);
+    saveSettings();
+}
+
+void RatingModel::clearRating()
+{
+    if (! m_history.isEmpty())
+    {
+        m_history.clear();
+        emit historyChanged();
+    }
+    QDir(getDir()).removeRecursively();
+    setRating(1000);
+    setBestRating(1000);
+    setNumberGames(0);
+}
+
+QString RatingModel::getDir() const
+{
+    return QStringLiteral("%1/Rated Games/%2").arg(
+                QStandardPaths::writableLocation(QStandardPaths::AppDataLocation),
+                m_gameVariantName);
+}
+
+QString RatingModel::getFile(int gameNumber) const
+{
+    return QStringLiteral("%1/%2 %3.blksgf").arg(
+                getDir(), m_gameVariantName, QString::number(gameNumber));
+}
+
+int RatingModel::getGameNumberOfFile(const QString& file) const
+{
+    QString left = QStringLiteral("%1/%2 ").arg(getDir(), m_gameVariantName);
+    if (! file.startsWith(left))
+        return 0;
+    QString right = QStringLiteral(".blksgf");
+    if (! file.endsWith(right))
+        return 0;
+    auto leftLen = left.length();
+    auto rightLen = right.length();
+    int n;
+    bool ok;
+    n = QStringRef(&file, leftLen,
+                   file.length() - leftLen - rightLen).toInt(&ok);
+    return ok && n >= 1 && n <= m_numberGames ? n : 0;
+}
+
+int RatingModel::getNextHumanPlayer() const
+{
+    Variant variant;
+    if (! parse_variant_id(m_gameVariant.toLocal8Bit().constData(), variant))
+        return 0;
+    mt19937 generator;
+    generator.discard(static_cast<unsigned>(m_numberGames));
+    uniform_int_distribution<> distribution(0, get_nu_players(variant) - 1);
+    return distribution(generator);
+}
+
+int RatingModel::getNextLevel(int maxLevel) const
+{
+    Variant variant;
+    if (! parse_variant_id(m_gameVariant.toLocal8Bit().constData(), variant))
+        return 1;
+    int level = 1; // Initialize to avoid compiler warning
+    double minDiff = 0; // Initialize to avoid compiler warning
+    for (int i = 1; i <= maxLevel; ++i)
+    {
+        auto diff =
+                abs(m_rating.get()
+                    - Player::get_rating(variant, static_cast<unsigned>(i)).get());
+        if (i == 1 || diff < minDiff)
+        {
+            minDiff = diff;
+            level = i;
+        }
+    }
+    return level;
+}
+
+void RatingModel::saveSettings()
+{
+    QSettings settings(QStringLiteral("%1/%2.ini").arg(getDir(),
+                                                       m_gameVariantName),
+                       QSettings::IniFormat);
+    if (m_numberGames == 0)
+    {
+        settings.remove(QStringLiteral("rated_games"));
+        settings.remove(QStringLiteral("rating"));
+        settings.remove(QStringLiteral("best_rating"));
+    }
+    else
+    {
+        settings.setValue(QStringLiteral("rated_games"), m_numberGames);
+        settings.setValue(QStringLiteral("rating"), m_rating.get());
+        settings.setValue(QStringLiteral("best_rating"),
+                          round(m_bestRating.get()));
+    }
+    QList<QObject*> newHistory;
+    newHistory.reserve(m_history.size());
+    for (auto& i : m_history)
+    {
+        auto& info = dynamic_cast<RatedGameInfo&>(*i);
+        if (info.number() <= m_numberGames - maxSavedGames)
+            QFile::remove(getFile(info.number()));
+        else
+            newHistory.append(&info);
+    }
+    if (newHistory.size() != m_history.size())
+    {
+        m_history = newHistory;
+        emit historyChanged();
+    }
+    settings.remove(QStringLiteral("rated_game_info"));
+    if (m_numberGames > 0)
+    {
+        settings.beginWriteArray(QStringLiteral("rated_game_info"));
+        int n = 0;
+        for (auto& i : m_history)
+        {
+            auto& info = dynamic_cast<RatedGameInfo&>(*i);
+            if (info.number() <= m_numberGames - maxSavedGames)
+                continue;
+            settings.setArrayIndex(n++);
+            settings.setValue(QStringLiteral("number"), info.number());
+            settings.setValue(QStringLiteral("color"), info.color());
+            settings.setValue(QStringLiteral("result"), info.result());
+            settings.setValue(QStringLiteral("date"), info.date());
+            settings.setValue(QStringLiteral("level"), info.level());
+            settings.setValue(QStringLiteral("rating"), round(info.rating()));
+        }
+        settings.endArray();
+    }
+}
+
+void RatingModel::setBestRating(double rating)
+{
+    m_bestRating = Rating(rating);
+    emit bestRatingChanged();
+}
+
+void RatingModel::setGameVariant(const QString& gameVariant)
+{
+    if (m_gameVariant == gameVariant)
+        return;
+    Variant variant;
+    if (! libpentobi_base::parse_variant_id(
+                gameVariant.toLocal8Bit().constData(), variant))
+    {
+        qDebug() << "Invalid game variant" << gameVariant;
+        return;
+    }
+    m_gameVariant = gameVariant;
+    m_gameVariantName =
+            QString::fromLocal8Bit(libpentobi_base::to_string(variant));
+    QSettings settings(QStringLiteral("%1/%2.ini").arg(getDir(),
+                                                       m_gameVariantName),
+                       QSettings::IniFormat);
+    auto currentRating =
+            settings.value(QStringLiteral("rating"), 1000).toDouble();
+    auto bestRating =
+            settings.value(QStringLiteral("best_rating"), 0).toDouble();
+    setRating(currentRating);
+    setBestRating(bestRating);
+    m_history.clear();
+    auto size = settings.beginReadArray(QStringLiteral("rated_game_info"));
+    for (int i = 0; i < size; ++i)
+    {
+        settings.setArrayIndex(i);
+        auto number = settings.value(QStringLiteral("number")).toInt();
+        auto color = settings.value(QStringLiteral("color")).toInt();
+        auto result = settings.value(QStringLiteral("result")).toDouble();
+        auto date = settings.value(QStringLiteral("date")).toString();
+        auto level = settings.value(QStringLiteral("level")).toInt();
+        auto rating = settings.value(QStringLiteral("rating")).toDouble();
+        m_history.append(new RatedGameInfo(this, number, color, result, date,
+                                           level, rating));
+    }
+    settings.endArray();
+    sort(m_history.begin(), m_history.end(),
+         [](const QObject* o1, const QObject* o2)
+         {
+             return dynamic_cast<const RatedGameInfo&>(*o1).number()
+                     > dynamic_cast<const RatedGameInfo&>(*o2).number();
+         });
+    emit historyChanged();
+    setNumberGames(settings.value(QStringLiteral("rated_games"), 0).toInt());
+    emit gameVariantChanged();
+}
+
+void RatingModel::setInitialRating(double rating)
+{
+    setRating(rating);
+    setBestRating(rating);
+    saveSettings();
+}
+
+void RatingModel::setNumberGames(int numberGames)
+{
+    if (numberGames < 0)
+    {
+        qWarning("RatingModel: invalid number of games");
+        return;
+    }
+    if (m_numberGames == numberGames)
+        return;
+    m_numberGames = numberGames;
+    emit numberGamesChanged();
+}
+
+void RatingModel::setRating(double rating)
+{
+    m_rating = Rating(rating);
+    emit ratingChanged();
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/pentobi/RatingModel.h b/src/pentobi/RatingModel.h
new file mode 100644 (file)
index 0000000..d06708b
--- /dev/null
@@ -0,0 +1,156 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/RatingModel.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_RATING_MODEL_H
+#define PENTOBI_RATING_MODEL_H
+
+#include <QObject>
+#include "libboardgame_base/Rating.h"
+
+class GameModel;
+
+using libboardgame_base::Rating;
+
+//-----------------------------------------------------------------------------
+
+class RatedGameInfo
+    : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(int number READ number CONSTANT)
+
+    /** 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. */
+    Q_PROPERTY(int color READ color CONSTANT)
+
+    /** Game result.
+        0=Loss, 0.5=tie, 1=win from the viewpoint of the human. */
+    Q_PROPERTY(double result READ result CONSTANT)
+
+    /** Date of the game in "YYYY-MM-DD" format. */
+    Q_PROPERTY(QString date READ date CONSTANT)
+
+    /** The playing level of the computer opponent. */
+    Q_PROPERTY(int level READ level CONSTANT)
+
+    /** The rating of the human after the game. */
+    Q_PROPERTY(double rating READ rating CONSTANT)
+
+public:
+    RatedGameInfo(QObject* parent, int number, int color, double result,
+                  const QString& date, int level, double rating);
+
+    int number() const { return m_number; }
+
+    int color() const { return m_color; }
+
+    double result() const { return m_result; }
+
+    const QString& date() const { return m_date; }
+
+    int level() const { return m_level; }
+
+    double rating() const { return m_rating; }
+
+private:
+    int m_number;
+
+    int m_color;
+
+    int m_level;
+
+    double m_result;
+
+    double m_rating;
+
+    QString m_date;
+};
+
+//-----------------------------------------------------------------------------
+
+class RatingModel
+    : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(double bestRating READ bestRating NOTIFY bestRatingChanged)
+    Q_PROPERTY(QString gameVariant MEMBER m_gameVariant WRITE setGameVariant NOTIFY gameVariantChanged)
+    Q_PROPERTY(QList<QObject*> history READ history NOTIFY historyChanged)
+    Q_PROPERTY(int numberGames READ numberGames NOTIFY numberGamesChanged)
+    Q_PROPERTY(double rating READ rating NOTIFY ratingChanged)
+
+public:
+    using QObject::QObject;
+
+
+    Q_INVOKABLE void addResult(GameModel* gameModel, int level);
+
+    Q_INVOKABLE void clearRating();
+
+    Q_INVOKABLE int getNextHumanPlayer() const;
+
+    Q_INVOKABLE int getNextLevel(int maxLevel) const;
+
+    Q_INVOKABLE void setInitialRating(double rating);
+
+    Q_INVOKABLE QString getFile(int gameNumber) const;
+
+    /** Get the game number corresponding to a file.
+        @return The game number or 0 if file is not a rated game. */
+    Q_INVOKABLE int getGameNumberOfFile(const QString& file) const;
+
+
+    double bestRating() const { return m_bestRating.get(); }
+
+    const QList<QObject*>& history() const { return m_history; }
+
+    int numberGames() const { return m_numberGames; }
+
+    double rating() const { return m_rating.get(); }
+
+    void setGameVariant(const QString& gameVariant);
+
+signals:
+    void bestRatingChanged();
+
+    void gameVariantChanged();
+
+    void historyChanged();
+
+    void numberGamesChanged();
+
+    void ratingChanged();
+
+private:
+    int m_numberGames = 0;
+
+    Rating m_bestRating = Rating(1000.);
+
+    Rating m_rating = Rating(1000.);
+
+    QString m_gameVariant;
+
+    QString m_gameVariantName;
+
+    QList<QObject*> m_history;
+
+
+    QString getDir() const;
+
+    void saveSettings();
+
+    void setBestRating(double rating);
+
+    void setRating(double rating);
+
+    void setNumberGames(int numberGames);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_RATING_MODEL_H
diff --git a/src/pentobi/SyncSettings.h b/src/pentobi/SyncSettings.h
new file mode 100644 (file)
index 0000000..d18240d
--- /dev/null
@@ -0,0 +1,48 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/SyncSettings.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_SYNC_SETTINGS_H
+#define PENTOBI_SYNC_SETTINGS_H
+
+#include <QSettings>
+
+//-----------------------------------------------------------------------------
+
+/** Settings with sync function.
+    This item does not use property bindings like Qt.labs.settings.Settings,
+    but get/set methods for getting and setting values at defined times. It
+    also provides a sync() method, which is necessary to ensure that pending
+    changes are written in case the application in killed on Android soon after
+    having been suspended (see QTBUG-70291). */
+class SyncSettings
+    : public QObject
+{
+    Q_OBJECT
+
+public:
+    using QObject::QObject;
+
+    Q_INVOKABLE bool valueBool(const QString& key, bool defaultValue) {
+        return m_settings.value(key, defaultValue).toBool(); }
+
+    Q_INVOKABLE int valueInt(const QString& key, int defaultValue) {
+        return m_settings.value(key, defaultValue).toInt(); }
+
+    Q_INVOKABLE void setValueBool(const QString& key, bool value) {
+        m_settings.setValue(key, value); }
+
+    Q_INVOKABLE void setValueInt(const QString& key, int value) {
+        m_settings.setValue(key, value); }
+
+    Q_INVOKABLE void sync() { m_settings.sync(); }
+
+private:
+    QSettings m_settings;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_SYNC_SETTINGS_H
diff --git a/src/pentobi/android/AndroidManifest.xml b/src/pentobi/android/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..0dd3ca5
--- /dev/null
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<manifest package="net.sf.pentobi" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="16.2" android:versionCode="160020" 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" android:windowSoftInputMode="adjustPan">
+            <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"/>
+            <!-- Splash screen -->
+            <meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splash"/>
+            <!-- extract android style -->
+            <!-- available android:values :
+                * default - In most cases this will be the same as "full", but it can also be something else if needed, e.g., for compatibility reasons
+                * full - useful QWidget & Quick Controls 1 apps
+                * minimal - useful for Quick Controls 2 apps, it is much faster than "full"
+                * none - useful for apps that don't use any of the above Qt modules
+                -->
+            <meta-data android:name="android.app_extract_android_style" android:value="minimal"/>
+            <!-- Background running -->
+            <meta-data android:name="android.app.background_running" android:value="false"/>
+            <!-- Auto screen scale factor -->
+            <meta-data android:name="android.app.auto_screen_scale_factor" android:value="false"/>
+        </activity>
+    </application>
+    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="28"/>
+    <supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <!-- %%INSERT_FEATURES -->
+</manifest>
diff --git a/src/pentobi/android/res/drawable-hdpi/icon.png b/src/pentobi/android/res/drawable-hdpi/icon.png
new file mode 100644 (file)
index 0000000..aa2b8f5
Binary files /dev/null and b/src/pentobi/android/res/drawable-hdpi/icon.png differ
diff --git a/src/pentobi/android/res/drawable-mdpi/icon.png b/src/pentobi/android/res/drawable-mdpi/icon.png
new file mode 100644 (file)
index 0000000..9a93a86
Binary files /dev/null and b/src/pentobi/android/res/drawable-mdpi/icon.png differ
diff --git a/src/pentobi/android/res/drawable/splash.xml b/src/pentobi/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/android/res/values/theme.xml b/src/pentobi/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/android_icons_svg/icon48.svg b/src/pentobi/android_icons_svg/icon48.svg
new file mode 100644 (file)
index 0000000..7c054c0
--- /dev/null
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g transform="translate(30.406 -14.813)">
+  <path opacity=".99" fill="#c00" d="m-24.83 16.813c-1.9807 0-3.5762 1.5955-3.5762 3.5762v7.4238h11v-11h-7.4238z" fill-rule="evenodd"/>
+  <path d="m-28.406 27.813 1-1.0005h9v-9l1.0003-1v11z" fill-opacity=".15686"/>
+  <path fill="#fff" d="m-24.83 16.813c-0.96479 0-1.8347 0.383-2.4766 1h8.9004l1-1h-7.4238zm-2.5762 1.0996c-0.617 0.6419-1 1.5118-1 2.4766v7.4238l1-1v-8.9004z" fill-opacity=".15686"/>
+ </g>
+ <g id="c" transform="matrix(1.1 0 0 1.1 18.5 -6.7992)">
+  <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#edd400"/>
+  <path d="m-15 28 0.9091-0.90942h8.1819v-8.1819l0.90935-0.90926v10z" fill-opacity=".15686"/>
+  <path fill-opacity=".15686" d="m-15 28v-10h10l-0.90902 0.90863h-8.1819v8.1819z" fill="#fff"/>
+ </g>
+ <use xlink:href="#c" transform="translate(1.2222e-8 11)" height="72" width="72" y="0" x="0"/>
+ <use xlink:href="#c" transform="translate(11 22)" height="72" width="72" y="0" x="0"/>
+ <g id="b" transform="matrix(1.1 0 0 1.1 29.5 4.2007)">
+  <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#3465a4"/>
+  <path d="m-15 28 0.90918-0.90926h8.1819v-8.1819l0.90926-0.90942v10z" fill-opacity=".15686"/>
+  <path fill-opacity=".15686" d="m-15 28v-10h10l-0.90893 0.90879h-8.1819v8.1819z" fill="#fff"/>
+ </g>
+ <use xlink:href="#b" transform="translate(11 11)" height="72" width="72" y="0" x="0"/>
+ <use xlink:href="#b" transform="translate(11)" height="72" width="72" y="0" x="0"/>
+ <g id="a" transform="matrix(1.1 0 0 1.1 40.5 -17.799)">
+  <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#73d216"/>
+  <path d="m-15 28 0.90926-0.90958h8.1819v-8.1819l0.90918-0.90911v10z" fill-opacity=".15686"/>
+  <path fill-opacity=".15686" d="m-15 28v-10h10l-0.90885 0.90848h-8.1819v8.1819z" fill="#fff"/>
+ </g>
+ <use xlink:href="#a" transform="translate(11 11)" height="72" width="72" y="0" x="0"/>
+ <use xlink:href="#a" transform="translate(11 22)" height="72" width="72" y="0" x="0"/>
+ <g id="e" transform="matrix(1.1 0 0 1.0999 29.499 -17.798)">
+  <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#c00"/>
+  <path d="m-15 28 0.9091-0.90958h8.1819v-8.1819l0.90935-0.90911v10z" fill-opacity=".15686"/>
+  <path fill="#fff" d="m-15 28v-10h10l-0.90902 0.90848h-8.1819v8.1819z" fill-opacity=".15686"/>
+ </g>
+ <g transform="translate(39.928 .74318)">
+  <g transform="translate(-9.7734)">
+   <path opacity=".99" fill="#edd400" d="m-28.154 34.257v7.4238c-0.000001 1.9807 1.5955 3.5762 3.5762 3.5762h7.4238v-11h-11z" fill-rule="evenodd"/>
+   <path d="m-17.154 34.257-1 1v9h-8.9004c0.6419 0.617 1.5118 1 2.4766 1h7.4238v-11z" fill-opacity=".15686"/>
+  </g>
+  <path fill="#fff" d="m-37.928 34.257v7.4258c0 0.96478 0.38301 1.8327 1 2.4746v-8.9004h9l1-1h-11z" fill-opacity=".15686"/>
+ </g>
+ <g transform="translate(74.315 17.385)">
+  <path opacity=".99" fill="#3465a4" d="m-39.314 17.615v11h7.4238c1.974 0 3.5635-1.5833 3.5742-3.5547v-7.4453h-10.998z" fill-rule="evenodd"/>
+  <path d="m-28.315 17.615-1 1v8.9004c0.61705-0.64191 1-1.5117 1-2.4766v-7.4238zm-10 10-1 1h7.4258c0.96473 0 1.8327-0.38306 2.4746-1h-8.9004z" fill-opacity=".15686"/>
+  <path fill="#fff" d="m-39.315 17.615v11l1-1v-9h9l1-1h-11z" fill-opacity=".15686"/>
+ </g>
+ <g transform="translate(60.33 -21.321)">
+  <g transform="translate(25.231 1.9445)">
+   <path opacity=".99" fill="#73d216" d="m-50.561 21.376v11h11v-7.4238c0-1.9807-1.5955-3.5762-3.5762-3.5762h-7.4238z" fill-rule="evenodd"/>
+   <path fill="#fff" d="m-50.561 21.376v11l1-1v-9h8.9004c-0.64189-0.61694-1.5099-1-2.4746-1h-7.4258z" fill-opacity=".15686"/>
+  </g>
+  <path d="m-15.33 24.42v8.9004h-9l-1 1h11v-7.4238c0-0.96479-0.383-1.8347-1-2.4766z" fill-opacity=".15686"/>
+ </g>
+ <use id="d" xlink:href="#e" transform="translate(11.001 11.001)" height="100%" width="100%" y="0" x="0"/>
+ <use xlink:href="#d" transform="translate(-11 .00072815)" height="100%" width="100%" y="0" x="0"/>
+</svg>
diff --git a/src/pentobi/android_icons_svg/icon72.svg b/src/pentobi/android_icons_svg/icon72.svg
new file mode 100644 (file)
index 0000000..2463d0f
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="72" width="72" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g transform="translate(47.16 -34.465)">
+  <path opacity=".99" fill="#c00" d="m-38.16 36.465c-3.878 0-7 3.122-7 7v10h17v-17h-10z" fill-rule="evenodd"/>
+  <path d="m-28.16 36.465-2 2v13h-13l-2 2h17v-17z" fill-opacity=".15686"/>
+  <path fill="#fff" d="m-38.16 36.465c-1.915 0-3.6449 0.76228-4.9062 2h12.906l2-2h-10zm-5 2.0938c-1.2377 1.2614-2 2.9912-2 4.9062v10l2-2v-12.906z" fill-opacity=".15686"/>
+ </g>
+ <g id="c" transform="matrix(1.7 0 0 1.7 27.5 -11.6)">
+  <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#edd400"/>
+  <path d="m-15 28 1.1765-1.1764h7.6471v-7.6471l1.1765-1.1764v10z" fill-opacity=".15686"/>
+  <path fill-opacity=".15686" d="m-15 28v-10h10l-1.1764 1.1765h-7.6471v7.6471z" fill="#fff"/>
+ </g>
+ <use xlink:href="#c" transform="translate(1.8889e-8 17)" height="72" width="72" y="0" x="0"/>
+ <use xlink:href="#c" transform="translate(17 34)" height="72" width="72" y="0" x="0"/>
+ <g id="b" transform="matrix(1.7 0 0 1.7 44.5 5.4)">
+  <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#3465a4"/>
+  <path d="m-15 28 1.1765-1.1764h7.6471v-7.6471l1.1765-1.1764v10z" fill-opacity=".15686"/>
+  <path fill-opacity=".15686" d="m-15 28v-10h10l-1.1764 1.1765h-7.6471v7.6471z" fill="#fff"/>
+ </g>
+ <use xlink:href="#b" transform="translate(17 17)" height="72" width="72" y="0" x="0"/>
+ <use xlink:href="#b" transform="translate(17 4.0042e-7)" height="72" width="72" y="0" x="0"/>
+ <g id="a" transform="matrix(1.7 0 0 1.7 61.5 -28.6)">
+  <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#73d216"/>
+  <path d="m-15 28 1.1765-1.1764h7.6471v-7.6471l1.1765-1.1764v10z" fill-opacity=".15686"/>
+  <path fill-opacity=".15686" d="m-15 28v-10h10l-1.1764 1.1765h-7.6471v7.6471z" fill="#fff"/>
+ </g>
+ <use xlink:href="#a" transform="translate(17 17)" height="72" width="72" y="0" x="0"/>
+ <use xlink:href="#a" transform="translate(17 34)" height="72" width="72" y="0" x="0"/>
+ <g id="e" transform="matrix(1.7 0 0 1.7 44.5 -28.6)">
+  <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#c00"/>
+  <path d="m-15 28 1.1765-1.1764h7.6471v-7.6471l1.1765-1.1764v10z" fill-opacity=".15686"/>
+  <path fill="#fff" d="m-15 28v-10h10l-1.1764 1.1765h-7.6471v7.6471z" fill-opacity=".15686"/>
+ </g>
+ <g transform="translate(45.776 37.573)">
+  <path opacity=".99" fill="#edd400" d="m-43.776 15.427v10c0 3.878 3.122 7 7 7h10v-17h-17z" fill-rule="evenodd"/>
+  <path d="m-26.776 15.427-2 2v13h-12.904c1.2614 1.2377 2.9893 2 4.9043 2h10v-17z" fill-opacity=".15686"/>
+  <path fill="#fff" d="m-43.776 15.427v10c0 1.9141 0.76336 3.6431 2 4.9043v-12.904h13l2-2h-17z" fill-opacity=".15686"/>
+ </g>
+ <g transform="translate(96.877 35.451)">
+  <path opacity=".99" fill="#3465a4" d="m-43.877 17.549v17h10c3.878 0 7-3.122 7-7v-10h-17z" fill-rule="evenodd"/>
+  <path d="m-26.877 17.549-2 2v12.904c1.2366-1.2612 2-2.9903 2-4.9043v-10zm-15 15-2 2h10c1.915 0 3.6429-0.76229 4.9043-2h-12.904z" fill-opacity=".15686"/>
+  <path fill="#fff" d="m-43.877 17.549v17l2-2v-13h13l2-2h-17z" fill-opacity=".15686"/>
+ </g>
+ <g transform="translate(119.89 -16.717)">
+  <path opacity=".99" fill="#73d216" d="m-66.889 18.717v17h17v-10c0-3.878-3.122-7-7-7h-10z" fill-rule="evenodd"/>
+  <path d="m-51.889 20.814v12.902h-13l-2 2h17v-10c0-1.914-0.76342-3.6411-2-4.9023z" fill-opacity=".15686"/>
+  <path fill="#fff" d="m-66.889 18.717v17l2-2v-13h12.904c-1.2614-1.2377-2.9893-2-4.9043-2h-10z" fill-opacity=".15686"/>
+ </g>
+ <use id="d" xlink:href="#e" transform="translate(17 17)" height="100%" width="100%" y="0" x="0"/>
+ <use xlink:href="#d" transform="translate(-17,-7e-5)" height="100%" width="100%" y="0" x="0"/>
+</svg>
diff --git a/src/pentobi/qml/AboutDialog.qml b/src/pentobi/qml/AboutDialog.qml
new file mode 100644 (file)
index 0000000..d794233
--- /dev/null
@@ -0,0 +1,71 @@
+import QtQuick 2.11
+import QtQuick.Controls 2.2
+import "." as Pentobi
+
+Pentobi.Dialog {
+    id: root
+
+    footer: Pentobi.DialogButtonBox { ButtonClose { } }
+
+    Item {
+        implicitWidth:
+            Math.max(Math.min(column.implicitWidth, maxContentWidth),
+                     minContentWidth)
+        implicitHeight: column.implicitHeight
+
+        Column {
+            id: column
+
+            anchors.fill: parent
+            spacing: 0.5 * font.pixelSize
+            leftPadding: spacing
+            rightPadding: leftPadding
+
+            Image {
+                source: "qrc:pentobi_icon/pentobi-64.svg"
+                anchors.horizontalCenter: parent.horizontalCenter
+            }
+            Label {
+                //: The argument is the application version.
+                text: qsTr("Pentobi %1").arg(Qt.application.version)
+                font {
+                    bold: true
+                    pixelSize: 1.3 * root.font.pixelSize
+                }
+                anchors.horizontalCenter: parent.horizontalCenter
+            }
+            Label {
+                text: qsTr("Computer opponent for the board game Blokus")
+                wrapMode: Text.Wrap
+                horizontalAlignment: Text.AlignHCenter
+                width: Math.min(implicitWidth, maxContentWidth)
+                anchors.horizontalCenter: parent.horizontalCenter
+            }
+            Label {
+                text: "<a href=\"https://pentobi.sourceforge.io\" style=\"text-decoration:none\">pentobi.sourceforge.io</a>"
+                textFormat: Text.RichText
+                elide: Qt.ElideRight
+                width: Math.min(implicitWidth, maxContentWidth)
+                anchors.horizontalCenter: parent.horizontalCenter
+                onLinkActivated: Qt.openUrlExternally(link)
+
+                MouseArea {
+                    enabled: isDesktop
+                    anchors.fill: parent
+                    hoverEnabled: true
+                    acceptedButtons: Qt.NoButton
+                    cursorShape: containsMouse ? Qt.PointingHandCursor : Qt.ArrowCursor
+                }
+            }
+            Label {
+                text: qsTr("Copyright © 2011–%1 Markus Enzenberger").arg(2019)
+                font.pixelSize: 0.9 * root.font.pixelSize
+                opacity: 0.8
+                wrapMode: Text.Wrap
+                horizontalAlignment: Text.AlignHCenter
+                width: Math.min(implicitWidth, maxContentWidth)
+                anchors.horizontalCenter: parent.horizontalCenter
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/AnalyzeDialog.qml b/src/pentobi/qml/AnalyzeDialog.qml
new file mode 100644 (file)
index 0000000..2db821a
--- /dev/null
@@ -0,0 +1,51 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/AnalyzeDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.2
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Dialog {
+    footer: DialogButtonBoxOkCancel { }
+    onAccepted: {
+        var nuSimulations
+        switch (comboBox.currentIndex) {
+        case 2: nuSimulations = 75000; break
+        case 1: nuSimulations = 15000; break
+        default: nuSimulations = 3000
+        }
+        Logic.analyzeGame(nuSimulations)
+    }
+
+    Item {
+        implicitWidth:
+            Math.max(Math.min(columnLayout.implicitWidth, maxContentWidth),
+                     minContentWidth)
+        implicitHeight: columnLayout.implicitHeight
+
+        ColumnLayout {
+            id: columnLayout
+
+            anchors.fill: parent
+
+            Label {
+                id: label
+
+                Layout.fillWidth: true
+                text: qsTr("Analysis speed:")
+            }
+            ComboBox {
+                id: comboBox
+
+                model: [ qsTr("Fast"), qsTr("Normal"), qsTr("Slow") ]
+                Layout.fillWidth: true
+                Layout.preferredWidth: font.pixelSize * 15
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/AnalyzeGame.qml b/src/pentobi/qml/AnalyzeGame.qml
new file mode 100644 (file)
index 0000000..ff2b153
--- /dev/null
@@ -0,0 +1,100 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/AnalyzeGame.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.0
+
+Item {
+    id: root
+
+    property var elements: analyzeGameModel.elements
+    property var color: [ color0[0], color1[0], color2[0], color3[0] ]
+    property int markMoveNumber: analyzeGameModel.markMoveNumber
+    property QtObject theme
+
+    property real margin
+    // Distance between moves on the x axis
+    property real dist
+
+    onElementsChanged: {
+        analyzeGameModel.markCurrentMove(gameModel)
+        canvas.requestPaint()
+    }
+    onMarkMoveNumberChanged: canvas.requestPaint()
+    onThemeChanged: canvas.requestPaint()
+
+    Canvas {
+        id: canvas
+
+        visible: elements.length > 0 || analyzeGameModel.isRunning
+        anchors.fill: parent
+        antialiasing: true
+        onPaint: {
+            var elements = analyzeGameModel.elements
+            var nuMoves = elements.length
+            var w = width
+            var h = height
+            // Use the whole width unless few moves have been played
+            var nuBins = Math.ceil(Math.max(nuMoves, 50))
+            var d = w / nuBins
+            var ctx = getContext("2d")
+            ctx.fillStyle = theme.colorBackground
+            ctx.fillRect(0, 0, w, h)
+            ctx.strokeStyle = theme.colorCommentBorder
+            ctx.save()
+            ctx.translate(d / 2, d / 2)
+            w -= d
+            h -= d
+            ctx.beginPath()
+            ctx.moveTo(0, 0)
+            ctx.lineTo(w, 0)
+            ctx.moveTo(0, h)
+            ctx.lineTo(w, h)
+            ctx.stroke()
+            ctx.beginPath()
+            ctx.moveTo(0, h / 2)
+            ctx.lineTo(w, h / 2)
+            ctx.stroke()
+            var n = markMoveNumber
+            if (n >= 0 && n <= nuMoves) {
+                ctx.beginPath()
+                ctx.moveTo(n * d, 0)
+                ctx.lineTo(n * d, h)
+                ctx.stroke()
+            }
+            var radius = d / 2
+            var i
+            for (i = 0; i < nuMoves; ++i) {
+                ctx.beginPath()
+                ctx.fillStyle = color[elements[i].moveColor]
+                ctx.arc(i * d, h - elements[i].value * h,
+                        radius, 0, 2 * Math.PI)
+                ctx.fill()
+            }
+            ctx.restore()
+            dist = d
+            margin = d / 2
+        }
+    }
+    Label {
+        visible: elements.length === 0 && ! analyzeGameModel.isRunning
+        anchors.centerIn: parent
+        color: theme.colorText
+        opacity: 0.8
+        text: qsTr("(No analysis)")
+    }
+    MouseArea {
+        anchors.fill: parent
+
+        onClicked: {
+            if (dist === 0 || elements.length === 0
+                    || analyzeGameModel.isRunning)
+                return
+            var moveNumber = Math.round((mouseX - margin) / dist)
+            analyzeGameModel.gotoMove(gameModel, moveNumber)
+        }
+    }
+}
diff --git a/src/pentobi/qml/AppearanceDialog.qml b/src/pentobi/qml/AppearanceDialog.qml
new file mode 100644 (file)
index 0000000..1a2319f
--- /dev/null
@@ -0,0 +1,213 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/AppearanceDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.2
+import "." as Pentobi
+
+Pentobi.Dialog {
+    id: root
+
+    // Mobile layout may not have enough screen space for apply button and the
+    // immovable dialog will cover most of the screen anyway. Note that using
+    // the same ButtonBox for both and setting visible to false for ButtonApply
+    // if not desktop causes a binding loop for implicitWidth in Qt 5.11 and
+    // elided Text on the dialog buttons.
+    property DialogButtonBox footerDesktop: Pentobi.DialogButtonBox {
+        ButtonCancel { }
+        ButtonApply {
+            enabled:
+                checkBoxCoordinates.checked !== gameDisplay.showCoordinates
+                || checkBoxShowVariations.checked !== gameModel.showVariations
+                || checkBoxAnimatePieces.checked !== gameDisplay.enableAnimations
+                || checkBoxMoveNumber.checked !== gameDisplay.showMoveNumber
+                || comboBoxTheme.currentIndex !== currentThemeIndex
+                || comboBoxMoveMarking.currentIndex !== currentMoveMarkingIndex
+                || comboBoxComment.currentIndex !== currentCommentIndex
+        }
+        ButtonOk { }
+    }
+    property DialogButtonBox footerMobile: DialogButtonBoxOkCancel { }
+    property int currentThemeIndex
+    property int currentMoveMarkingIndex
+    property int currentCommentIndex
+
+    footer: isDesktop ? footerDesktop : footerMobile
+    onOpened: {
+        checkBoxCoordinates.checked = gameDisplay.showCoordinates
+        checkBoxShowVariations.checked = gameModel.showVariations
+        checkBoxAnimatePieces.checked = gameDisplay.enableAnimations
+        if (themeName === "dark")
+            currentThemeIndex = 1
+        else if (themeName === "colorblind-light")
+            currentThemeIndex = 2
+        else if (themeName === "colorblind-dark")
+            currentThemeIndex = 3
+        else if (themeName === "system")
+            currentThemeIndex = isAndroid ? 1 : 4
+        else
+            currentThemeIndex = 0
+        comboBoxTheme.currentIndex = currentThemeIndex
+        if (gameDisplay.moveMarking === "last_dot")
+            currentMoveMarkingIndex = 0
+        else if (gameDisplay.moveMarking === "last_number")
+            currentMoveMarkingIndex = 1
+        else if (gameDisplay.moveMarking === "all_number")
+            currentMoveMarkingIndex = 2
+        else if (gameDisplay.moveMarking === "none")
+            currentMoveMarkingIndex = 3
+        else
+            currentMoveMarkingIndex = 0
+        comboBoxMoveMarking.currentIndex = currentMoveMarkingIndex
+        if (isDesktop) {
+            checkBoxMoveNumber.checked = gameDisplay.showMoveNumber
+            if (gameDisplay.commentMode === "always")
+                currentCommentIndex = 0
+            else if (gameDisplay.commentMode === "never")
+                currentCommentIndex = 2
+            else
+                currentCommentIndex = 1
+            comboBoxComment.currentIndex = currentCommentIndex
+        }
+    }
+    onAccepted: {
+        gameDisplay.showCoordinates = checkBoxCoordinates.checked
+        gameModel.showVariations = checkBoxShowVariations.checked
+        gameDisplay.enableAnimations = checkBoxAnimatePieces.checked
+        switch (comboBoxTheme.currentIndex) {
+        case 0: themeName = "light"; break
+        case 1: themeName = "dark"; break
+        case 2: themeName = "colorblind-light"; break
+        case 3: themeName = "colorblind-dark"; break
+        case 4: themeName = "system"; break
+        }
+        switch (comboBoxMoveMarking.currentIndex) {
+        case 0: gameDisplay.moveMarking = "last_dot"; break
+        case 1: gameDisplay.moveMarking = "last_number"; break
+        case 2: gameDisplay.moveMarking = "all_number"; break
+        case 3: gameDisplay.moveMarking = "none"; break
+        }
+        if (isDesktop)
+            gameDisplay.showMoveNumber = checkBoxMoveNumber.checked
+            switch (comboBoxComment.currentIndex) {
+            case 0: gameDisplay.commentMode = "always"; break
+            case 1: gameDisplay.commentMode = "as_needed"; break
+            case 2: gameDisplay.commentMode = "never"; break
+            }
+    }
+    onApplied: {
+        onAccepted()
+        onOpened()
+    }
+
+    Flickable {
+        implicitWidth:
+            Math.max(Math.min(columnLayout.implicitWidth, maxContentWidth),
+                     minContentWidth)
+        implicitHeight: Math.min(columnLayout.implicitHeight, maxContentHeight)
+        contentHeight: columnLayout.implicitHeight
+        clip: true
+
+        ColumnLayout {
+            id: columnLayout
+
+            anchors.fill: parent
+
+            CheckBox {
+                id: checkBoxCoordinates
+
+                text: qsTr("Coordinates")
+            }
+            CheckBox {
+                id: checkBoxShowVariations
+
+                text: qsTr("Show variations")
+            }
+            CheckBox {
+                id: checkBoxMoveNumber
+
+                visible: isDesktop
+                //: Check box in appearance dialog whether to show the
+                //: move number in the status bar.
+                text: qsTr("Move number")
+            }
+            CheckBox {
+                id: checkBoxAnimatePieces
+
+                text: qsTr("Animations")
+            }
+            Label {
+                text: qsTr("Color theme:")
+                Layout.topMargin: 0.5 * font.pixelSize
+
+
+            }
+            ComboBox {
+                id: comboBoxTheme
+
+                model: isAndroid ?
+                           [
+                               qsTr("Light"),
+                               qsTr("Dark"),
+                               qsTr("Colorblind light"),
+                               qsTr("Colorblind dark")
+                           ] :
+                           [
+                               qsTr("Light"),
+                               qsTr("Dark"),
+                               qsTr("Colorblind light"),
+                               qsTr("Colorblind dark"),
+                               //: Name of theme using default system colors
+                               qsTr("System")
+                           ]
+                Layout.preferredWidth: font.pixelSize * 20
+                Layout.fillWidth: true
+            }
+            Label {
+                text: qsTr("Move marking:")
+                Layout.topMargin: 0.5 * font.pixelSize
+
+
+            }
+            ComboBox {
+                id: comboBoxMoveMarking
+
+                model: [
+                    qsTr("Last with dot"),
+                    qsTr("Last with number"),
+                    qsTr("All with number"),
+                    //: Move marking/None
+                    qsTr("None")
+                ]
+                Layout.preferredWidth: font.pixelSize * 20
+                Layout.fillWidth: true
+            }
+            Label {
+                visible: isDesktop
+                text: qsTr("Show comment:")
+                Layout.topMargin: 0.5 * font.pixelSize
+
+
+            }
+            ComboBox {
+                id: comboBoxComment
+
+                visible: isDesktop
+                model: [
+                    //: Show-comment mode
+                    qsTr("Always"),
+                    //: Show-comment mode
+                    qsTr("As needed"),
+                    //: Show-comment mode
+                    qsTr("Never")
+                ]
+                Layout.preferredWidth: font.pixelSize * 20
+                Layout.fillWidth: true
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/AsciiArtSaveDialog.qml b/src/pentobi/qml/AsciiArtSaveDialog.qml
new file mode 100644 (file)
index 0000000..8a909e6
--- /dev/null
@@ -0,0 +1,21 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/AsciiArtSaveDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.FileDialog {
+    title: qsTr("Export ASCII Art")
+    selectExisting: false
+    nameFilterLabels: [ qsTr("Text files") ]
+    nameFilters: [ [ "*.txt", "*.TXT" ] ]
+    folder: rootWindow.folder
+    onAccepted: {
+        rootWindow.folder = folder
+        Logic.exportAsciiArt(fileUrl)
+    }
+}
diff --git a/src/pentobi/qml/Board.qml b/src/pentobi/qml/Board.qml
new file mode 100644 (file)
index 0000000..e380caf
--- /dev/null
@@ -0,0 +1,295 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Board.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Window 2.2
+
+Item {
+    id: root
+
+    property string gameVariant: gameModel.gameVariant
+    property bool showCoordinates
+    property bool isTrigon: gameVariant.startsWith("trigon")
+    property bool isNexos: gameVariant.startsWith("nexos")
+    property bool isCallisto: gameVariant.startsWith("callisto")
+    property bool isGembloQ: gameVariant.startsWith("gembloq")
+    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
+        case "gembloq":
+        case "gembloq_2_4":
+            return 56
+        case "gembloq_2":
+            return 44
+        case "gembloq_3":
+            return 52
+        default:
+            return 20
+        }
+    }
+    property int rows:
+        isTrigon ? (columns + 1) / 2 : isGembloQ ? columns / 2 : columns
+
+    property real gridWidth: {
+        // Avoid fractional piece element sizes if the piece elements are squares
+        var sideLength
+        if (isTrigon) sideLength = Math.min(width, Math.sqrt(3) * height)
+        else sideLength = Math.min(width, height)
+        var n = columns
+        if (showCoordinates) n += (isTrigon ? 3 : 2)
+        if (isTrigon) return sideLength / (n + 1)
+        if (isNexos) return Math.floor(sideLength * Screen.devicePixelRatio / (n - 0.5)) / Screen.devicePixelRatio
+        if (isGembloQ) return Math.floor(2 * sideLength * Screen.devicePixelRatio / n) / 2 / Screen.devicePixelRatio
+        return Math.floor(sideLength * Screen.devicePixelRatio / n) / Screen.devicePixelRatio
+    }
+    property real gridHeight: {
+        if (isTrigon) return Math.sqrt(3) * gridWidth
+        if (isGembloQ) return 2 * gridWidth
+        return gridWidth
+    }
+    property real startingPointSize: {
+        if (isTrigon) return 0.27 * gridHeight
+        if (isGembloQ) return 0.45 * gridHeight
+        return 0.35 * gridHeight
+    }
+    property int coordinateFontSize: {
+        if (isTrigon) return 0.4 * gridHeight
+        if (isGembloQ) return 0.35 * gridHeight
+        return 0.6 * gridHeight
+    }
+    property Item grabImageTarget: grabImageTarget
+
+    signal clicked(point pos)
+    signal rightClicked(point pos)
+
+    function mapFromGameX(x) {
+        if (isTrigon) return image.x + grabImageTarget.x + (x + 0.5) * gridWidth
+        if (isNexos) return image.x + grabImageTarget.x + (x - 0.25) * gridWidth
+        return image.x + grabImageTarget.x + x * gridWidth
+    }
+    function mapFromGameY(y) {
+        if (isNexos) return image.y + grabImageTarget.y + (y - 0.25) * gridHeight
+        return image.y + grabImageTarget.y + y * gridHeight
+    }
+    function mapToGame(pos) {
+        if (isTrigon)
+            return Qt.point((pos.x - grabImageTarget.x - image.x - 0.5 * gridWidth) / gridWidth,
+                            (pos.y - grabImageTarget.y - image.y) / gridHeight)
+        if (isNexos)
+            return Qt.point((pos.x - grabImageTarget.x - image.x + 0.25 * gridWidth) / gridWidth,
+                            (pos.y - grabImageTarget.y - image.y + 0.25 * gridHeight) / gridHeight)
+        return Qt.point((pos.x - grabImageTarget.x - image.x) / gridWidth,
+                        (pos.y - grabImageTarget.y - image.y) / gridHeight)
+    }
+    // Needs all arguments for dependencies
+    function getStartingPointX(x, gridWidth, pointSize) {
+        return mapFromGameX(x) - grabImageTarget.x + (gridWidth - pointSize) / 2
+    }
+    // Needs all arguments for dependencies
+    function getStartingPointY(y, gridHeight, pointSize) {
+        return mapFromGameY(y) - grabImageTarget.y + (gridHeight - pointSize) / 2
+    }
+    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
+    }
+    function getColumnCoord(x) {
+        if (x > 25)
+            return String.fromCharCode("A".charCodeAt(0) + x / 26 - 1)
+                    + String.fromCharCode("A".charCodeAt(0) + (x % 26))
+        return String.fromCharCode("A".charCodeAt(0) + x)
+    }
+
+    Item {
+        id: grabImageTarget
+
+        anchors.centerIn: parent
+        width: {
+            if (! showCoordinates)
+                return image.width
+            if (isTrigon)
+                return image.width + 3 * gridWidth
+            return image.width + 2 * gridWidth
+        }
+        height: {
+            if (! showCoordinates)
+                return image.height
+            return image.height + 2 * gridHeight
+        }
+
+        Image {
+            id: image
+
+            width: {
+                if (isTrigon) return gridWidth * (columns + 1)
+                if (isNexos) return gridWidth * (columns - 0.5)
+                return gridWidth * columns
+            }
+            height: {
+                if (isNexos) return gridHeight * (rows - 0.5)
+                return gridHeight * rows
+            }
+            anchors.centerIn: parent
+            source: width > 0 && height > 0 ?
+                        "image://pentobi/board/" + gameVariant + "/"
+                        + theme.colorBoard[0] + "/" + theme.colorBoard[1] + "/"
+                        + theme.colorBoard[2] + "/" + theme.colorBoard[3] + "/"
+                        + theme.colorBoard[4] + "/" + theme.colorBoard[5] :
+                        ""
+            sourceSize { width: width; height: height }
+            cache: false
+        }
+        Repeater {
+            model: gameModel.startingPoints0
+
+            Rectangle {
+                color: color0[0]
+                width: startingPointSize; height: width
+                radius: width / 2
+                x: getStartingPointX(modelData.x, gridWidth, width)
+                y: getStartingPointY(modelData.y, gridHeight, height)
+            }
+        }
+        Repeater {
+            model: gameModel.startingPoints1
+
+            Rectangle {
+                color: color1[0]
+                width: startingPointSize; height: width
+                radius: width / 2
+                x: getStartingPointX(modelData.x, gridWidth, width)
+                y: getStartingPointY(modelData.y, gridHeight, height)
+            }
+        }
+        Repeater {
+            model: gameModel.startingPoints2
+
+            Rectangle {
+                color: color2[0]
+                width: startingPointSize; height: width
+                radius: width / 2
+                x: getStartingPointX(modelData.x, gridWidth, width)
+                y: getStartingPointY(modelData.y, gridHeight, height)
+            }
+        }
+        Repeater {
+            model: gameModel.startingPoints3
+
+            Rectangle {
+                color: color3[0]
+                width: startingPointSize; height: width
+                radius: width / 2
+                x: getStartingPointX(modelData.x, gridWidth, width)
+                y: getStartingPointY(modelData.y, gridHeight, height)
+            }
+        }
+        Repeater {
+            model: gameModel.startingPointsAny
+
+            Rectangle {
+                color: theme.colorStartingPoint
+                width: startingPointSize; height: width
+                radius: width / 2
+                x: mapFromGameX(modelData.x) - grabImageTarget.x
+                   + (gridWidth - width) / 2
+                y: mapFromGameY(modelData.y) - grabImageTarget.y
+                   + getCenterYTrigon(modelData) - height / 2
+            }
+        }
+        Repeater {
+            model: showCoordinates ? columns : 0
+
+            Text {
+                text: getColumnCoord(index)
+                color: theme.colorText
+                opacity: 0.55 - 0.1 * theme.colorBackground.hslLightness
+                font { pixelSize: coordinateFontSize; preferShaping: false }
+                x: mapFromGameX(index) - grabImageTarget.x
+                   + (gridWidth - width) / 2
+                y: mapFromGameY(-1) - grabImageTarget.y
+                   + (gridHeight - height) / 2
+            }
+        }
+        Repeater {
+            model: showCoordinates ? columns : 0
+
+            Text {
+                text: getColumnCoord(index)
+                color: theme.colorText
+                opacity: 0.55 - 0.1 * theme.colorBackground.hslLightness
+                font { pixelSize: coordinateFontSize; preferShaping: false }
+                x: mapFromGameX(index) - grabImageTarget.x
+                   + (gridWidth - width) / 2
+                y: mapFromGameY(rows) - grabImageTarget.y
+                   + (gridHeight - height) / 2
+            }
+        }
+        Repeater {
+            model: showCoordinates ? rows : 0
+
+            Text {
+                text: index + 1
+                color: theme.colorText
+                opacity: 0.55 - 0.1 * theme.colorBackground.hslLightness
+                font { pixelSize: coordinateFontSize; preferShaping: false }
+                x: mapFromGameX(isTrigon ? -1.5 : -1) - grabImageTarget.x
+                   + (gridWidth - width) / 2
+                y: mapFromGameY(rows - index - 1) - grabImageTarget.y
+                   + (gridHeight - height) / 2
+            }
+        }
+        Repeater {
+            model: showCoordinates ? rows : 0
+
+            Text {
+                text: index + 1
+                color: theme.colorText
+                opacity: 0.55 - 0.1 * theme.colorBackground.hslLightness
+                font { pixelSize: coordinateFontSize; preferShaping: false }
+                x: mapFromGameX(isTrigon ? columns + 0.5 : columns)
+                    - grabImageTarget.x + (gridWidth - width) / 2
+                y: mapFromGameY(rows - index - 1) - grabImageTarget.y
+                   + (gridHeight - height) / 2
+            }
+        }
+        MouseArea {
+            anchors.fill: parent
+            acceptedButtons: Qt.LeftButton | Qt.RightButton
+            onPressAndHold: {
+                var pos = mapToGame(Qt.point(mouseX + grabImageTarget.x,
+                                             mouseY + grabImageTarget.y))
+                pos.x = Math.floor(pos.x)
+                pos.y = Math.floor(pos.y)
+                root.rightClicked(pos)
+            }
+            onClicked: {
+                var pos = mapToGame(Qt.point(mouseX + grabImageTarget.x,
+                                             mouseY + grabImageTarget.y))
+                pos.x = Math.floor(pos.x)
+                pos.y = Math.floor(pos.y)
+                if (mouse.button & Qt.RightButton)
+                    root.rightClicked(pos)
+                else
+                    root.clicked(pos)
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/BoardContextMenu.qml b/src/pentobi/qml/BoardContextMenu.qml
new file mode 100644 (file)
index 0000000..2c72000
--- /dev/null
@@ -0,0 +1,33 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/BoardContextMenu.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "." as Pentobi
+
+Pentobi.Menu {
+    property int moveNumber
+
+    property string _annotation
+
+    dynamicWidth: true
+    onOpened: _annotation = gameModel.getMoveAnnotation(moveNumber)
+
+    Pentobi.MenuItem {
+        enabled: moveNumber !== gameModel.moveNumber && ! isRated
+        text: qsTr("Go to Move %1").arg(moveNumber)
+        onTriggered: gameModel.gotoMove(moveNumber)
+    }
+    Pentobi.MenuItem {
+        text: _annotation === "" ?
+                  qsTr("Move Annotation") :
+                  //: The argument is the annotation symbol for the current move
+                  qsTr("Move Annotation (%1)").arg(_annotation)
+        onTriggered: {
+            var dialog = moveAnnotationDialog.get()
+            dialog.moveNumber = moveNumber
+            moveAnnotationDialog.open()
+        }
+    }
+}
diff --git a/src/pentobi/qml/BusyIndicator.qml b/src/pentobi/qml/BusyIndicator.qml
new file mode 100644 (file)
index 0000000..874bf5f
--- /dev/null
@@ -0,0 +1,61 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/BusyIndicator.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.6
+import QtQuick.Controls 2.1
+
+BusyIndicator {
+    id: root
+
+    contentItem: Item {
+        implicitWidth: 64
+        implicitHeight: 64
+
+        Item {
+            id: item
+
+            anchors.fill: parent
+            opacity: root.running ? 1 : 0
+
+            Behavior on opacity {
+                OpacityAnimator {
+                    duration: enableAnimations ? animationDurationFast : 0
+                }
+            }
+
+            Repeater {
+                id: repeater
+
+                model: 8
+
+                Rectangle {
+                    x: item.width / 2 - width / 2
+                    y: item.height / 2 - height / 2
+                    width: 0.15 * item.width; height: width
+                    radius: width / 2
+                    color: theme.colorText
+                    opacity: 0.5
+                    transform: [
+                        Translate {
+                            y: -item.width / 2 + width
+                        },
+                        Rotation {
+                            angle: index / repeater.count * 360
+                            origin { x: width / 2; y: height / 2 }
+                        }
+                    ]
+                }
+            }
+            RotationAnimator {
+                target: item
+                running: root.visible && root.running
+                from: 0; to: 360
+                loops: Animation.Infinite
+                duration: 1700
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/Button.qml b/src/pentobi/qml/Button.qml
new file mode 100644 (file)
index 0000000..441bb06
--- /dev/null
@@ -0,0 +1,61 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Button.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Window 2.2
+import QtQuick.Controls 2.3
+
+ToolButton {
+    id: root
+
+    // See ButtonToolTip
+    property bool buttonToolTipHovered
+    property bool effectiveHovered:
+        isDesktop && buttonToolTipHovered && enabled
+
+    implicitWidth: Math.min(getIconSize() + (isDesktop ? 14 : 30),
+                            0.11 * rootWindow.contentItem.height,
+                            0.13 * rootWindow.contentItem.width)
+    implicitHeight: implicitWidth
+
+    // We use SVG icon sources of size 16x16 and want the icon about the same
+    // size as the font, but use multipliers in quarter-size steps (4) for
+    // better pixel alignment. Minimum size is 8. Note that on some Android 4.2
+    // devices, Qt 5.11 reports a much too low pixelDensity (e.g.
+    // pixelDensity=4.2, devicePixelRatio=1.5 on a 4.0" 480x800 device) but
+    // uses a reasonable font size, so deriving the size directly from
+    // pixelDensity and/or devicePixelRatio is not a good idea.
+    function getIconSize() {
+        return Math.max(
+                    Math.round(
+                        1.2 * font.pixelSize * Screen.devicePixelRatio / 4)
+                    / Screen.devicePixelRatio * 4,
+                    Screen.devicePixelRatio * 8)
+    }
+
+    Behavior on opacity {
+        NumberAnimation { duration: gameDisplay.animationDurationFast }
+    }
+
+    opacity: root.enabled ? 0.5 : 0.25
+    hoverEnabled: false
+    display: AbstractButton.IconOnly
+    icon {
+        color: theme.colorText
+        width: getIconSize()
+        height: getIconSize()
+    }
+    focusPolicy: Qt.NoFocus
+    flat: true
+    background: Rectangle {
+        radius: 0.05 * width
+        color: down ? theme.colorButtonPressed :
+                      effectiveHovered ? theme.colorButtonHovered
+                                       : "transparent"
+        border.color: down || effectiveHovered ? theme.colorButtonBorder
+                                               : "transparent"
+    }
+}
diff --git a/src/pentobi/qml/ButtonApply.qml b/src/pentobi/qml/ButtonApply.qml
new file mode 100644 (file)
index 0000000..a85d408
--- /dev/null
@@ -0,0 +1,12 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ButtonApply.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.2
+
+Button {
+    text: qsTr("Apply")
+    DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole
+}
diff --git a/src/pentobi/qml/ButtonCancel.qml b/src/pentobi/qml/ButtonCancel.qml
new file mode 100644 (file)
index 0000000..c8fe66c
--- /dev/null
@@ -0,0 +1,12 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ButtonCancel.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.2
+
+Button {
+    text: qsTr("Cancel")
+    DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
+}
diff --git a/src/pentobi/qml/ButtonClose.qml b/src/pentobi/qml/ButtonClose.qml
new file mode 100644 (file)
index 0000000..1003a06
--- /dev/null
@@ -0,0 +1,12 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ButtonClose.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.2
+
+Button {
+    text: qsTr("Close")
+    DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
+}
diff --git a/src/pentobi/qml/ButtonOk.qml b/src/pentobi/qml/ButtonOk.qml
new file mode 100644 (file)
index 0000000..f27018e
--- /dev/null
@@ -0,0 +1,12 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ButtonOk.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.2
+
+Button {
+    text: qsTr("OK")
+    DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
+}
diff --git a/src/pentobi/qml/ButtonToolTip.qml b/src/pentobi/qml/ButtonToolTip.qml
new file mode 100644 (file)
index 0000000..ec7b3f2
--- /dev/null
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ButtonToolTip.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import QtQuick.Controls 2.4
+import "." as Pentobi
+
+// Used instead of attached tooltip because of QTBUG-30801 (tooltip not shown
+// when the button is disabled). Must be declared such that x and y have the
+// same meaning as in the button (e.g. same parent).
+MouseArea {
+    property Pentobi.Button button
+
+    property bool _inhibitAfterPress
+
+    visible: button.visible && isDesktop
+    x: button.x
+    y: button.y
+    width: button.width
+    height: button.height
+    acceptedButtons: Qt.NoButton
+    hoverEnabled: true
+    onExited: _inhibitAfterPress = false
+    ToolTip.visible: containsMouse && ToolTip.text && ! _inhibitAfterPress
+    ToolTip.delay: 1000
+    ToolTip.timeout: 7000
+    Component.onCompleted:
+        button.buttonToolTipHovered = Qt.binding(function() {
+            return containsMouse })
+
+    Connections {
+        target: button
+        onPressed: _inhibitAfterPress = true
+    }
+}
diff --git a/src/pentobi/qml/Comment.qml b/src/pentobi/qml/Comment.qml
new file mode 100644 (file)
index 0000000..4e384f3
--- /dev/null
@@ -0,0 +1,44 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Comment.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+
+ScrollView {
+    function dropFocus() { if (textArea.activeFocus) textArea.focus = false }
+
+    clip: true
+    ScrollBar.vertical.minimumSize: 0.2
+
+    TextArea {
+        id: textArea
+
+        text: gameModel.comment
+        color: theme.colorCommentText
+        selectionColor: theme.colorSelection
+        selectedTextColor: theme.colorSelectedText
+        selectByMouse: isDesktop
+        wrapMode: TextEdit.Wrap
+        focus: true
+        onTextChanged: gameModel.comment = text
+        background: Rectangle {
+            // Qt 5.12.0 alpha doesn't size the background if it is in a
+            // SwipeView like in GameDisplayMobile
+            anchors.fill: parent
+            color: theme.colorCommentBase
+            radius: 2
+            border.color:
+                textArea.activeFocus ? theme.colorCommentFocus
+                                     : theme.colorCommentBorder
+        }
+        Keys.onPressed:
+            if (event.key === Qt.Key_Tab)
+            {
+                focus = false
+                event.accepted = true
+            }
+    }
+}
diff --git a/src/pentobi/qml/ComputerDialog.qml b/src/pentobi/qml/ComputerDialog.qml
new file mode 100644 (file)
index 0000000..c96dc39
--- /dev/null
@@ -0,0 +1,193 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ComputerDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.2
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Dialog {
+    id: root
+
+    footer: DialogButtonBoxOkCancel { }
+    onOpened: {
+        checkBox0.checked = computerPlays0
+        checkBox1.checked = computerPlays1
+        checkBox2.checked = computerPlays2
+        checkBox3.checked = computerPlays3
+        slider.value = playerModel.level
+    }
+    onAccepted: {
+        computerPlays0 = checkBox0.checked
+        computerPlays1 = checkBox1.checked
+        computerPlays2 = checkBox2.checked
+        computerPlays3 = checkBox3.checked
+        if (! Logic.isComputerToPlay() || playerModel.level !== slider.value)
+            Logic.cancelRunning()
+        playerModel.level = slider.value
+        if (! gameModel.isGameOver)
+            Logic.checkComputerMove()
+    }
+
+    Item {
+        implicitWidth:
+            Math.max(Math.min(font.pixelSize * 16, maxContentWidth),
+                     columnLayout.implicitWidth, minContentWidth)
+        implicitHeight: columnLayout.implicitHeight
+
+        ColumnLayout {
+            id: columnLayout
+
+            anchors.fill: parent
+
+            ColumnLayout {
+                Layout.fillWidth: true
+
+                Label { text: qsTr("Computer plays:") }
+                GridLayout {
+                    columns: gameModel.nuPlayers === 2 ? 1 : 2
+                    Layout.fillWidth: true
+
+                    Row {
+                        Layout.fillWidth: true
+
+                        Rectangle {
+                            width: font.pixelSize; height: width
+                            radius: width / 2
+                            anchors.verticalCenter: parent.verticalCenter
+                            color: gameDisplay.color0[0]
+                        }
+                        Rectangle {
+                            visible: gameModel.nuColors === 4
+                                     && gameModel.nuPlayers === 2
+                            width: font.pixelSize; height: width
+                            radius: width / 2
+                            anchors.verticalCenter: parent.verticalCenter
+                            color: gameDisplay.color2[0]
+                        }
+                        CheckBox {
+                            id: checkBox0
+
+                            enabled: ! isRated
+                            text: {
+                                if (gameModel.nuColors === 4
+                                        && gameModel.nuPlayers === 2)
+                                    return qsTr("Blue/Red")
+                                if (gameModel.gameVariant === "duo")
+                                    return qsTr("Purple")
+                                if (gameModel.gameVariant === "junior")
+                                    return qsTr("Green")
+                                return qsTr("Blue")
+                            }
+                            onClicked:
+                                if (gameModel.nuColors === 4
+                                        && gameModel.nuPlayers === 2)
+                                    checkBox2.checked = checked
+                        }
+                    }
+                    Row {
+                        Layout.fillWidth: true
+
+                        Rectangle {
+                            width: font.pixelSize; height: width
+                            radius: width / 2
+                            anchors.verticalCenter: parent.verticalCenter
+                            color: gameDisplay.color1[0]
+                        }
+                        Rectangle {
+                            visible: gameModel.nuColors === 4
+                                     && gameModel.nuPlayers === 2
+                            width: font.pixelSize; height: width
+                            radius: width / 2
+                            anchors.verticalCenter: parent.verticalCenter
+                            color: gameDisplay.color3[0]
+                        }
+                        CheckBox {
+                            id: checkBox1
+
+                            enabled: ! isRated
+                            text: {
+                                if (gameModel.nuColors === 4
+                                        && gameModel.nuPlayers === 2)
+                                    return qsTr("Yellow/Green")
+                                if (gameModel.gameVariant === "duo"
+                                        || gameModel.gameVariant === "junior")
+                                    return qsTr("Orange")
+                                if (gameModel.nuColors === 2)
+                                    return qsTr("Green")
+                                return qsTr("Yellow")
+                            }
+                            onClicked:
+                                if (gameModel.nuColors === 4
+                                        && gameModel.nuPlayers === 2)
+                                    checkBox3.checked = checked
+                        }
+                    }
+                    Row {
+                        visible: gameModel.nuPlayers > 3
+                        Layout.fillWidth: true
+
+                        Rectangle {
+                            width: font.pixelSize; height: width
+                            radius: width / 2
+                            anchors.verticalCenter: parent.verticalCenter
+                            color: gameDisplay.color3[0]
+                        }
+                        CheckBox {
+                            id: checkBox3
+
+                            enabled: ! isRated
+                            text: qsTr("Green")
+                        }
+                    }
+                    Row {
+                        visible: gameModel.nuPlayers > 2
+                        Layout.fillWidth: true
+
+                        Rectangle {
+                            width: font.pixelSize; height: width
+                            radius: width / 2
+                            anchors.verticalCenter: parent.verticalCenter
+                            color: gameDisplay.color2[0]
+                        }
+                        CheckBox {
+                            id: checkBox2
+
+                            enabled: ! isRated
+                            text: qsTr("Red")
+                        }
+                    }
+                }
+            }
+            RowLayout {
+                Layout.topMargin: 0.6 * font.pixelSize
+                Layout.fillWidth: true
+
+                Label {
+                    id: labelLevel
+
+                    enabled: ! isRated
+                    text: qsTr("Level %1").arg(slider.value)
+                }
+                Slider {
+                    id: slider
+
+                    enabled: ! isRated
+                    from: 1; to: playerModel.maxLevel; stepSize: 1
+                    // Implicit width of main contentItem might not fully fit
+                    // on small screens if 2x2 check boxes are displayed. In
+                    // this case, there is no need to clip the slider also,
+                    // which expands to the dialog width if ! isDesktop
+                    Layout.maximumWidth:
+                        maxContentWidth - labelLevel.implicitWidth
+                        - parent.spacing
+                    Layout.fillWidth: true
+                }
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/Controls.js b/src/pentobi/qml/Controls.js
new file mode 100644 (file)
index 0000000..12317fb
--- /dev/null
@@ -0,0 +1,33 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Controls.js
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+// Helper function to add a mnemonic without using an ampersand in the
+// translatable MenuItem text or in the Action text.
+// Ampersands in texts cause problems for translation with Weblate because
+// Weblate does not support searching strings containing ampersands.
+// Also, there seems to be a bug in Qt that in some cases registers mnemonics
+// in Action texts used in a MenuItem as global shortcuts (last occurred with
+// Qt 5.11.1)
+function addMnemonic(text, mnemonic) {
+    if (! isDesktop || mnemonic === "")
+        return text
+    mnemonic = mnemonic.toLowerCase()
+    var textLower = text.toLowerCase()
+    var pos = textLower.indexOf(mnemonic)
+    // Prefer beginning of word
+    while (pos >= 0 && pos < textLower.length) {
+        if (pos === 0 || textLower.charAt(pos - 1) === " ")
+            break
+        pos = textLower.indexOf(mnemonic, pos + 1)
+    }
+    if (pos < 0 || pos >= textLower.length)
+        pos = textLower.indexOf(mnemonic)
+    if (pos < 0) {
+        console.warn("mnemonic", mnemonic, "not found in", text)
+        return text
+    }
+    return text.substring(0, pos) + "&" + text.substring(pos)
+}
diff --git a/src/pentobi/qml/Dialog.qml b/src/pentobi/qml/Dialog.qml
new file mode 100644 (file)
index 0000000..e08294f
--- /dev/null
@@ -0,0 +1,77 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Dialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import QtQuick.Controls 2.2
+
+Dialog {
+    property real maxContentWidth:
+        rootWindow.contentItem.width - leftPadding - rightPadding
+    property real maxContentHeight: {
+        var h = rootWindow.contentItem.height - topPadding - bottomPadding
+        if (header && header.visible)
+            h -= header.implicitHeight
+        if (footer && footer.visible)
+            h -= footer.implicitHeight
+        return h
+    }
+    property real minContentWidth:
+        // Match window width on mobile devices within reason
+        isDesktop ? 0 : Math.min(40 * font.pixelSize, maxContentWidth)
+
+    function centerDialog() {
+        // Don't bind x and y because that can cause a binding loop if the
+        // application window is interactively resized
+        if (ApplicationWindow.window) {
+            x = (ApplicationWindow.window.width - width) / 2
+            y = (ApplicationWindow.window.height - height) / 2
+        }
+    }
+    // Qt 5.11 doesn't support default buttons yet, this function can be
+    // called as a replacement if the Return key is pressed and should be
+    // reimplemented if needed in derived dialogs.
+    // We don't handle the return key inside the dialog because the dialog will
+    // not consume the event in Qt 5.11 even if it accepts the key event and
+    // might therefore trigger global actions.
+    function returnPressed() {
+        if (! hasButtonFocus())
+            accept()
+    }
+    // Check if any button in the footer the focus. We don't want to handle
+    // the return key as accept (see comemnt above) if any button has the
+    // visual focus because the user might expect that pressing return triggers
+    // the button with the focus.
+    function hasButtonFocus() {
+        for (var i = 0; i < footer.contentChildren.length; ++i) {
+            if (footer.contentChildren[i].visualFocus)
+                return true
+        }
+        return false
+    }
+
+    modal: true
+    focus: true
+    clip: true
+    closePolicy: Popup.CloseOnEscape
+    onOpened: centerDialog()
+    onWidthChanged: centerDialog()
+    onHeightChanged: centerDialog()
+    ApplicationWindow.onWindowChanged:
+        if (ApplicationWindow.window) {
+            ApplicationWindow.window.onWidthChanged.connect(centerDialog)
+            ApplicationWindow.window.onHeightChanged.connect(centerDialog)
+        }
+    Component.onCompleted:
+        if (! isDesktop)
+            // Save some screen space on smartphones
+            title = ""
+
+    Shortcut {
+        sequence: "Return"
+        enabled: visible
+        onActivated: returnPressed()
+    }
+}
diff --git a/src/pentobi/qml/DialogButtonBox.qml b/src/pentobi/qml/DialogButtonBox.qml
new file mode 100644 (file)
index 0000000..c68c425
--- /dev/null
@@ -0,0 +1,13 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/DialogButtonBox.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.4
+
+DialogButtonBox {
+    // In Qt 5.11, undefined alignment can cause a binding loop for
+    // implicitWidth of the dialog in default style
+    alignment: Qt.AlignRight
+}
diff --git a/src/pentobi/qml/DialogButtonBoxOkCancel.qml b/src/pentobi/qml/DialogButtonBoxOkCancel.qml
new file mode 100644 (file)
index 0000000..563a144
--- /dev/null
@@ -0,0 +1,12 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/DialogButtonBoxOkCancel.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "." as Pentobi
+
+Pentobi.DialogButtonBox {
+    ButtonCancel { }
+    ButtonOk { }
+}
diff --git a/src/pentobi/qml/DialogLoader.qml b/src/pentobi/qml/DialogLoader.qml
new file mode 100644 (file)
index 0000000..d99bd05
--- /dev/null
@@ -0,0 +1,17 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/DialogLoader.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Loader {
+    property string url
+
+    function get() {
+        if (! item) source = url
+        return item
+    }
+    function open() { get().open() }
+}
diff --git a/src/pentobi/qml/ExportImageDialog.qml b/src/pentobi/qml/ExportImageDialog.qml
new file mode 100644 (file)
index 0000000..c8a0d80
--- /dev/null
@@ -0,0 +1,66 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ExportImageDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.0
+import QtQuick.Controls 2.2
+import "." as Pentobi
+
+Pentobi.Dialog {
+    id: root
+
+    footer: Pentobi.DialogButtonBox {
+        ButtonOk {
+            enabled: textField.acceptableInput
+            onClicked: checkAccept()
+            DialogButtonBox.buttonRole: DialogButtonBox.InvalidRole
+        }
+        ButtonCancel { }
+    }
+    onOpened: textField.selectAll()
+    onAccepted: {
+        exportImageWidth = parseInt(textField.text)
+        var dialog = imageSaveDialog.get()
+        dialog.name = gameModel.suggestFileName(folder, "png")
+        dialog.selectNameFilter(0)
+        dialog.open()
+    }
+
+    function returnPressed() {
+        if (! hasButtonFocus())
+            checkAccept()
+    }
+    function checkAccept() {
+        if (textField.acceptableInput)
+            accept()
+    }
+
+    Item {
+        implicitWidth:
+            Math.max(Math.min(rowLayout.implicitWidth, maxContentWidth),
+                     minContentWidth)
+        implicitHeight: rowLayout.implicitHeight
+
+        RowLayout {
+            id: rowLayout
+
+            anchors.fill: parent
+
+            Label { text: qsTr("Image width:") }
+            TextField {
+                id: textField
+
+                text: exportImageWidth
+                focus: true
+                inputMethodHints: Qt.ImhDigitsOnly
+                validator: IntValidator{ bottom: 0; top: 32767 }
+                selectByMouse: true
+                Layout.preferredWidth: font.pixelSize * 5
+            }
+            Item { Layout.fillWidth: true }
+        }
+    }
+}
diff --git a/src/pentobi/qml/FatalMessage.qml b/src/pentobi/qml/FatalMessage.qml
new file mode 100644 (file)
index 0000000..aa8edd4
--- /dev/null
@@ -0,0 +1,9 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/FatalMessage.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+MessageDialog {
+    onClosed: Qt.quit()
+}
diff --git a/src/pentobi/qml/FileDialog.qml b/src/pentobi/qml/FileDialog.qml
new file mode 100644 (file)
index 0000000..3e329f2
--- /dev/null
@@ -0,0 +1,296 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/FileDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import QtQuick.Controls 2.4
+import QtQuick.Layouts 1.1
+import Qt.labs.folderlistmodel 2.11
+import "." as Pentobi
+import "Main.js" as Logic
+
+Pentobi.Dialog {
+    id: root
+
+    property bool selectExisting: true
+    property alias name: nameField.text
+    property url folder
+    property url fileUrl
+    property var nameFilterLabels
+    property var nameFilters
+    readonly property url defaultFolder: androidUtils.getDefaultFolder()
+
+    function returnPressed() {
+        if (! hasButtonFocus())
+            checkAccept()
+    }
+    function selectNameField() {
+        if (! isAndroid) {
+            var pos = name.lastIndexOf(".")
+            if (pos < 0)
+                nameField.selectAll()
+            else
+                nameField.select(0, pos)
+        }
+        view.currentIndex = -1
+    }
+
+    function selectNameFilter(index) {
+        comboBoxNameFilter.currentIndex = index
+    }
+
+    signal nameFilterChanged(int index)
+
+    property url _lastFolder
+
+    function isValidName(name) {
+        return name.trim().length > 0
+                && ! (! selectExisting && name.trim().startsWith("."))
+    }
+
+    function checkAccept() {
+        if (! isValidName(name))
+            return
+        folder = folderModel.folder
+        fileUrl = folder + "/" + name.trim()
+        if (! selectExisting
+                && gameModel.checkFileExists(Logic.getFileFromUrl(fileUrl))) {
+            Logic.showQuestion(qsTr("Overwrite file?"), accept)
+            return
+        }
+        accept()
+    }
+
+    footer: Pentobi.DialogButtonBox {
+        Button {
+            enabled: isValidName(name)
+            text: selectExisting ? qsTr("Open") : qsTr("Save")
+            onClicked: checkAccept()
+        }
+        ButtonCancel { }
+    }
+    onOpened: {
+        if (isAndroid && ! folder.toString().startsWith(defaultFolder.toString()))
+            folder = defaultFolder
+        selectNameField()
+    }
+
+    Item {
+        implicitWidth: Math.max(Math.min(font.pixelSize * 30, maxContentWidth),
+                                minContentWidth)
+        implicitHeight: Math.min(font.pixelSize * 30, maxContentHeight)
+
+        Shortcut {
+            sequence: "Alt+Left"
+            onActivated: backButton.onClicked()
+        }
+        ColumnLayout
+        {
+            anchors.fill: parent
+
+            TextField {
+                id: nameField
+
+                visible: ! selectExisting
+                focus: ! isAndroid
+                selectByMouse: true
+                Layout.fillWidth: true
+                Component.onCompleted: nameField.cursorPosition = nameField.length
+                onTextEdited: view.currentIndex = -1
+            }
+            RowLayout {
+                Layout.fillWidth: true
+
+                ToolButton {
+                    id: backButton
+
+                    property bool hasParent:
+                        ! folderModel.folder.toString().endsWith(":///")
+                        && ! (isAndroid && folderModel.folder === defaultFolder)
+
+                    opacity: hasParent ? 1 : 0.5
+                    onClicked:
+                        if (hasParent) {
+                            _lastFolder = folderModel.folder
+                            folderModel.folder = folderModel.parentFolder
+                            if (selectExisting)
+                                name = ""
+                        }
+                    icon {
+                        source: "icons/filedialog-parent.svg"
+                        // Icon size is 16x16
+                        width: font.pixelSize < 20 ? 16 : font.pixelSize
+                        height: font.pixelSize < 20 ? 16 : font.pixelSize
+                        color: frame.palette.buttonText
+                    }
+                }
+                Label {
+                    text: {
+                        if (isAndroid
+                                && folderModel.folder.toString().startsWith(defaultFolder.toString()))
+                            return folderModel.folder.toString().substr(defaultFolder.toString().length + 1)
+                        Logic.getFileFromUrl(folderModel.folder)
+                    }
+                    elide: Text.ElideLeft
+                    Layout.fillWidth: true
+                }
+                ToolButton {
+                    visible: ! selectExisting
+                    icon {
+                        source: "icons/filedialog-newfolder.svg"
+                        // Icon size is 16x16
+                        width: font.pixelSize < 20 ? 16 : font.pixelSize
+                        height: font.pixelSize < 20 ? 16 : font.pixelSize
+                        color: frame.palette.buttonText
+                    }
+                    onClicked: {
+                        var dialog = newFolderDialog.get()
+                        dialog.folder = folderModel.folder
+                        dialog.open()
+                    }
+                }
+            }
+            Frame {
+                id: frame
+
+                padding: 0.1 * font.pixelSize
+                focusPolicy: Qt.TabFocus
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                background: Rectangle {
+                    color: frame.palette.base
+                    border.color: frame.activeFocus ? frame.palette.highlight : frame.palette.mid
+                    radius: 2
+                }
+                ListView {
+                    id: view
+
+                    anchors.fill: parent
+                    clip: true
+                    model: folderModel
+                    boundsBehavior: Flickable.StopAtBounds
+                    highlight: Rectangle {
+                        // Should logically use palette.highlight, but in
+                        // most styles other than the desktop style Fusion,
+                        // palette.highlight is a too flashy color.
+                        color: isDesktop ? palette.highlight : palette.midlight
+                    }
+                    highlightMoveDuration: 0
+                    focus: true
+                    onActiveFocusChanged:
+                        if (activeFocus && currentIndex < 0 && count)
+                            currentIndex = 0
+                    onCurrentIndexChanged:
+                        if (currentIndex >= 0
+                                && ! folderModel.isFolder(currentIndex))
+                            name = folderModel.get(currentIndex, "fileName")
+                    delegate: AbstractButton {
+                        width: view.width
+                        height: 2 * font.pixelSize
+                        focusPolicy: Qt.NoFocus
+                        contentItem: Row {
+                            spacing: 0.3 * font.pixelSize
+                            leftPadding: 0.2 * font.pixelSize
+
+                            Image {
+                                anchors.verticalCenter: parent.verticalCenter
+                                visible: folderModel.isFolder(index)
+                                // Icon size is 16x16
+                                width: font.pixelSize < 20 ? 16 : font.pixelSize
+                                height: width
+                                source: "icons/filedialog-folder.svg"
+                                sourceSize { width: width; height: height }
+                            }
+                            Label {
+                                width: parent.width - parent.spacing - parent.leftPadding
+                                text: index < 0 ? "" : fileName
+                                anchors.verticalCenter: parent.verticalCenter
+                                color: view.currentIndex == index ?
+                                           // See comment at highlight
+                                           (isDesktop ? frame.palette.highlightedText
+                                                      : frame.palette.buttonText) :
+                                           frame.palette.text
+                                horizontalAlignment: Text.AlignHLeft
+                                verticalAlignment: Text.AlignVCenter
+                                elide: Text.ElideMiddle
+                            }
+                        }
+                        onClicked: {
+                            if (folderModel.isFolder(index)) {
+                                delayedOpenFolderTimer.folderName = fileName
+                                delayedOpenFolderTimer.restart()
+                            }
+                            else {
+                                name = fileName
+                                if (! selectExisting)
+                                    selectNameField()
+                            }
+                            view.currentIndex = index
+                        }
+                        onDoubleClicked:
+                            if (! folderModel.isFolder(index))
+                                checkAccept()
+                    }
+                    ScrollBar.vertical: ScrollBar { active: true }
+
+                    FolderListModel {
+                        id: folderModel
+
+                        folder: root.folder
+                        nameFilters: [ root.nameFilter ]
+                        showDirsFirst: true
+                        onStatusChanged:
+                            if (status === FolderListModel.Ready) {
+                                var i = folderModel.indexOf(_lastFolder)
+                                if (i >= 0)
+                                    view.currentIndex = i
+                                else
+                                    view.currentIndex = -1
+                            }
+                    }
+                    // Open folder with small delay such that the folder name
+                    // is visibly highlighted when clicked before it opens. We
+                    // can't set view.currentIndex in onPressed, otherwise the
+                    // item is unwantedly highlighted when flicking the list.
+                    Timer {
+                        id: delayedOpenFolderTimer
+
+                        property string folderName
+
+                        interval: 100
+
+                        onTriggered: {
+                            if (! folderModel.folder.toString().endsWith("/"))
+                                folderModel.folder = folderModel.folder + "/"
+                            _lastFolder = ""
+                            folderModel.folder = folderModel.folder + folderName
+                            if (selectExisting)
+                                name = ""
+                        }
+                    }
+                }
+            }
+            ComboBox {
+                id: comboBoxNameFilter
+
+                model: {
+                    var result = nameFilterLabels
+                    nameFilterLabels.push(qsTr("All files"))
+                    return result
+                }
+                onCurrentIndexChanged: {
+                    if (currentIndex < root.nameFilters.length)
+                        folderModel.nameFilters = root.nameFilters[currentIndex]
+                    else
+                        folderModel.nameFilters = [ "*" ]
+                    nameFilterChanged(currentIndex)
+                }
+                Layout.preferredWidth:
+                    Math.min(font.pixelSize * 14, maxContentWidth)
+                Layout.alignment: Qt.AlignRight
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/GameDisplay.js b/src/pentobi/qml/GameDisplay.js
new file mode 100644 (file)
index 0000000..bd622de
--- /dev/null
@@ -0,0 +1,219 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameDisplay.js
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+function createColorPieces(component, pieceModels) {
+    if (pieceModels.length === 0)
+        return []
+    var properties = { }
+    var pieces = []
+    for (var i = 0; i < pieceModels.length; ++i) {
+        properties["pieceModel"] = pieceModels[i]
+        pieces.push(component.createObject(gameDisplay, properties))
+    }
+    return pieces
+}
+
+function createPieces() {
+    destroyPieces()
+    var file
+    var gameVariant = gameModel.gameVariant
+    if (gameVariant.startsWith("trigon"))
+        file = "PieceTrigon.qml"
+    else if (gameVariant.startsWith("nexos"))
+        file = "PieceNexos.qml"
+    else if (gameVariant.startsWith("callisto"))
+        file = "PieceCallisto.qml"
+    else if (gameVariant.startsWith("gembloq"))
+        file = "PieceGembloQ.qml"
+    else
+        file = "PieceClassic.qml"
+    var component = Qt.createComponent(file)
+    if (component.status !== Component.Ready)
+        console.warn(component.errorString())
+    pieces0 = createColorPieces(component, gameModel.getPieceModels(0))
+    pieces1 = createColorPieces(component, gameModel.getPieceModels(1))
+    pieces2 = createColorPieces(component, gameModel.getPieceModels(2))
+    pieces3 = createColorPieces(component, gameModel.getPieceModels(3))
+    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()
+    }
+}
+
+function destroyPieces() {
+    pieceSelector.transitionsEnabled = false
+    pickedPiece = null
+    destroyColorPieces(pieces0); pieces0 = []
+    destroyColorPieces(pieces1); pieces1 = []
+    destroyColorPieces(pieces2); pieces2 = []
+    destroyColorPieces(pieces3); pieces3 = []
+}
+
+function dropPieceFast() {
+    if (! pickedPiece)
+        return
+    var old = enableAnimations
+    enableAnimations = false
+    pickedPiece = null
+    enableAnimations = old
+}
+
+function findPiece(pieceModel) {
+    var pieces
+    switch (pieceModel.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 movePiece(x, y) {
+    if (pickedPiece == null)
+        return
+    var pos = pieceManipulator.mapToItem(
+                board, pieceManipulator.width / 2, pieceManipulator.height / 2)
+    var fastMove
+    if (! board.contains(pos)) {
+        // Outside board before moving, move to center of board
+        pos = mapFromItem(board, board.width / 2, board.height / 2)
+        x = pos.x - pieceManipulator.width / 2
+        y = pos.y - pieceManipulator.height / 2
+        fastMove = false
+    }
+    else {
+        pos = pieceManipulator.mapToItem(
+                    board,
+                    pieceManipulator.width / 2 + x - pieceManipulator.x,
+                    pieceManipulator.height / 2 + y - pieceManipulator.y)
+        pos.x = Math.max(0, pos.x)
+        pos.x = Math.min(board.width - 1, pos.x)
+        pos.y = Math.max(0, pos.y)
+        pos.y = Math.min(board.height - 1, pos.y)
+        pos = mapFromItem(board, pos.x, pos.y)
+        x = pos.x - pieceManipulator.width / 2
+        y = pos.y - pieceManipulator.height / 2
+        fastMove = true
+    }
+    pieceManipulator.fastMove = fastMove
+    pieceManipulator.x = x
+    pieceManipulator.y = y
+    pieceManipulator.fastMove = false
+}
+
+function onBoardClicked(pos) {
+    dropCommentFocus()
+    if (! setupMode)
+        return
+    var pieceModel = gameModel.addEmpty(pos)
+    if (! pieceModel)
+        return
+    var piece = findPiece(pieceModel)
+    pos = mapFromItem(piece, (piece.width - pieceManipulator.width) / 2,
+                      (piece.height - pieceManipulator.height) / 2)
+    pieceManipulator.x = pos.x
+    pieceManipulator.y = pos.y
+    pickedPiece = piece
+}
+
+function onBoardRightClicked(pos) {
+    dropCommentFocus()
+    var n = gameModel.getMoveNumberAt(pos)
+    if (n < 0)
+        return
+    gameDisplay.openBoardContextMenu(
+                n, board.mapFromGameX(pos.x + 0.5),
+                board.mapFromGameY(pos.y + 0.5))
+}
+
+function shiftPiece(dx, dy) {
+    if (gameModel.gameVariant.startsWith("gembloq"))
+        // In GembloQ, every piece has at least one full square, so we can use
+        // half the x resolution, which makes positioning easier for the user
+        movePiece(pieceManipulator.x + dx * board.gridWidth,
+                  pieceManipulator.y + dy * board.gridHeight / 2)
+    else if (gameModel.gameVariant.startsWith("nexos"))
+        movePiece(pieceManipulator.x + dx * board.gridWidth,
+                  pieceManipulator.y + dy * board.gridHeight)
+    else
+        movePiece(pieceManipulator.x + dx * board.gridWidth / 2,
+                  pieceManipulator.y + dy * board.gridHeight / 2)
+}
+
+function shiftPieceFast(dx, dy) {
+    movePiece(pieceManipulator.x + dx * board.width / 4,
+              pieceManipulator.y + dy * board.height / 4)
+}
+
+function pickPiece(piece) {
+    pickPieceAt(piece, mapFromItem(piece, 0, 0))
+}
+
+function pickPieceAt(piece, coord) {
+    if (playerModel.isGenMoveRunning || gameModel.isGameOver)
+        return
+    if (piece.pieceModel.color !== gameModel.toPlay && ! setupMode) {
+        gameDisplay.showToPlay()
+        return
+    }
+    if (! pieceManipulator.pieceModel) {
+        // Position pieceManipulator at center of piece if possible, but
+        // make sure it is completely visible
+        var x = coord.x - pieceManipulator.width / 2
+        var y = coord.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 pickPieceAtBoard(piece) {
+    pickPieceAt(piece, mapFromItem(board, board.width / 2, board.height / 2))
+}
+
+function playPickedPiece() {
+    if (! pickedPiece)
+        return
+    var pos = pieceManipulator.mapToItem(board, pieceManipulator.width / 2,
+                                         pieceManipulator.height / 2)
+    if (! board.contains(pos))
+        pickedPiece = null
+    else if (setupMode)
+        gameModel.addSetup(pieceManipulator.pieceModel, board.mapToGame(pos))
+    else if (pieceManipulator.legal)
+        play(pieceManipulator.pieceModel, board.mapToGame(pos))
+}
+
+function showMove(move) {
+    var pieceModel = gameModel.preparePiece(move)
+    if (pieceModel === null)
+        return
+    var newPickedPiece = findPiece(pieceModel)
+    if (pickedPiece && newPickedPiece !== pickedPiece)
+        pickedPiece = null
+    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 = newPickedPiece
+}
diff --git a/src/pentobi/qml/GameDisplayDesktop.qml b/src/pentobi/qml/GameDisplayDesktop.qml
new file mode 100644 (file)
index 0000000..bfacdcf
--- /dev/null
@@ -0,0 +1,356 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameDisplayDesktop.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import QtQuick.Controls 2.0
+import Qt.labs.settings 1.0
+import "." as Pentobi
+import "GameDisplay.js" as Logic
+
+Item
+{
+    id: root
+
+    property Item pickedPiece
+
+    // Values: "last_dot", "last_number", "all_number", "none"
+    property string moveMarking: "last_dot"
+
+    property alias showCoordinates: board.showCoordinates
+    property bool enableAnimations: true
+    property real animationDurationMove: enableAnimations ? 300 : 0
+    property real animationDurationFast: enableAnimations ? 80 : 0
+    property bool setupMode
+    property string commentMode: "as_needed"
+    property bool showMoveNumber
+
+    property size imageSourceSize: {
+        var width = board.gridWidth, height = board.gridHeight
+        if (board.isTrigon || board.isGembloQ)
+            return Qt.size(2 * width, height)
+        if (board.isNexos)
+            return Qt.size(1.5 * width, 0.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
+    property var color0: {
+        if (gameModel.gameVariant === "duo") return theme.colorPurple
+        if (gameModel.gameVariant === "junior") return theme.colorGreen
+        return theme.colorBlue
+    }
+    property var color1: {
+        if (gameModel.gameVariant === "duo"
+                || gameModel.gameVariant === "junior") return theme.colorOrange
+        if (gameModel.nuColors === 2) return theme.colorGreen
+        return theme.colorYellow
+    }
+    property var color2: theme.colorRed
+    property var color3: theme.colorGreen
+    property alias isCommentVisible: comment.visible
+
+    readonly property real _relativeBoardWidth: 0.52
+
+    signal play(var pieceModel, point gameCoord)
+
+    function createPieces() { Logic.createPieces() }
+    function destroyPieces() { Logic.destroyPieces() }
+    function findPiece(pieceModel) { return Logic.findPiece(pieceModel) }
+    function pickPieceAtBoard(piece) { Logic.pickPieceAtBoard(piece) }
+    function shiftPiece(dx, dy) { Logic.shiftPiece(dx, dy) }
+    function shiftPieceFast(dx, dy) { Logic.shiftPieceFast(dx, dy) }
+    function playPickedPiece() { Logic.playPickedPiece() }
+    function showToPlay() { }
+    function showComment() { comment.visible = true }
+    function setCommentVisible(visible) { comment.visible = visible }
+    function showPieces() { }
+    function dropCommentFocus() { if (comment.item) comment.item.dropFocus() }
+    function showMove(move) { Logic.showMove(move) }
+    function getBoard() { return board }
+    function showTemporaryMessage(text) {
+        showStatus(text)
+        messageTimer.restart()
+    }
+    function startSearch() { showStatus(qsTr("Computer is thinking…")) }
+    function endSearch() { if (! messageTimer.running) clearStatus() }
+    function startAnalysis() {
+        showStatus(qsTr("Running game analysis…"))
+        comment.visible = false
+    }
+    function endAnalysis() { if (! messageTimer.running) clearStatus() }
+    function deleteAnalysis() { }
+    function analysisAutoloaded() { comment.visible = false }
+    function searchCallback(elapsedSeconds, 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 (! playerModel.isGenMoveRunning || elapsedSeconds < 10)
+            return
+        var text
+        var seconds = Math.ceil(remainingSeconds)
+        if (seconds < 90)
+            text = qsTr("Computer is thinking… (up to %1 seconds remaining)").arg(seconds)
+        else
+        {
+            var minutes = Math.ceil(remainingSeconds / 60)
+            text = qsTr("Computer is thinking… (up to %1 minutes remaining)").arg(minutes)
+        }
+        showStatus(text)
+    }
+    function openBoardContextMenu(moveNumber, x, y) {
+        if (! boardContextMenu.item)
+            boardContextMenu.sourceComponent = boardContextMenuComponent
+        boardContextMenu.item.moveNumber = moveNumber
+        if (isDesktop)
+            boardContextMenu.item.popup()
+        else
+            boardContextMenu.item.popup(x, y)
+    }
+
+    function showStatus(text) {
+        messageTimer.stop()
+        statusText.text = text
+        statusText.opacity = 1
+    }
+    function clearStatus() { statusText.opacity = 0 }
+
+    function _updateCommentVisible() {
+        if (commentMode === "always")
+            comment.visible = true
+        else if (commentMode === "never")
+            comment.visible = false
+        else
+            comment.visible = gameModel.comment !== ""
+    }
+
+    onWidthChanged: Logic.dropPieceFast()
+    onHeightChanged: Logic.dropPieceFast()
+    onCommentModeChanged: _updateCommentVisible()
+
+    Settings {
+        property alias enableAnimations: root.enableAnimations
+        property alias moveMarking: root.moveMarking
+        property alias showCoordinates: root.showCoordinates
+        property alias showMoveNumber: root.showMoveNumber
+        property alias setupMode: root.setupMode
+        property alias commentMode: root.commentMode
+
+        category: "GameDisplayDesktop"
+    }
+    Item {
+        id: mainContent
+
+        anchors {
+            left: parent.left
+            right: parent.right
+            leftMargin: 3
+            rightMargin: 3
+            topMargin: 2
+            top: parent.top
+            bottom: statusBar.top
+        }
+
+        Item {
+            anchors.centerIn: parent
+            width: Math.min(parent.width, parent.height / _relativeBoardWidth)
+            height: {
+                var height = width * _relativeBoardWidth
+                if (board.isTrigon)
+                    height *= Math.sqrt(3) / 2
+                return height
+            }
+
+            Board {
+                id: board
+
+                anchors {
+                    left: parent.left
+                    top: parent.top
+                    bottom: parent.bottom
+                }
+                width: _relativeBoardWidth * parent.width
+                onClicked: Logic.onBoardClicked(pos)
+                onRightClicked: Logic.onBoardRightClicked(pos)
+
+                Loader {
+                    id: boardContextMenu
+
+                    Component {
+                        id: boardContextMenuComponent
+
+                        BoardContextMenu { }
+                    }
+                }
+            }
+            Item {
+                anchors {
+                    left: board.right
+                    right: parent.right
+                    leftMargin:
+                        Math.min(
+                            Math.max(
+                                5,
+                                mainContent.width
+                                - mainContent.height / _relativeBoardWidth),
+                            0.03 * board.width)
+                    verticalCenter: board.verticalCenter
+                }
+                height: board.grabImageTarget.height
+
+                ScoreDisplay {
+                    id: scoreDisplay
+
+                    anchors {
+                        left: parent.left
+                        right: parent.right
+                        top: parent.top
+                    }
+                    height: 0.035 * parent.height
+                }
+                PieceSelectorDesktop {
+                    id: pieceSelector
+
+                    anchors {
+                        left: parent.left
+                        right: parent.right
+                        top: scoreDisplay.bottom
+                        topMargin: 0.02 * parent.height
+                    }
+                    height: (board.isTrigon ? 0.75 : 0.7) * parent.height
+                    transitionsEnabled: false
+                    onPiecePicked: Logic.pickPiece(piece)
+                }
+                Item {
+                    anchors {
+                        left: parent.left
+                        right: parent.right
+                        top: pieceSelector.bottom
+                        bottom: parent.bottom
+                    }
+
+                    Loader {
+                        id: comment
+
+                        anchors.fill: parent
+                        visible: false
+                        sourceComponent:
+                            visible || item ? commentComponent : null
+
+                        Component {
+                            id: commentComponent
+
+                            Comment { }
+                        }
+                    }
+                    Loader {
+                        id: analyzeGame
+
+                        anchors.fill: parent
+                        visible: ! comment.visible
+                                 && (analyzeGameModel.elements.length > 0
+                                     || analyzeGameModel.isRunning)
+                        sourceComponent:
+                            visible || item ? analyzeGameComponent : null
+
+                        Component {
+                            id: analyzeGameComponent
+
+                            AnalyzeGame { theme: rootWindow.theme }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    Item {
+        id: statusBar
+
+        anchors {
+            left: parent.left
+            right: parent.right
+            bottom: parent.bottom
+        }
+        height: 1.7 * statusText.font.pixelSize
+
+        Label {
+            id: statusText
+
+            anchors {
+                left: parent.left
+                top: top.right
+                bottom: parent.bottom
+                leftMargin: 5
+            }
+            opacity: 0
+            color: theme.colorText
+
+            Behavior on opacity {
+                NumberAnimation {
+                    duration: animationDurationFast
+                }
+            }
+        }
+        Label {
+            visible: root.showMoveNumber
+            anchors {
+                right: parent.right
+                top: top.right
+                bottom: parent.bottom
+                rightMargin: 5
+            }
+            text: gameModel.positionInfoShort
+            color: theme.colorText
+            opacity: 0.8
+        }
+        Timer {
+            id: messageTimer
+
+            interval: 3000
+            onTriggered: clearStatus()
+        }
+    }
+    PieceManipulator {
+        id: pieceManipulator
+
+        legal: {
+            if (pickedPiece === null) return false
+            // Need explicit dependencies on x, y, pieceModel.state
+            var pos = parent.mapToItem(board, x + width / 2, y + height / 2)
+            if (setupMode)
+                return gameModel.isLegalSetupPos(pickedPiece.pieceModel,
+                                                 pickedPiece.pieceModel.state,
+                                                 board.mapToGame(pos))
+            return gameModel.isLegalPos(pickedPiece.pieceModel,
+                                        pickedPiece.pieceModel.state,
+                                        board.mapToGame(pos))
+        }
+        width: {
+            var f
+            if (board.isTrigon) f = 7
+            else if (board.isNexos) f = 11
+            else if (board.isGembloQ) f = 10.5
+            else if (board.isCallisto) f = 6.5
+            else f = 7.3
+            return Math.max(200, f * board.gridHeight)
+        }
+        height: width
+        pieceModel: pickedPiece ? pickedPiece.pieceModel : null
+        onPiecePlayed: Logic.playPickedPiece()
+    }
+    Connections {
+        target: gameModel
+        onPositionChanged:
+            if (analyzeGameModel.elements.length > 0
+                    || analyzeGameModel.isRunning)
+                comment.visible = false
+            else
+                _updateCommentVisible()
+    }
+}
diff --git a/src/pentobi/qml/GameDisplayMobile.qml b/src/pentobi/qml/GameDisplayMobile.qml
new file mode 100644 (file)
index 0000000..2889625
--- /dev/null
@@ -0,0 +1,265 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameDisplayMobile.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.0
+import QtQuick.Layouts 1.0
+import Qt.labs.settings 1.0
+import "." as Pentobi
+import "GameDisplay.js" as Logic
+
+Item
+{
+    id: root
+
+    property Item pickedPiece
+
+    // Values: "last_dot", "last_number", "all_number", "none"
+    property string moveMarking: "last_dot"
+
+    property alias showCoordinates: board.showCoordinates
+    property bool enableAnimations: true
+    property real animationDurationMove: enableAnimations ? 300 : 0
+    property real animationDurationFast: enableAnimations ? 80 : 0
+    property bool setupMode
+    property alias boardContextMenu: boardContextMenu
+    property size imageSourceSize: {
+        var width = board.gridWidth, height = board.gridHeight
+        if (board.isTrigon || board.isGembloQ)
+            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
+    property var color0: {
+        if (gameModel.gameVariant === "duo") return theme.colorPurple
+        if (gameModel.gameVariant === "junior") return theme.colorGreen
+        return theme.colorBlue
+    }
+    property var color1: {
+        if (gameModel.gameVariant === "duo"
+                || gameModel.gameVariant === "junior") return theme.colorOrange
+        if (gameModel.nuColors === 2) return theme.colorGreen
+        return theme.colorYellow
+    }
+    property var color2: theme.colorRed
+    property var color3: theme.colorGreen
+    property bool isCommentVisible: swipeView.currentIndex === 1
+
+    signal play(var pieceModel, point gameCoord)
+
+    function createPieces() { Logic.createPieces() }
+    function destroyPieces() { Logic.destroyPieces() }
+    function findPiece(pieceModel) { return Logic.findPiece(pieceModel) }
+    function pickPieceAtBoard(piece) { Logic.pickPieceAtBoard(piece) }
+    function shiftPiece(dx, dy) { Logic.shiftPiece(dx, dy) }
+    function shiftPieceFast(dx, dy) { Logic.shiftPieceFast(dx, dy) }
+    function playPickedPiece() { Logic.playPickedPiece() }
+    function showToPlay() { pieceSelector.contentY = 0 }
+    function showAnalyzeGame() { pickedPiece = null; swipeView.currentIndex = 2 }
+    function showComment() { pickedPiece = null; swipeView.currentIndex = 1 }
+    function showPieces() { swipeView.currentIndex = 0 }
+    function dropCommentFocus() { navigationPanel.dropCommentFocus() }
+    function showMove(move) { Logic.showMove(move) }
+    function getBoard() { return board }
+    function showTemporaryMessage(text) { message.showTemporary(text) }
+    function searchCallback(elapsedSeconds, remainingSeconds) { }
+    function startSearch() { }
+    function endSearch() { }
+    function startAnalysis() { showAnalyzeGame() }
+    function endAnalysis() { }
+    function deleteAnalysis() { if (swipeView.currentIndex === 2) showPieces() }
+    function analysisAutoloaded() { }
+    function openBoardContextMenu(moveNumber, x, y) {
+        if (! boardContextMenu.item)
+            boardContextMenu.sourceComponent = boardContextMenuComponent
+        boardContextMenu.item.moveNumber = moveNumber
+        if (isDesktop)
+            boardContextMenu.item.popup()
+        else
+            boardContextMenu.item.popup(x, y)
+    }
+
+    onWidthChanged: Logic.dropPieceFast()
+    onHeightChanged: Logic.dropPieceFast()
+
+    Settings {
+        property alias enableAnimations: root.enableAnimations
+        property alias moveMarking: root.moveMarking
+        property alias showCoordinates: root.showCoordinates
+        property alias swipeViewCurrentIndex: swipeView.currentIndex
+        property alias setupMode: root.setupMode
+
+        category: "GameDisplayMobile"
+    }
+    Column {
+        id: column
+
+        width: root.width
+        anchors.centerIn: root
+        spacing: 0.01 * board.width
+
+        Board {
+            id: board
+
+            width: Math.min(parent.width, 0.7 * root.height)
+            height: isTrigon ? Math.sqrt(3) / 2 * width : width
+            anchors.horizontalCenter: parent.horizontalCenter
+            onClicked: Logic.onBoardClicked(pos)
+            onRightClicked: Logic.onBoardRightClicked(pos)
+
+            Loader {
+                id: boardContextMenu
+
+                Component {
+                    id: boardContextMenuComponent
+
+                    BoardContextMenu { }
+                }
+            }
+        }
+        SwipeView {
+            id: swipeView
+
+            width: Math.min(1.3 * board.width, root.width)
+            height: Math.min(root.height - board.height, board.height)
+            clip: width < rootWindow.contentItem.width
+            anchors.horizontalCenter: board.horizontalCenter
+
+            Column {
+                id: columnPieces
+
+                spacing: 2
+
+                ScoreDisplay {
+                    id: scoreDisplay
+
+                    width: swipeView.width
+                    height: 0.06 * swipeView.width
+                    anchors.horizontalCenter: parent.horizontalCenter
+                }
+                PieceSelectorMobile {
+                    id: pieceSelector
+
+                    property real elementSize:
+                        // Show at least 3 rows
+                        Math.min(board.width / columns, height / 3)
+
+                    columns: pieces0 && pieces0.length <= 21 ? 7 : 8
+                    width: elementSize * columns
+                    height: swipeView.height - scoreDisplay.height
+                            - columnPieces.spacing
+                    rowSpacing: {
+                        // Don't show partial pieces
+                        var n = Math.floor(height / elementSize)
+                        return (height - n * elementSize) / n
+                    }
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    transitionsEnabled: false
+                    onPiecePicked: Logic.pickPiece(piece)
+                }
+            }
+            NavigationPanel {
+                id: navigationPanel
+            }
+            ColumnLayout {
+                AnalyzeGame {
+                    theme: rootWindow.theme
+                    Layout.margins: 0.01 * parent.width
+                    Layout.fillWidth: true
+                    Layout.fillHeight: true
+                }
+                NavigationButtons
+                {
+                    Layout.fillWidth: true
+                    Layout.maximumHeight:
+                        Math.min(50, 0.08 * rootWindow.contentItem.height,
+                                 root.width / 6)
+                }
+            }
+        }
+    }
+    Pentobi.BusyIndicator {
+        id: busyIndicator
+
+        running: busyIndicatorRunning
+        width: Math.min(0.2 * swipeView.width, swipeView.height)
+        height: width
+        x: (root.width - width) / 2
+        y: column.y + swipeView.y + (swipeView.height - height) / 2
+        opacity: 0.7
+    }
+    Rectangle {
+        id: message
+
+        function showTemporary(text) {
+            messageText.text = text
+            opacity = 1
+            messageTimer.restart()
+        }
+
+        opacity: 0
+        x: (root.width - width) / 2
+        y: column.y + swipeView.y + (swipeView.height - height) / 2
+        radius: 0.1 * height
+        color: theme.colorMessageBase
+        implicitWidth: messageText.implicitWidth + 0.5 * messageText.implicitHeight
+        implicitHeight: 1.5 * messageText.implicitHeight
+
+        Behavior on opacity {
+            NumberAnimation {
+                duration: animationDurationFast
+            }
+        }
+
+        Text {
+            id: messageText
+
+            anchors.centerIn: parent
+            color: theme.colorMessageText
+        }
+        Timer {
+            id: messageTimer
+
+            interval: 2500
+            onTriggered: message.opacity = 0
+        }
+    }
+    PieceManipulator {
+        id: pieceManipulator
+
+        legal: {
+            if (pickedPiece === null) return false
+            // Need explicit dependencies on x, y, pieceModel.state
+            var pos = parent.mapToItem(board, x + width / 2, y + height / 2)
+            if (setupMode)
+                return gameModel.isLegalSetupPos(pickedPiece.pieceModel,
+                                                 pickedPiece.pieceModel.state,
+                                                 board.mapToGame(pos))
+            return gameModel.isLegalPos(pickedPiece.pieceModel,
+                                        pickedPiece.pieceModel.state,
+                                        board.mapToGame(pos))
+        }
+        width: {
+            var f
+            if (board.isTrigon) f = 7
+            else if (board.isNexos) f = 12.5
+            else if (board.isGembloQ) f = 12
+            else if (board.isCallisto) f = 6.7
+            else f = 8.7
+            return Math.max(200, f * board.gridHeight)
+        }
+        height: width
+        pieceModel: pickedPiece ? pickedPiece.pieceModel : null
+        onPiecePlayed: Logic.playPickedPiece()
+    }
+}
diff --git a/src/pentobi/qml/GameInfoDialog.qml b/src/pentobi/qml/GameInfoDialog.qml
new file mode 100644 (file)
index 0000000..c1fe63b
--- /dev/null
@@ -0,0 +1,134 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameInfoDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.0
+import "." as Pentobi
+
+Pentobi.Dialog {
+    footer: DialogButtonBoxOkCancel { }
+    onOpened: {
+        textFieldPlayerName0.text = gameModel.playerName0
+        textFieldPlayerName1.text = gameModel.playerName1
+        textFieldPlayerName2.text = gameModel.playerName2
+        textFieldPlayerName3.text = gameModel.playerName3
+        textFieldDate.text = gameModel.date
+        textFieldTime.text = gameModel.time
+        textFieldEvent.text = gameModel.event
+        textFieldRound.text = gameModel.round
+    }
+    onAccepted: {
+        gameModel.playerName0 = textFieldPlayerName0.text
+        gameModel.playerName1 = textFieldPlayerName1.text
+        gameModel.playerName2 = textFieldPlayerName2.text
+        gameModel.playerName3 = textFieldPlayerName3.text
+        gameModel.date = textFieldDate.text
+        gameModel.time = textFieldTime.text
+        gameModel.event = textFieldEvent.text
+        gameModel.round = textFieldRound.text
+    }
+
+    Flickable {
+        implicitWidth: Math.max(Math.min(font.pixelSize * 22, maxContentWidth),
+                                minContentWidth)
+        implicitHeight: Math.min(gridLayout.implicitHeight, maxContentHeight)
+        contentHeight: gridLayout.implicitHeight
+        clip: true
+
+        GridLayout {
+            id: gridLayout
+
+            anchors.fill: parent
+            columns: 2
+
+            Label {
+                text: {
+                    if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+                        return qsTr("Player Blue/Red:")
+                    if (gameModel.gameVariant === "duo")
+                        return qsTr("Player Purple:")
+                    if (gameModel.gameVariant === "junior")
+                        return qsTr("Player Green:")
+                    return qsTr("Player Blue:")
+                }
+            }
+            TextField {
+                id: textFieldPlayerName0
+
+                selectByMouse: true
+                Layout.fillWidth: true
+            }
+            Label {
+                text: {
+                    if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+                        return qsTr("Player Yellow/Green:")
+                    if (gameModel.gameVariant === "duo" || gameModel.gameVariant === "junior")
+                        return qsTr("Player Orange:")
+                    if (gameModel.nuColors === 2)
+                        return qsTr("Player Green:")
+                    return qsTr("Player Yellow:")
+                }
+            }
+            TextField {
+                id: textFieldPlayerName1
+
+                selectByMouse: true
+                Layout.fillWidth: true
+            }
+            Label {
+                visible: textFieldPlayerName2.visible
+                text: qsTr("Player Red:")
+            }
+            TextField {
+                id: textFieldPlayerName2
+
+                visible: gameModel.nuPlayers > 2
+                selectByMouse: true
+                Layout.fillWidth: true
+            }
+            Label {
+                visible: textFieldPlayerName3.visible
+                text: qsTr("Player Green:")
+            }
+            TextField {
+                id: textFieldPlayerName3
+
+                visible: gameModel.nuPlayers > 3
+                selectByMouse: true
+                Layout.fillWidth: true
+            }
+            Label { text: qsTr("Date:") }
+            TextField {
+                id: textFieldDate
+
+                selectByMouse: true
+                Layout.fillWidth: true
+            }
+            Label { text: qsTr("Time:") }
+            TextField {
+                id: textFieldTime
+
+                selectByMouse: true
+                Layout.fillWidth: true
+            }
+            Label { text: qsTr("Event:") }
+            TextField {
+                id: textFieldEvent
+
+                selectByMouse: true
+                Layout.fillWidth: true
+            }
+            Label { text: qsTr("Round:") }
+            TextField {
+                id: textFieldRound
+
+                selectByMouse: true
+                Layout.fillWidth: true
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/GameVariantDialog.qml b/src/pentobi/qml/GameVariantDialog.qml
new file mode 100644 (file)
index 0000000..f934cfe
--- /dev/null
@@ -0,0 +1,273 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameVariantDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.2
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Dialog {
+    property string gameVariant
+
+    footer: DialogButtonBoxOkCancel { }
+    onOpened: {
+        gameVariant = gameModel.gameVariant
+        if (gameVariant.startsWith("classic")) comboBox.currentIndex = 0
+        else if (gameVariant === "duo") comboBox.currentIndex = 1
+        else if (gameVariant === "junior") comboBox.currentIndex = 2
+        else if (gameVariant.startsWith("trigon")) comboBox.currentIndex = 3
+        else if (gameVariant.startsWith("nexos")) comboBox.currentIndex = 4
+        else if (gameVariant.startsWith("gembloq")) comboBox.currentIndex = 5
+        else if (gameVariant.startsWith("callisto")) comboBox.currentIndex = 6
+    }
+    onAccepted: Logic.changeGameVariant(gameVariant)
+
+    Item {
+        implicitWidth:
+            Math.max(Math.min(columnLayout.implicitWidth, maxContentWidth),
+                     minContentWidth)
+        implicitHeight: columnLayout.implicitHeight
+
+
+        ColumnLayout {
+            id: columnLayout
+
+            anchors.fill: parent
+
+            ComboBox {
+                id: comboBox
+
+                model: [
+                    qsTr("Classic"), qsTr("Duo"), qsTr("Junior"),
+                    qsTr("Trigon"), qsTr("Nexos"), qsTr("GembloQ"),
+                    qsTr("Callisto") ]
+                onCurrentIndexChanged:
+                    switch (currentIndex) {
+                    case 0:
+                        if (! gameVariant.startsWith("classic"))
+                            gameVariant = "classic_2"
+                        break
+                    case 1:
+                        if (gameVariant !== "duo")
+                            gameVariant = "duo"
+                        break
+                    case 2:
+                        if (gameVariant !== "junior")
+                            gameVariant = "junior"
+                        break
+                    case 3:
+                        if (! gameVariant.startsWith("trigon"))
+                            gameVariant = "trigon_2"
+                        break
+                    case 4:
+                        if (! gameVariant.startsWith("nexos"))
+                            gameVariant = "nexos_2"
+                        break
+                    case 5:
+                        if (! gameVariant.startsWith("gembloq"))
+                            gameVariant = "gembloq_2"
+                        break
+                    case 6:
+                        if (! gameVariant.startsWith("callisto"))
+                            gameVariant = "callisto_2"
+                        break
+                    }
+                Layout.fillWidth: true
+            }
+            GridLayout {
+                columns: 2
+                Layout.fillWidth: true
+
+                Label {
+                    text: qsTr("Players:")
+                    Layout.fillWidth: true
+                }
+                RowLayout {
+                    Layout.fillWidth: true
+
+                    RadioButton {
+                        text: "2"
+                        checked: gameVariant === "classic_2"
+                                 || gameVariant === "duo"
+                                 || gameVariant === "junior"
+                                 || gameVariant === "trigon_2"
+                                 || gameVariant === "nexos_2"
+                                 || gameVariant === "gembloq_2"
+                                 || gameVariant === "callisto_2"
+                                 || gameVariant === "gembloq_2_4"
+                                 || gameVariant === "callisto_2_4"
+                        onClicked:
+                            if (checked) {
+                                if (gameVariant.startsWith("classic"))
+                                    gameVariant = "classic_2"
+                                else if (gameVariant.startsWith("trigon"))
+                                    gameVariant = "trigon_2"
+                                else if (gameVariant.startsWith("nexos"))
+                                    gameVariant = "nexos_2"
+                                else if (gameVariant === "callisto")
+                                    gameVariant = "callisto_2_4"
+                                else if (gameVariant === "callisto_3")
+                                    gameVariant = "callisto_2"
+                                else if (gameVariant === "gembloq")
+                                    gameVariant = "gembloq_2_4"
+                                else if (gameVariant === "gembloq_3")
+                                    gameVariant = "gembloq_2"
+                            }
+                        Layout.fillWidth: true
+                    }
+                    RadioButton {
+                        text: "3"
+                        opacity: enabled
+                        enabled: gameVariant.startsWith("classic")
+                                 || gameVariant.startsWith("trigon")
+                                 || gameVariant.startsWith("gembloq")
+                                 || gameVariant.startsWith("callisto")
+                        checked: gameVariant === "classic_3"
+                                 || gameVariant === "trigon_3"
+                                 || gameVariant === "gembloq_3"
+                                 || gameVariant === "callisto_3"
+                        onClicked:
+                            if (checked) {
+                                if (gameVariant.startsWith("classic"))
+                                    gameVariant = "classic_3"
+                                else if (gameVariant.startsWith("trigon"))
+                                    gameVariant = "trigon_3"
+                                else if (gameVariant.startsWith("gembloq"))
+                                    gameVariant = "gembloq_3"
+                                else if (gameVariant.startsWith("callisto"))
+                                    gameVariant = "callisto_3"
+                            }
+                        Layout.fillWidth: true
+                    }
+                    RadioButton {
+                        text: "4"
+                        opacity: enabled
+                        enabled: gameVariant.startsWith("classic")
+                                 || gameVariant.startsWith("trigon")
+                                 || gameVariant.startsWith("nexos")
+                                 || gameVariant.startsWith("gembloq")
+                                 || gameVariant.startsWith("callisto")
+                        checked: gameVariant === "classic"
+                                 || gameVariant === "trigon"
+                                 || gameVariant === "nexos"
+                                 || gameVariant === "gembloq"
+                                 || gameVariant === "callisto"
+                        onClicked:
+                            if (checked) {
+                                if (gameVariant.startsWith("classic"))
+                                    gameVariant = "classic"
+                                else if (gameVariant.startsWith("trigon"))
+                                    gameVariant = "trigon"
+                                else if (gameVariant.startsWith("nexos"))
+                                    gameVariant = "nexos"
+                                else if (gameVariant.startsWith("gembloq"))
+                                    gameVariant = "gembloq"
+                                else if (gameVariant.startsWith("callisto"))
+                                    gameVariant = "callisto"
+                            }
+                        Layout.fillWidth: true
+                    }
+                }
+                Label {
+                    text: qsTr("Colors:")
+                    Layout.fillWidth: true
+                }
+                RowLayout {
+                    Layout.fillWidth: true
+
+                    RadioButton {
+                        text: "2"
+                        opacity: gameVariant === "duo"
+                                 || gameVariant === "junior"
+                                 || gameVariant === "gembloq_2"
+                                 || gameVariant === "gembloq_2_4"
+                                 || gameVariant === "callisto_2"
+                                 || gameVariant === "callisto_2_4"
+                        enabled: gameVariant === "duo"
+                                 || gameVariant === "junior"
+                                 || gameVariant.startsWith("gembloq")
+                                 || gameVariant.startsWith("callisto")
+                        checked: gameVariant === "duo"
+                                 || gameVariant === "junior"
+                                 || gameVariant === "gembloq_2"
+                                 || gameVariant === "callisto_2"
+                        onClicked:
+                            if (checked) {
+                                if (gameVariant.startsWith("callisto"))
+                                    gameVariant = "callisto_2"
+                                else if (gameVariant.startsWith("gembloq"))
+                                    gameVariant = "gembloq_2"
+                            }
+                        Layout.fillWidth: true
+                    }
+                    RadioButton {
+                        text: "3"
+                        opacity: checked
+                        enabled: gameVariant.startsWith("trigon")
+                                 || gameVariant.startsWith("gembloq")
+                                 || gameVariant.startsWith("callisto")
+                        checked: gameVariant === "trigon_3"
+                                 || gameVariant === "gembloq_3"
+                                 || gameVariant === "callisto_3"
+                        onClicked:
+                            if (checked) {
+                                if (gameVariant.startsWith("trigon"))
+                                    gameVariant = "trigon_3"
+                                else if (gameVariant.startsWith("gembloq"))
+                                    gameVariant = "gembloq_3"
+                                else if (gameVariant.startsWith("callisto"))
+                                    gameVariant = "callisto_3"
+                            }
+                        Layout.fillWidth: true
+                    }
+                    RadioButton {
+                        text: "4"
+                        opacity: gameVariant.startsWith("classic")
+                                 || gameVariant === "trigon"
+                                 || gameVariant === "trigon_2"
+                                 || gameVariant.startsWith("nexos")
+                                 || gameVariant === "gembloq"
+                                 || gameVariant === "gembloq_2"
+                                 || gameVariant === "gembloq_2_4"
+                                 || gameVariant === "callisto"
+                                 || gameVariant === "callisto_2"
+                                 || gameVariant === "callisto_2_4"
+                        enabled: gameVariant.startsWith("classic")
+                                 || gameVariant.startsWith("trigon")
+                                 || gameVariant.startsWith("nexos")
+                                 || gameVariant.startsWith("gembloq")
+                                 || gameVariant.startsWith("callisto")
+                        checked: gameVariant.startsWith("classic")
+                                 || gameVariant === "trigon"
+                                 || gameVariant === "trigon_2"
+                                 || gameVariant.startsWith("nexos")
+                                 || gameVariant === "gembloq"
+                                 || gameVariant === "gembloq_2_4"
+                                 || gameVariant === "callisto"
+                                 || gameVariant === "callisto_2_4"
+                        onClicked:
+                            if (checked) {
+                                if (gameVariant.startsWith("trigon"))
+                                    gameVariant = "trigon"
+                                else if (gameVariant.startsWith("nexos"))
+                                    gameVariant = "nexos"
+                                else if (gameVariant === "gembloq_2")
+                                    gameVariant = "gembloq_2_4"
+                                else if (gameVariant === "gembloq_3")
+                                    gameVariant = "gembloq"
+                                else if (gameVariant === "callisto_2")
+                                    gameVariant = "callisto_2_4"
+                                else if (gameVariant === "callisto_3")
+                                    gameVariant = "callisto"
+                            }
+                        Layout.fillWidth: true
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/GotoMoveDialog.qml b/src/pentobi/qml/GotoMoveDialog.qml
new file mode 100644 (file)
index 0000000..382e722
--- /dev/null
@@ -0,0 +1,64 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GotoMoveDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.0
+import QtQuick.Controls 2.2
+import "." as Pentobi
+
+Pentobi.Dialog {
+    id: root
+
+    footer: Pentobi.DialogButtonBox {
+        ButtonOk {
+            enabled: textField.acceptableInput
+            onClicked: checkAccept()
+            DialogButtonBox.buttonRole: DialogButtonBox.InvalidRole
+        }
+        ButtonCancel { }
+    }
+    onOpened: textField.selectAll()
+    onAccepted: gameModel.gotoMove(parseInt(textField.text))
+
+    function returnPressed() {
+        if (! hasButtonFocus())
+            checkAccept()
+    }
+    function checkAccept() {
+        if (textField.acceptableInput)
+            accept()
+    }
+
+    Item {
+        implicitWidth:
+            Math.max(Math.min(rowLayout.implicitWidth, maxContentWidth),
+                     minContentWidth)
+        implicitHeight: rowLayout.implicitHeight
+
+        RowLayout {
+            id: rowLayout
+
+            anchors.fill: parent
+
+            Label { text: qsTr("Move number:") }
+            TextField {
+                id: textField
+
+                text: gameModel.moveNumber === 0 ?
+                          gameModel.moveNumber + gameModel.movesLeft : gameModel.moveNumber
+                focus: true
+                selectByMouse: true
+                inputMethodHints: Qt.ImhDigitsOnly
+                validator: IntValidator{
+                    bottom: 0
+                    top: gameModel.moveNumber + gameModel.movesLeft
+                }
+                Layout.preferredWidth: font.pixelSize * 5
+            }
+            Item { Layout.fillWidth: true }
+        }
+    }
+}
diff --git a/src/pentobi/qml/HelpWindow.qml b/src/pentobi/qml/HelpWindow.qml
new file mode 100644 (file)
index 0000000..39f8d4a
--- /dev/null
@@ -0,0 +1,67 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/HelpWindow.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import QtQuick.Layouts 1.1
+import QtQuick.Window 2.1
+import QtQuick.Controls 2.3
+import QtWebView 1.1
+import pentobi 1.0
+import Qt.labs.settings 1.0
+
+Window {
+    id: root
+
+    property url startUrl
+    property real defaultWidth: Math.min(font.pixelSize * 48, Screen.desktopAvailableWidth)
+    property real defaultHeight: Math.min(font.pixelSize * 57, Screen.desktopAvailableHeight)
+
+    // Instead of initializing webView.url, we provide an init function that
+    // needs to be called after show() to work around an issue with the initial
+    // zoom factor of WebView sometimes very large on Android. Note that this
+    // workaround only reduces the likelihood for this bug to occur
+    // (QTBUG-58290, last occurred with Qt 5.11.2)
+    function init() { webView.url = startUrl }
+
+    width: defaultWidth; height: defaultHeight
+    minimumWidth: 240; minimumHeight: 240
+    x: (Screen.width - defaultWidth) / 2
+    y: (Screen.height - defaultHeight) / 2
+    title: qsTr("Pentobi Help")
+
+    // Note that Android doesn't actually support multiple windows, but using
+    // WebView in a window works around a bug related to QTBUG-62409, which
+    // makes WebView consume Back button events, so we cannot close the help
+    // window with the back key. But we need to destroy the window after
+    // closing, otherwise it doesn't show when made visible again.
+    onClosing: if (isAndroid) helpWindow.source = ""
+
+    WebView {
+        id: webView
+
+        anchors.fill: parent
+    }
+    Shortcut {
+        sequence: "Ctrl+W"
+        onActivated: close()
+    }
+    Shortcut {
+        sequence: "Alt+Left"
+        onActivated: webView.goBack()
+    }
+    Shortcut {
+        sequence: "Alt+Right"
+        onActivated: webView.goForward()
+    }
+    Settings {
+        property alias x: root.x
+        property alias y: root.y
+        property alias width: root.width
+        property alias height: root.height
+
+        category: "HelpWindow"
+    }
+}
diff --git a/src/pentobi/qml/ImageSaveDialog.qml b/src/pentobi/qml/ImageSaveDialog.qml
new file mode 100644 (file)
index 0000000..d3e7d35
--- /dev/null
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ImageSaveDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.FileDialog {
+    title: qsTr("Save Image")
+    selectExisting: false
+    nameFilterLabels: [
+        qsTr("PNG image files"),
+        qsTr("JPEG image files")
+    ]
+    nameFilters: [
+        [ "*.png", "*.PNG" ],
+        [ "*.jpg", "*.JPG", "*.jpeg", "*.JPEG" ]
+    ]
+    folder: rootWindow.folder
+    onNameFilterChanged: {
+        if (index >= nameFilters.length)
+            return
+        var pos = name.lastIndexOf(".")
+        if (pos < 0)
+            return
+        var newName = name.substr(0, pos + 1)
+        pos = nameFilters[index][0].lastIndexOf(".")
+        newName += nameFilters[index][0].substr(pos + 1)
+        name = newName
+        selectNameField()
+    }
+    onAccepted: {
+        rootWindow.folder = folder
+        Logic.exportImage(fileUrl)
+    }
+}
diff --git a/src/pentobi/qml/InitialRatingDialog.qml b/src/pentobi/qml/InitialRatingDialog.qml
new file mode 100644 (file)
index 0000000..1b87a48
--- /dev/null
@@ -0,0 +1,63 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/InitialRatingDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.0
+import "." as Pentobi
+import "Main.js" as Logic
+
+Pentobi.Dialog {
+    footer: DialogButtonBoxOkCancel { }
+    onAccepted: {
+        ratingModel.setInitialRating(Math.round(slider.value))
+        Logic.ratedGameNoVerify()
+    }
+
+    ColumnLayout
+    {
+        Item {
+            implicitWidth:
+                Math.max(Math.min(textLabel.implicitWidth, maxContentWidth),
+                         minContentWidth)
+            implicitHeight: textLabel.implicitHeight
+            Layout.fillWidth: true
+
+            Label {
+                id: textLabel
+
+                anchors.fill: parent
+                text: qsTr("Initialize your rating for this game variant.")
+                wrapMode: Text.Wrap
+            }
+        }
+        RowLayout {
+            Layout.topMargin: 0.6 * font.pixelSize
+
+            Label {
+                text: qsTr("Initial rating:")
+            }
+            Label {
+                text: Math.round(slider.value)
+                font.bold: true
+            }
+        }
+        Slider {
+            id: slider
+
+            value: 800
+            from: 800; to: 2000; stepSize: 100
+            Layout.fillWidth: true
+        }
+        RowLayout {
+            Layout.fillWidth: true
+
+            Label { text: qsTr("Beginner") }
+            Item { Layout.fillWidth: true }
+            Label { text: qsTr("Expert") }
+        }
+    }
+}
diff --git a/src/pentobi/qml/LineSegment.qml b/src/pentobi/qml/LineSegment.qml
new file mode 100644 (file)
index 0000000..df4cb5a
--- /dev/null
@@ -0,0 +1,179 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/LineSegment.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+
+// Piece element for Nexos. See Square.qml for comments.
+Item {
+    id: root
+
+    property bool isHorizontal
+
+    Loader {
+        anchors.fill: root
+        opacity: imageOpacity0
+        sourceComponent: opacity > 0 || item ? component0 : null
+
+        Component {
+            id: component0
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+                mirror: ! isHorizontal
+                rotation: isHorizontal ? 0 : -90
+            }
+        }
+    }
+    Loader {
+        anchors.fill: root
+        opacity: imageOpacitySmall0
+        sourceComponent: opacity > 0 || item ? componentSmall0 : null
+
+        Component {
+            id: componentSmall0
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                antialiasing: true
+                mirror: ! isHorizontal
+                rotation: isHorizontal ? 0 : -90
+            }
+        }
+    }
+    Loader {
+        anchors.fill: root
+        opacity: imageOpacity90
+        sourceComponent: opacity > 0 || item ? component90 : null
+
+        Component {
+            id: component90
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+                mirror: isHorizontal
+                rotation: isHorizontal ? -180 : -90
+            }
+        }
+    }
+    Loader {
+        anchors.fill: root
+        opacity: imageOpacitySmall90
+        sourceComponent: opacity > 0 || item ? componentSmall90 : null
+
+        Component {
+            id: componentSmall90
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                antialiasing: true
+                mirror: isHorizontal
+                rotation: isHorizontal ? -180 : -90
+            }
+        }
+    }
+    Loader {
+        anchors.fill: root
+        opacity: imageOpacity180
+        sourceComponent: opacity > 0 || item ? component180 : null
+
+        Component {
+            id: component180
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+                mirror: ! isHorizontal
+                rotation: isHorizontal ? -180 : -270
+            }
+        }
+    }
+    Loader {
+        anchors.fill: root
+        opacity: imageOpacitySmall180
+        sourceComponent: opacity > 0 || item ? componentSmall180 : null
+
+        Component {
+            id: componentSmall180
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                antialiasing: true
+                mirror: ! isHorizontal
+                rotation: isHorizontal ? -180 : -270
+            }
+        }
+    }
+    Loader {
+        anchors.fill: root
+        opacity: imageOpacity270
+        sourceComponent: opacity > 0 || item ? component270 : null
+
+        Component {
+            id: component270
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+                mirror: isHorizontal
+                rotation: isHorizontal ? 0 : -270
+            }
+        }
+    }
+    Loader {
+        anchors.fill: root
+        opacity: imageOpacitySmall270
+        sourceComponent: opacity > 0 || item ? componentSmall270 : null
+
+        Component {
+            id: componentSmall270
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                antialiasing: true
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                mirror: isHorizontal
+                rotation: isHorizontal ? 0 : -270
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/Main.js b/src/pentobi/qml/Main.js
new file mode 100644 (file)
index 0000000..a5479cd
--- /dev/null
@@ -0,0 +1,785 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Main.js
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+function analyzeGame(nuSimulations) {
+    if (! gameModel.isMainVar) {
+        showInfo(qsTr("Game analysis is only possible in main variation."))
+        return
+    }
+    gameDisplay.startAnalysis()
+    cancelRunning()
+    analyzeGameModel.start(gameModel, playerModel, nuSimulations)
+}
+
+function autoSaveNoVerify() {
+    gameModel.autoSave()
+    syncSettings.setValueBool("computerPlays0", computerPlays0)
+    syncSettings.setValueBool("computerPlays1", computerPlays1)
+    syncSettings.setValueBool("computerPlays2", computerPlays2)
+    syncSettings.setValueBool("computerPlays3", computerPlays3)
+    syncSettings.setValueBool("isRated", isRated)
+    syncSettings.setValueBool("initComputerColorsOnNewGame", initComputerColorsOnNewGame)
+    syncSettings.setValueInt("level", playerModel.level)
+    syncSettings.sync()
+    analyzeGameModel.autoSave(gameModel)
+    // This will lose the geometry if the user has changed it, maximized the
+    // window and then closed it before returning to windowed state. But better
+    // than overwriting the geometry with the maximized/fullscreen one.
+    if (visibility === Window.Windowed) {
+        settings.x = x
+        settings.y = y
+        settings.width = width
+        settings.height = height
+    }
+    settings.visibility = visibility
+}
+
+function autoSaveNoVerifyAndQuit() {
+    autoSaveNoVerify()
+    Qt.quit()
+}
+
+function cancelRunning(showMessage) {
+    if (analyzeGameModel.isRunning) {
+        analyzeGameModel.cancel()
+        if (showMessage)
+            showTemporaryMessage(qsTr("Game analysis aborted"))
+    }
+    if (playerModel.isGenMoveRunning) {
+        playerModel.cancelGenMove()
+        if (showMessage)
+            showTemporaryMessage(qsTr("Computer move aborted"))
+    }
+    delayedCheckComputerMove.stop()
+}
+
+function changeGameVariant(gameVariant) {
+    if (gameModel.gameVariant === gameVariant)
+        return
+    verify(function() { changeGameVariantNoVerify(gameVariant) })
+}
+
+function changeGameVariantNoVerify(gameVariant) {
+    cancelRunning()
+    lengthyCommand.run(function() {
+        // Destroy pieces before changing game variant to avoid flickering
+        // in PieceSelectorMobile if toPlay != 0
+        gameDisplay.destroyPieces()
+        gameModel.changeGameVariant(gameVariant)
+        gameDisplay.createPieces()
+        gameDisplay.showToPlay()
+        gameDisplay.setupMode = false
+        isRated = false
+        analyzeGameModel.clear()
+        gameDisplay.showPieces()
+        initComputerColors()
+    })
+}
+
+function checkComputerMove() {
+    if (gameModel.isGameOver) {
+        var msg = gameModel.getResultMessage()
+        if (isRated) {
+            var oldRating = Math.round(ratingModel.rating)
+            ratingModel.addResult(gameModel, playerModel.level)
+            var newRating = Math.round(ratingModel.rating)
+            msg += "\n"
+            if (newRating > oldRating)
+                msg += qsTr("Your rating has increased from %1 to %2.").arg(oldRating).arg(newRating)
+            else if (newRating < oldRating)
+                msg += qsTr("Your rating has decreased from %1 to %2.").arg(oldRating).arg(newRating)
+            else
+                msg += qsTr("Your rating stays at %1.").arg(newRating)
+            isRated = false
+        }
+        showInfo(msg)
+        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()
+}
+
+function checkStoragePermission() {
+    if (! androidUtils.checkPermission("android.permission.WRITE_EXTERNAL_STORAGE")) {
+        showInfo(qsTr("No permission to access storage"))
+        return false
+    }
+    return true
+}
+
+function clearRating() {
+    showQuestion(qsTr("Delete all rating information for the current game variant?"),
+                 clearRatingNoVerify)
+}
+
+function clearRatingNoVerify() {
+    ratingModel.clearRating()
+    showTemporaryMessage(qsTr("Rating information deleted"))
+}
+
+/** 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()) {
+        setComputerNone()
+        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
+        {
+            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
+            }
+        }
+        initComputerColorsOnNewGame = true
+    }
+    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 = "themes/" + themeName + "/Theme.qml"
+    var component = Qt.createComponent(source)
+    if (component.status !== Component.Ready) {
+        console.warn(component.errorString())
+        source = "themes/light/Theme.qml"
+        component = Qt.createComponent(source)
+    }
+    return component.createObject(rootWindow)
+}
+
+function deleteAllVar() {
+    showQuestion(qsTr("Delete all variations?"), deleteAllVarNoVerify)
+}
+
+function deleteAllVarNoVerify() {
+    gameModel.deleteAllVar()
+    showTemporaryMessage(qsTr("Variations deleted"))
+}
+
+function exportAsciiArt(fileUrl) {
+    if (! checkStoragePermission())
+        return
+    var file = getFileFromUrl(fileUrl)
+    if (! gameModel.saveAsciiArt(file))
+        showInfo(qsTr("Save failed.") + "\n" + gameModel.getError())
+    else {
+        androidUtils.scanFile(file)
+        showTemporaryMessage(qsTr("File saved"))
+    }
+}
+
+function exportImage(fileUrl) {
+    if (! checkStoragePermission())
+        return
+    var board = gameDisplay.getBoard()
+    var size = Qt.size(exportImageWidth, exportImageWidth * board.height / board.width)
+    if (! board.grabImageTarget.grabToImage(function(result) {
+        var file = getFileFromUrl(fileUrl)
+        if (! result.saveToFile(file))
+            showInfo(qsTr("Saving image failed or unsupported image format"))
+        else {
+            androidUtils.scanFile(file)
+            showTemporaryMessage(qsTr("Image saved"))
+        }
+    }, size))
+        showInfo(qsTr("Creating image failed"))
+}
+
+function findNextComment() {
+    if (gameModel.findNextComment()) {
+        gameDisplay.showComment()
+        return
+    }
+    if (gameModel.canGoBackward)
+        // Current is not root
+        showQuestion(qsTr("End of tree was reached. Continue search from start of the tree?"),
+                     findNextCommentContinueFromRoot)
+    else
+        showInfo(qsTr("No comment found"))
+
+}
+
+function findNextCommentContinueFromRoot() {
+    if (gameModel.findNextCommentContinueFromRoot()) {
+        gameDisplay.showComment()
+        return
+    }
+    showInfo(qsTr("No comment found"))
+}
+
+function genMove() {
+    cancelRunning()
+    gameDisplay.pickedPiece = null
+    gameDisplay.showToPlay()
+    playerModel.startGenMove(gameModel)
+}
+
+function getFileFromUrl(fileUrl) {
+    var file = fileUrl.toString()
+    file = file.replace(/^(file:\/{3})/,"/")
+    return decodeURIComponent(file)
+}
+
+function getFileInfo(isRated, file, isModified) {
+    if (isRated)
+        //: Label for rated game. The argument is the game number.
+        return qsTr("Rated Game %1").arg(ratingModel.numberGames + 1)
+    if (isModified)
+        return qsTr("%1 (modified)").arg(file)
+    return file
+}
+
+function getGameLabel(setupMode, isRated, file, isModified, short) {
+    if (setupMode)
+        return short ?
+                    //: Small-screen label for setup mode (short for
+                    //: "Setup Mode").
+                    qsTr("Setup")
+                  : qsTr("Setup Mode")
+    if (isRated)
+        //: Label for ongoing rated game
+        return qsTr("Rated")
+    if (file === "")
+        return ""
+    var label
+    var n = ratingModel.getGameNumberOfFile(file)
+    if (n > 0)
+        label = short ?
+                    //: Small-screen label for finished rated game (short for
+                    //: "Rated Game"). The argument is the game number.
+                    qsTr("Rated %1").arg(n)
+                  : qsTr("Rated Game %1").arg(n)
+    else {
+        var pos = Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\"))
+        label = file.substring(pos + 1)
+        if (label.toLowerCase().endsWith(".blksgf"))
+            label = label.substring(0, label.length - ".blksgf".length)
+    }
+    return (isModified ? "*" : "") + label
+}
+
+function getWindowTitle(file, isModified) {
+    if (file === "")
+        //: Window title if no file is loaded.
+        return qsTr("Pentobi")
+    var pos = Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\"))
+    var name = file.substring(pos + 1)
+    if (isModified)
+        name = "*" + name
+    //: Window title if file is loaded. The argument is the file name
+    //: prepended with a star if the file has been modified.
+    return qsTr("%1 - Pentobi").arg(name)
+}
+
+function help() {
+    var lang = Qt.locale().name
+    var pos = lang.indexOf("_")
+    if (pos >= 0)
+        lang = lang.substr(0, pos)
+    if (lang !== "C" && lang !== "de")
+        lang = "C"
+    var url
+    if (isAndroid)
+        url = androidUtils.extractHelp(lang)
+    else if (helpDir)
+        url = "file://" + helpDir + "/" + lang + "/pentobi/index.html"
+    else
+        url = "qrc:///qml/help/" + lang + "/pentobi/index.html"
+    if (openHelpExternally) {
+        Qt.openUrlExternally(url)
+        return
+    }
+    if (! helpWindow.item) {
+        helpWindow.source = "HelpWindow.qml"
+        helpWindow.item.startUrl = url
+    }
+    helpWindow.item.show()
+    helpWindow.item.init()
+}
+
+function init() {
+    if (gameModel.loadAutoSave()) {
+        computerPlays0 =
+                syncSettings.valueBool("computerPlays0", computerPlays0)
+        computerPlays1 =
+                syncSettings.valueBool("computerPlays1", computerPlays1)
+        computerPlays2 =
+                syncSettings.valueBool("computerPlays2", computerPlays2)
+        computerPlays3 =
+                syncSettings.valueBool("computerPlays3", computerPlays3)
+        isRated = syncSettings.valueBool("isRated", isRated)
+        initComputerColorsOnNewGame =
+                syncSettings.valueBool("initComputerColorsOnNewGame",
+                                       initComputerColorsOnNewGame)
+    }
+    playerModel.level = syncSettings.valueInt("level", 1)
+    if (isMultiColor()) {
+        computerPlays2 = computerPlays0
+        computerPlays3 = computerPlays1
+    }
+    gameDisplay.createPieces()
+    if (gameModel.checkFileModifiedOutside())
+    {
+        showWindow()
+        showQuestion(qsTr("File has been modified by another application. Reload?"), reloadFile)
+        return
+    }
+    analyzeGameModel.loadAutoSave(gameModel)
+    if (analyzeGameModel.elements.length > 0)
+        gameDisplay.analysisAutoloaded()
+    // initialFile is a context property set from command line argument
+    if (initialFile) {
+        if (gameModel.isModified)
+            showWindow()
+        verify(function() { openFileBlocking(initialFile) })
+    }
+    showWindow()
+    if (isRated) {
+        // Game-related properties in settings could be inconsistent with
+        // autosaved game, better initialize with info from ratingModel
+        var player = ratingModel.getNextHumanPlayer()
+        computerPlays0 = (player !== 0)
+        computerPlays1 = (player !== 1)
+        computerPlays2 = (player !== 2)
+        computerPlays3 = (player !== 3)
+        if (isMultiColor()) {
+            computerPlays2 = computerPlays0
+            computerPlays3 = computerPlays1
+        }
+        playerModel.level = ratingModel.getNextLevel(playerModel.maxLevel)
+        showInfo(qsTr("Continuing rated game"))
+        checkComputerMove()
+        return
+    }
+    if (isComputerToPlay() && ! gameModel.canGoForward
+            && ! gameModel.isGameOver)
+        showQuestion(qsTr("Continue computer move?"), checkComputerMove)
+}
+
+function initComputerColors() {
+    if (! initComputerColorsOnNewGame)
+        return
+    // Default setting is that the computer plays all colors but the first
+    computerPlays0 = false
+    computerPlays1 = true
+    computerPlays2 = true
+    computerPlays3 = true
+    if (isMultiColor())
+        computerPlays2 = false
+}
+
+function isComputerToPlay() {
+    if (gameModel.gameVariant == "classic_3" && gameModel.toPlay === 3)
+        return computerPlays(gameModel.altPlayer)
+    return computerPlays(gameModel.toPlay)
+}
+
+function isMultiColor() {
+    return gameModel.nuColors === 4 && gameModel.nuPlayers === 2
+}
+
+function keepOnlyPosition() {
+    showQuestion(qsTr("Keep only position?"), keepOnlyPositionNoVerify)
+}
+
+function keepOnlyPositionNoVerify() {
+    gameModel.keepOnlyPosition()
+    showTemporaryMessage(qsTr("Kept only position"))
+}
+
+function keepOnlySubtree() {
+    showQuestion(qsTr("Keep only subtree?"), keepOnlySubtreeNoVerify)
+}
+
+function keepOnlySubtreeNoVerify() {
+    gameModel.keepOnlySubtree()
+    showTemporaryMessage(qsTr("Kept only subtree"))
+}
+
+function moveDownVar() {
+    gameModel.moveDownVar()
+    showVariationInfo()
+}
+
+function moveGenerated(move) {
+    gameModel.playMove(move)
+    if (isPlaySingleMoveRunning)
+        isPlaySingleMoveRunning = false
+    else
+        delayedCheckComputerMove.restart()
+}
+
+function moveUpVar() {
+    gameModel.moveUpVar()
+    showVariationInfo()
+}
+
+function newGame()
+{
+    verify(newGameNoVerify)
+}
+
+function newGameNoVerify()
+{
+    gameModel.newGame()
+    gameDisplay.setupMode = false
+    gameDisplay.showToPlay()
+    gameDisplay.showPieces()
+    isRated = false
+    analyzeGameModel.clear()
+    initComputerColors()
+}
+
+function nextPiece() {
+    var currentPickedPiece = null
+    if (gameDisplay.pickedPiece)
+        currentPickedPiece = gameDisplay.pickedPiece.pieceModel
+    var pieceModel = gameModel.nextPiece(currentPickedPiece)
+    if (pieceModel)
+        gameDisplay.pickPieceAtBoard(gameDisplay.findPiece(pieceModel))
+}
+
+function open() {
+    if (! checkStoragePermission())
+        return
+    verify(openNoVerify)
+}
+
+function openNoVerify() {
+    openDialog.open()
+}
+
+function openFile(file) {
+    lengthyCommand.run(function() { openFileBlocking(file) })
+}
+
+function openFileBlocking(file) {
+    var oldGameVariant = gameModel.gameVariant
+    var oldEnableAnimations = gameDisplay.enableAnimations
+    gameDisplay.enableAnimations = false
+    if (! gameModel.openFile(file))
+        showInfo(qsTr("Open failed.") + "\n" + gameModel.getError())
+    else
+        setComputerNone()
+    if (gameModel.gameVariant != oldGameVariant)
+        gameDisplay.createPieces()
+    gameDisplay.showToPlay()
+    gameDisplay.enableAnimations = oldEnableAnimations
+    gameDisplay.setupMode = false
+    isRated = false
+    analyzeGameModel.clear()
+    if (gameModel.comment.length > 0)
+        gameDisplay.showComment()
+    else
+        gameDisplay.showPieces()
+}
+
+function openFileUrl() {
+    openFile(getFileFromUrl(openDialog.item.fileUrl))
+}
+
+function openClipboard()
+{
+    verify(openClipboardNoVerify)
+}
+
+function openClipboardNoVerify() {
+    lengthyCommand.run(function() {
+        var oldGameVariant = gameModel.gameVariant
+        var oldEnableAnimations = gameDisplay.enableAnimations
+        gameDisplay.enableAnimations = false
+        if (! gameModel.openClipboard())
+            showInfo(qsTr("Open failed.") + "\n" + gameModel.getError())
+        else
+            setComputerNone()
+        if (gameModel.gameVariant != oldGameVariant)
+            gameDisplay.createPieces()
+        gameDisplay.showToPlay()
+        gameDisplay.enableAnimations = oldEnableAnimations
+        gameDisplay.setupMode = false
+        isRated = false
+        analyzeGameModel.clear()
+    })
+}
+
+function openRecentFile(file) {
+    verify(function() { openFile(file) })
+}
+
+function pickNamedPiece(name) {
+    var currentPickedPiece = null
+    if (gameDisplay.pickedPiece)
+        currentPickedPiece = gameDisplay.pickedPiece.pieceModel
+    var pieceModel = gameModel.pickNamedPiece(name, currentPickedPiece)
+    if (pieceModel)
+        gameDisplay.pickPieceAtBoard(gameDisplay.findPiece(pieceModel))
+}
+
+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 prevPiece() {
+    var currentPickedPiece = null
+    if (gameDisplay.pickedPiece)
+        currentPickedPiece = gameDisplay.pickedPiece.pieceModel
+    var pieceModel = gameModel.previousPiece(currentPickedPiece)
+    if (pieceModel)
+        gameDisplay.pickPieceAtBoard(gameDisplay.findPiece(pieceModel))
+}
+
+function quit() {
+    if (gameModel.checkAutosaveModifiedOutside()) {
+        if (! gameModel.isModified)
+            return true
+        showQuestion(qsTr("Autosaved game was changed by another instance of Pentobi. Overwrite?"),
+                     autoSaveNoVerifyAndQuit)
+        return false
+    }
+    autoSaveNoVerify()
+    return true
+}
+
+function ratedGame()
+{
+    verify(ratedGameCheckFirstGame)
+}
+
+function ratedGameCheckFirstGame() {
+    if (ratingModel.numberGames === 0)
+        initialRatingDialog.open()
+    else
+        ratedGameNoVerify()
+}
+
+function ratedGameNoVerify()
+{
+    var player = ratingModel.getNextHumanPlayer()
+    var level = ratingModel.getNextLevel(playerModel.maxLevel)
+    var gameVariant = gameModel.gameVariant
+    var msg
+    switch (player) {
+    case 0:
+        if (gameVariant === "duo")
+            msg = qsTr("Start rated game with Purple against Pentobi level %1?").arg(level)
+        else if (gameVariant === "junior")
+            msg = qsTr("Start rated game with Green against Pentobi level %1?").arg(level)
+        else if (isMultiColor())
+            msg = qsTr("Start rated game with Blue/Red against Pentobi level %1?").arg(level)
+        else
+            msg = qsTr("Start rated game with Blue against Pentobi level %1?").arg(level)
+        break
+    case 1:
+        if (gameVariant === "duo" || gameVariant === "junior")
+            msg = qsTr("Start rated game with Orange against Pentobi level %1?").arg(level)
+        else if (isMultiColor())
+            msg = qsTr("Start rated game with Yellow/Green against Pentobi level %1?").arg(level)
+        else if (gameModel.nuColors === 2)
+            msg = qsTr("Start rated game with Green against Pentobi level %1?").arg(level)
+        else
+            msg = qsTr("Start rated game with Yellow against Pentobi level %1?").arg(level)
+        break
+    case 2:
+        msg = qsTr("Start rated game with Red against Pentobi level %1?").arg(level)
+        break
+    case 3:
+        msg = qsTr("Start rated game with Green against Pentobi level %1?").arg(level)
+        break
+    }
+    showQuestion(msg, ratedGameStart)
+}
+
+function ratedGameStart() {
+    var player = ratingModel.getNextHumanPlayer()
+    computerPlays0 = (player !== 0)
+    computerPlays1 = (player !== 1)
+    computerPlays2 = (player !== 2)
+    computerPlays3 = (player !== 3)
+    if (isMultiColor()) {
+        computerPlays2 = computerPlays0
+        computerPlays3 = computerPlays1
+    }
+    var level = ratingModel.getNextLevel(playerModel.maxLevel)
+    playerModel.level = level
+    gameModel.newGame()
+    //: Player name for game info in rated game. First argument is version of
+    //: Pentobi, second argument is level.
+    var computerName =
+            qsTr("Pentobi %1 (level %2)").arg(Qt.application.version).arg(level)
+    //: Player name for game info in rated game.
+    var humanName = qsTr("Human")
+    gameModel.playerName0 = computerPlays0 ? computerName : humanName
+    gameModel.playerName1 = computerPlays1 ? computerName : humanName
+    if (gameModel.nuPlayers > 2)
+        gameModel.playerName2 = computerPlays2 ? computerName : humanName
+    if (gameModel.nuPlayers > 3)
+        gameModel.playerName3 = computerPlays3 ? computerName : humanName
+    gameModel.event = qsTr("Rated game")
+    gameModel.round = ratingModel.numberGames + 1
+    gameDisplay.setupMode = false
+    gameDisplay.showToPlay()
+    gameDisplay.showPieces()
+    isRated = true
+    analyzeGameModel.clear()
+    checkComputerMove()
+}
+
+function rating() {
+    if (ratingModel.numberGames === 0) {
+        showInfo(qsTr("You have not yet played rated games in this game variant."))
+        return
+    }
+    // Never reuse RatingDialog
+    // See comment in Main.qml at ratingModel.onHistoryChanged
+    ratingDialog.sourceComponent = null
+    ratingDialog.open()
+}
+
+function reloadFile() {
+    openFile(gameModel.file)
+}
+
+function save() {
+    if (! checkStoragePermission())
+        return
+    if (gameModel.checkFileModifiedOutside())
+        showQuestion(qsTr("File has been modified by another application. Overwrite?"),
+                     saveCurrentFile)
+    else
+        saveCurrentFile()
+}
+
+function saveAs() {
+    if (! checkStoragePermission())
+        return
+    var dialog = saveDialog.get()
+    dialog.name = gameModel.suggestGameFileName(folder)
+    dialog.open()
+}
+
+function saveCurrentFile() {
+    saveFile(gameModel.file)
+}
+
+function saveFile(file) {
+    if (! gameModel.save(file))
+        showInfo(qsTr("Save failed.") + "\n" + gameModel.getError())
+    else
+        showTemporaryMessage(qsTr("File saved"))
+}
+
+function setComputerNone() {
+    computerPlays0 = false
+    computerPlays1 = false
+    computerPlays2 = false
+    computerPlays3 = false
+}
+
+function showFatal(text) {
+    var dialog = fatalMessage.get()
+    dialog.text = text
+    dialog.open()
+}
+
+function showInfo(text) {
+    var dialog = infoMessage.get()
+    dialog.text = text
+    dialog.open()
+}
+
+function showQuestion(text, acceptedFunc) {
+    questionMessage.get().openWithCallback(text, acceptedFunc)
+}
+
+function showTemporaryMessage(text) {
+    gameDisplay.showTemporaryMessage(text)
+}
+
+function showVariationInfo() {
+    showTemporaryMessage(qsTr("Variation is now %1").arg(gameModel.getVariationInfo()))
+}
+
+function showWindow() {
+    x = settings.x
+    y = settings.y
+    width = settings.width
+    height = settings.height
+    switch (settings.visibility) {
+    case Window.Maximized: showMaximized(); break
+    case Window.FullScreen: showFullScreen(); break
+    default: show()
+    }
+}
+
+function truncate() {
+    showQuestion(qsTr("Truncate this subtree?"), gameModel.truncate)
+}
+
+function truncateChildren() {
+    showQuestion(qsTr("Truncate children?"), truncateChildrenNoVerify)
+}
+
+function truncateChildrenNoVerify() {
+    gameModel.truncateChildren()
+    showTemporaryMessage(qsTr("Children truncated"))
+}
+
+function undo() {
+    gameModel.undo()
+}
+
+function verify(callback)
+{
+    if (gameModel.isModified) {
+        showQuestion(qsTr("Discard game?"), callback)
+        return
+    }
+    callback()
+}
diff --git a/src/pentobi/qml/Main.qml b/src/pentobi/qml/Main.qml
new file mode 100644 (file)
index 0000000..7f39404
--- /dev/null
@@ -0,0 +1,531 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Main.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQml 2.2
+import QtQuick 2.11
+import QtQuick.Controls 2.3
+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: rootWindow
+
+    property bool computerPlays0
+    property bool computerPlays1: true
+    property bool computerPlays2: true
+    property bool computerPlays3: true
+    property bool isPlaySingleMoveRunning
+    property bool isRated
+
+    property alias gameDisplay: gameDisplayLoader.item
+
+    // If the user manually disabled all computer colors in the dialog, we
+    // assume that they want to edit games rather than play, and we will not
+    // initialize the computer colors on New Game but only clear the board.
+    property bool initComputerColorsOnNewGame: true
+
+    property bool isAndroid: Qt.platform.os === "android"
+    property string themeName: isAndroid ? "dark" : "system"
+    property QtObject theme: Logic.createTheme(themeName)
+    property url folder: androidUtils.getDefaultFolder()
+
+    property real defaultWidth:
+        isAndroid ? Screen.desktopAvailableWidth
+                  : Math.min(Screen.desktopAvailableWidth, 1164)
+    property real defaultHeight:
+        isAndroid ? Screen.desktopAvailableHeight
+                  : Math.min(Screen.desktopAvailableHeight, 662)
+
+    property int exportImageWidth: 420
+    property bool busyIndicatorRunning: lengthyCommand.isRunning
+                                        || playerModel.isGenMoveRunning
+                                        || analyzeGameModel.isRunning
+    property bool showToolBar: true
+
+    minimumWidth: isDesktop ? 481 : 240
+    minimumHeight: isDesktop ? 303 : 301
+    color: theme.colorBackground
+    title: Logic.getWindowTitle(gameModel.file, gameModel.isModified)
+    onClosing: if ( ! Logic.quit()) close.accepted = false
+    Component.onCompleted: Logic.init()
+
+    MouseArea {
+        anchors.fill: parent
+        onClicked: gameDisplay.dropCommentFocus()
+    }
+    Pentobi.ToolBar {
+        id: toolBar
+
+        visible: isDesktop || visibility !== Window.FullScreen
+        showContent: ! isDesktop || showToolBar
+        anchors {
+            left: parent.left
+            right: parent.right
+            top: parent.top
+            margins: isDesktop ? 2 : 0
+        }
+    }
+    Loader {
+        id: gameDisplayLoader
+
+        anchors {
+            left: parent.left
+            right: parent.right
+            top: toolBar.visible ? toolBar.bottom : parent.top
+            bottom: parent.bottom
+            margins: isDesktop ? 2 : 0
+        }
+        source:
+            isDesktop ? "GameDisplayDesktop.qml" : "GameDisplayMobile.qml"
+
+        Connections {
+            target: gameDisplayLoader.item
+            onPlay: Logic.play(pieceModel, gameCoord)
+        }
+    }
+    MouseArea {
+        visible: isDesktop
+        acceptedButtons: Qt.NoButton // only for setting cursor shape
+        anchors.fill: parent
+        cursorShape: busyIndicatorRunning ? Qt.BusyCursor : Qt.ArrowCursor
+    }
+    Settings {
+        id: settings
+
+        property real x: (Screen.width - defaultWidth) / 2
+        property real y: (Screen.height - defaultHeight) / 2
+        property real width: defaultWidth
+        property real height: defaultHeight
+        property int visibility
+        property alias folder: rootWindow.folder
+        property alias themeName: rootWindow.themeName
+        property alias exportImageWidth: rootWindow.exportImageWidth
+        property alias showToolBar: rootWindow.showToolBar
+        property alias showVariations: gameModel.showVariations
+    }
+    GameModel {
+        id: gameModel
+
+        onPositionAboutToChange: Logic.cancelRunning(true)
+        onPositionChanged: {
+            gameDisplay.pickedPiece = null
+            if (gameModel.canGoBackward || gameModel.canGoForward
+                    || gameModel.moveNumber > 0)
+                gameDisplay.setupMode = false
+            analyzeGameModel.markCurrentMove(gameModel)
+            gameDisplay.dropCommentFocus()
+        }
+        onInvalidSgfFile: Logic.showInfo(gameModel.getError())
+    }
+    PlayerModel {
+        id: playerModel
+
+        gameVariant: gameModel.gameVariant
+        onMoveGenerated: Logic.moveGenerated(move)
+        onSearchCallback: gameDisplay.searchCallback(elapsedSeconds, remainingSeconds)
+        onIsGenMoveRunningChanged:
+            if (isGenMoveRunning) gameDisplay.startSearch()
+            else gameDisplay.endSearch()
+        Component.onCompleted:
+            if (notEnoughMemory())
+                Logic.showFatal(qsTr("Not enough memory"))
+    }
+    AnalyzeGameModel {
+        id: analyzeGameModel
+
+        onIsRunningChanged: if (! isRunning) gameDisplay.endAnalysis()
+    }
+    RatingModel {
+        id: ratingModel
+
+        gameVariant: gameModel.gameVariant
+    }
+    AndroidUtils { id: androidUtils }
+    SyncSettings { id: syncSettings }
+    DialogLoader { id: aboutDialog; url: "AboutDialog.qml" }
+    DialogLoader { id: computerDialog; url: "ComputerDialog.qml" }
+    DialogLoader { id: fatalMessage; url: "FatalMessage.qml" }
+    DialogLoader { id: gameVariantDialog; url: "GameVariantDialog.qml" }
+    DialogLoader { id: gameInfoDialog; url: "GameInfoDialog.qml" }
+    DialogLoader { id: initialRatingDialog; url: "InitialRatingDialog.qml" }
+    DialogLoader { id: newFolderDialog; url: "NewFolderDialog.qml" }
+    DialogLoader { id: openDialog; url: "OpenDialog.qml" }
+    DialogLoader { id: exportImageDialog; url: "ExportImageDialog.qml" }
+    DialogLoader { id: imageSaveDialog; url: "ImageSaveDialog.qml" }
+    DialogLoader { id: asciiArtSaveDialog; url: "AsciiArtSaveDialog.qml" }
+    DialogLoader { id: gotoMoveDialog; url: "GotoMoveDialog.qml" }
+    DialogLoader { id: ratingDialog; url: "RatingDialog.qml" }
+    DialogLoader { id: saveDialog; url: "SaveDialog.qml" }
+    DialogLoader { id: infoMessage; url: "MessageDialog.qml" }
+    DialogLoader { id: questionMessage; url: "QuestionDialog.qml" }
+    DialogLoader { id: analyzeDialog; url: "AnalyzeDialog.qml" }
+    DialogLoader { id: appearanceDialog; url: "AppearanceDialog.qml" }
+    DialogLoader { id: moveAnnotationDialog; url: "MoveAnnotationDialog.qml" }
+    Loader { id: helpWindow }
+
+    // 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: 500
+        onTriggered: Logic.checkComputerMove()
+    }
+
+    // Delay lengthy blocking function calls such that 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
+        enabled: isAndroid
+        onStateChanged:
+            if (Qt.application.state === Qt.ApplicationSuspended)
+                Logic.autoSaveNoVerify()
+    }
+
+    Action {
+        id: actionBackToMainVar
+
+        shortcut: "Ctrl+M"
+        text: qsTr("Main Variation")
+        enabled: ! isRated && ! gameModel.isMainVar
+        onTriggered: Qt.callLater(function() { gameModel.backToMainVar() }) // QTBUG-69682
+    }
+    Action {
+        id: actionBackward
+
+        shortcut: "Ctrl+Left"
+        enabled: gameModel.canGoBackward && ! isRated
+        onTriggered: gameModel.goBackward()
+    }
+    Action {
+        id: actionBackward10
+
+        shortcut: "Ctrl+Shift+Left"
+        enabled: gameModel.canGoBackward && ! isRated
+        onTriggered: gameModel.goBackward10()
+    }
+    Action {
+        id: actionBeginning
+
+        shortcut: "Ctrl+Home"
+        enabled: gameModel.canGoBackward && ! isRated
+        onTriggered: gameModel.goBeginning()
+    }
+    Action {
+        id: actionForward
+
+        shortcut: "Ctrl+Right"
+        enabled: gameModel.canGoForward && ! isRated
+        onTriggered: gameModel.goForward()
+    }
+    Action {
+        id: actionForward10
+
+        shortcut: "Ctrl+Shift+Right"
+        enabled: gameModel.canGoForward && ! isRated
+        onTriggered: gameModel.goForward10()
+    }
+    Action {
+        id: actionEnd
+
+        shortcut: "Ctrl+End"
+        enabled: gameModel.canGoForward && ! isRated
+        onTriggered: gameModel.goEnd()
+    }
+    Action {
+        id: actionPrevVar
+
+        shortcut: "Ctrl+Up"
+        enabled: gameModel.hasPrevVar && ! isRated
+        onTriggered: gameModel.goPrevVar()
+    }
+    Action {
+        id: actionNextVar
+
+        shortcut: "Ctrl+Down"
+        enabled: gameModel.hasNextVar && ! isRated
+        onTriggered: gameModel.goNextVar()
+    }
+    Action {
+        id: actionBeginningOfBranch
+
+        shortcut: "Ctrl+B"
+        text: qsTr("Beginning of Branch")
+        enabled: ! isRated && gameModel.hasEarlierVar
+        onTriggered: Qt.callLater(function() { gameModel.gotoBeginningOfBranch() }) // QTBUG-69682
+    }
+    Action {
+        id: actionComment
+
+        shortcut: "Ctrl+T"
+        text: qsTr("Comment")
+        checkable: true
+        checked: gameDisplay.isCommentVisible
+        onTriggered:
+            if (isDesktop)
+                gameDisplay.setCommentVisible(checked)
+            else {
+                if (checked)
+                    gameDisplay.showComment()
+                else
+                    gameDisplay.showPieces()
+            }
+    }
+    Action {
+        id: actionComputerSettings
+
+        shortcut: "Ctrl+U"
+        //: Menu item Computer/Settings
+        text: qsTr("Settings")
+        onTriggered: computerDialog.open()
+    }
+    Action {
+        id: actionFindMove
+
+        shortcut: "Ctrl+H"
+        text: qsTr("Find Move")
+        enabled: ! gameModel.isGameOver
+        onTriggered: gameDisplay.showMove(gameModel.findMoveNext())
+    }
+    Action {
+        id: actionNextComment
+
+        shortcut: "Ctrl+E"
+        text: qsTr("Next Comment")
+        enabled: ! isRated && (gameModel.canGoForward || gameModel.canGoBackward)
+        onTriggered: Logic.findNextComment()
+    }
+    Action {
+        id: actionFullscreen
+
+        shortcut: "F11"
+        text: qsTr("Fullscreen")
+        checkable: true
+        checked: visibility === Window.FullScreen
+        onTriggered: {
+            if (visibility !== Window.FullScreen)
+                visibility = Window.FullScreen
+            else
+                visibility = Window.AutomaticVisibility
+        }
+    }
+    Action {
+        id: actionGameInfo
+
+        shortcut: "Ctrl+I"
+        text: qsTr("Game Info")
+        onTriggered: gameInfoDialog.open()
+    }
+    Action {
+        id: actionGotoMove
+
+        shortcut: "Ctrl+G"
+        text: qsTr("Move Number…")
+        enabled: ! isRated && (gameModel.moveNumber + gameModel.movesLeft >= 1)
+        onTriggered: gotoMoveDialog.open()
+    }
+    Action {
+        id: actionHelp
+
+        shortcut: "F1"
+        text: qsTr("Pentobi Help")
+        onTriggered: Logic.help()
+    }
+    Action {
+        id: actionNew
+
+        shortcut: "Ctrl+N"
+        text: qsTr("New")
+        enabled: gameDisplay.setupMode || gameModel.isModified
+                 || gameModel.file !== "" || isRated
+        onTriggered: Qt.callLater(function() { Logic.newGame() }) // QTBUG-69682
+    }
+    Action {
+        id: actionNewRated
+
+        shortcut: "Ctrl+Shift+N"
+        text: qsTr("Rated Game")
+        enabled: ! isRated
+        onTriggered: Logic.ratedGame()
+    }
+    Action {
+        id: actionOpen
+
+        shortcut: "Ctrl+O"
+        text: qsTr("Open…")
+        onTriggered: Logic.open()
+    }
+    Action {
+        id: actionPlay
+
+        shortcut: "Ctrl+L"
+        text: qsTr("Play")
+        enabled: ! gameModel.isGameOver && ! isRated
+        onTriggered: Logic.computerPlay()
+    }
+    Action {
+        id: actionPlaySingle
+
+        shortcut: "Ctrl+Shift+L"
+        //: Play a single move
+        text: qsTr("Play Move")
+        enabled: ! gameModel.isGameOver && ! isRated
+        onTriggered: { isPlaySingleMoveRunning = true; Logic.genMove() }
+    }
+    Action {
+        id: actionQuit
+
+        shortcut: "Ctrl+Q"
+        text: qsTr("Quit")
+        onTriggered: close()
+    }
+    Action {
+        id: actionSave
+
+        shortcut: "Ctrl+S"
+        text: qsTr("Save")
+        enabled: gameModel.isModified
+        onTriggered: if (gameModel.file !== "") Logic.save(); else Logic.saveAs()
+    }
+    Action {
+        id: actionSaveAs
+
+        shortcut: "Ctrl+Shift+S"
+        text: qsTr("Save As…")
+        enabled: gameModel.isModified || gameModel.file !== ""
+        onTriggered: Logic.saveAs()
+    }
+    Action {
+        id: actionStop
+
+        text: qsTr("Stop")
+        enabled: (playerModel.isGenMoveRunning
+                  || delayedCheckComputerMove.running
+                  || analyzeGameModel.isRunning)
+                 && ! isRated
+        onTriggered:
+            Qt.callLater(function() { Logic.cancelRunning(true) }) // QTBUG-69682
+    }
+    Action {
+        id: actionUndo
+
+        text: qsTr("Undo Move")
+        enabled: gameModel.canUndo && ! gameDisplay.setupMode && ! isRated
+        onTriggered: Qt.callLater(function() { Logic.undo() }) // QTBUG-69682
+    }
+    Instantiator {
+        model: [ "1", "2", "A", "C", "E", "F", "G", "H", "I", "J", "L",
+            "N", "O", "P", "S", "T", "U", "V", "W", "X", "Y", "Z" ]
+
+        Shortcut {
+            sequence: modelData
+            onActivated: Logic.pickNamedPiece(modelData)
+        }
+    }
+    Shortcut {
+        sequence: "Back"
+        enabled: isAndroid
+        onActivated: {
+            if (visibility === Window.FullScreen)
+                visibility = Window.AutomaticVisibility
+            else
+                close()
+        }
+    }
+    Shortcut {
+        sequence: "Return"
+        enabled: ! isAndroid
+        onActivated: gameDisplay.playPickedPiece()
+    }
+    Shortcut {
+        sequence: "Escape"
+        onActivated:
+            if (gameDisplay.pickedPiece)
+                gameDisplay.pickedPiece = null
+            else if (visibility === Window.FullScreen)
+                visibility = Window.AutomaticVisibility
+    }
+    Shortcut {
+        sequence: "Ctrl+Shift+H"
+        enabled: ! gameModel.isGameOver
+        onActivated: gameDisplay.showMove(gameModel.findMovePrevious())
+    }
+    Shortcut {
+        sequence: "Down"
+        onActivated: gameDisplay.shiftPiece(0, 1)
+    }
+    Shortcut {
+        sequence: "Shift+Down"
+        onActivated: gameDisplay.shiftPieceFast(0, 1)
+    }
+    Shortcut {
+        sequence: "Left"
+        onActivated: gameDisplay.shiftPiece(-1, 0)
+    }
+    Shortcut {
+        sequence: "Shift+Left"
+        onActivated: gameDisplay.shiftPieceFast(-1, 0)
+    }
+    Shortcut {
+        sequence: "Right"
+        onActivated: gameDisplay.shiftPiece(1, 0)
+    }
+    Shortcut {
+        sequence: "Shift+Right"
+        onActivated: gameDisplay.shiftPieceFast(1, 0)
+    }
+    Shortcut {
+        sequence: "Up"
+        onActivated: gameDisplay.shiftPiece(0, -1)
+    }
+    Shortcut {
+        sequence: "Shift+Up"
+        onActivated: gameDisplay.shiftPieceFast(0, -1)
+    }
+    Shortcut {
+        enabled: gameDisplay.pickedPiece
+        sequence: "Space"
+        onActivated: gameDisplay.pickedPiece.pieceModel.nextOrientation()
+    }
+    Shortcut {
+        sequence: "+"
+        onActivated: Logic.nextPiece()
+    }
+    Shortcut {
+        sequence: "Alt+M"
+        onActivated: toolBar.clickMenuButton()
+    }
+    Shortcut {
+        enabled: gameDisplay.pickedPiece
+        sequence: "Shift+Space"
+        onActivated: gameDisplay.pickedPiece.pieceModel.previousOrientation()
+    }
+    Shortcut {
+        sequence: "-"
+        onActivated: Logic.prevPiece()
+    }
+}
diff --git a/src/pentobi/qml/Menu.qml b/src/pentobi/qml/Menu.qml
new file mode 100644 (file)
index 0000000..df1ff7f
--- /dev/null
@@ -0,0 +1,84 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Menu.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import "Controls.js" as PentobiControls
+import "." as Pentobi
+
+Menu {
+    function addMnemonic(text, mnemonic) { return PentobiControls.addMnemonic(text, mnemonic) }
+
+    property bool dynamicWidth: isDesktop
+
+    width: {
+        if (! dynamicWidth)
+            return Math.min(font.pixelSize * 18, rootWindow.contentItem.width)
+        var maxWidth = 0
+        for (var i = 0; i < count; ++i)
+            maxWidth = Math.max(maxWidth, itemAt(i).implicitWidth)
+        return Math.min(maxWidth, rootWindow.contentItem.width)
+    }
+    cascade: isDesktop
+    closePolicy: isDesktop ?
+                     Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
+                   : Popup.CloseOnEscape | Popup.CloseOnPressOutside
+    delegate: Pentobi.MenuItem { }
+    background: Rectangle {
+        // Note that MenuItem in Qt 5.11 does neither fully use the system
+        // palette, nor make its actually used colors available in its own
+        // palette.
+        color: isDesktop ? palette.window : palette.base
+        border.color: palette.mid
+    }
+    // Workaround for QTBUG-69541 (Opened Menu highlights last used item on Android)
+    onOpened: if (isAndroid) currentIndex = -1
+    // Workaround for QTBUG-69540 (Menu highlights disabled item on click).
+    // Also part of workaround for QTBUG-70181, see Pentobi.MenuItem.Keys.onPressed
+    onCurrentIndexChanged: {
+        if (isAndroid || currentIndex < 0)
+            return
+        var i
+        for (i = currentIndex; i < count; ++i)
+            if (itemAt(i) instanceof MenuItem && itemAt(i).enabled) {
+                currentIndex = i
+                return
+            }
+        for (i = currentIndex - 1; i >= 0; --i)
+            if (itemAt(i) instanceof MenuItem && itemAt(i).enabled) {
+                currentIndex = i
+                return
+            }
+        currentIndex = -1
+    }
+    Component.onCompleted: {
+        // Sanity checks for mnemonics
+        if (! isDebug || ! isDesktop)
+            return
+        var allMnemonics = []
+        var i, j, text, pos, mnemonic, textWithoutMnemonic
+        for (i = 0; i < count; ++i) {
+            if (itemAt(i))
+                text = itemAt(i).text
+            else if (menuAt(i))
+                text = menuAt(i).title
+            if (! text)
+                continue
+            pos = text.indexOf("&")
+            if (pos < 0 || pos === text.length - 1) {
+                textWithoutMnemonic = text
+                continue
+            }
+            mnemonic = text.substr(pos + 1, 1).toLowerCase()
+            for (j = 0; j < allMnemonics.length; ++j)
+                if (allMnemonics[j] === mnemonic)
+                    console.warn("Duplicate mnemonic:", text)
+            allMnemonics.push(mnemonic)
+        }
+        if (allMnemonics.length > 0 && textWithoutMnemonic)
+            console.warn("No mnemonic:", textWithoutMnemonic)
+    }
+}
diff --git a/src/pentobi/qml/MenuComputer.qml b/src/pentobi/qml/MenuComputer.qml
new file mode 100644 (file)
index 0000000..44903f9
--- /dev/null
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuComputer.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "." as Pentobi
+
+Pentobi.Menu {
+    title: addMnemonic(qsTr("Computer"),
+                       //: Mnemonic for menu Computer. Leave empty for no mnemonic.
+                       qsTr("C"))
+
+    Pentobi.MenuItem {
+        action: actionComputerSettings
+        text: addMnemonic(action.text,
+                          //: Mnemonic for menu item Computer Settings. Leave empty for no mnemonic.
+                          qsTr("S"))
+    }
+    Pentobi.MenuItem {
+        action: actionPlay
+        text: addMnemonic(actionPlay.text,
+                          //: Mnemonic for menu item Play. Leave empty for no mnemonic.
+                          qsTr("P"))
+    }
+    Pentobi.MenuItem {
+        action: actionPlaySingle
+        text: addMnemonic(action.text,
+                          //: Mnemonic for menu item Play Move. Leave empty for no mnemonic.
+                          qsTr("M"))
+    }
+    Pentobi.MenuItem {
+        action: actionStop
+        text: addMnemonic(action.text,
+                          //: Mnemonic for menu item Stop. Leave empty for no mnemonic.
+                          qsTr("O"))
+    }
+}
diff --git a/src/pentobi/qml/MenuEdit.qml b/src/pentobi/qml/MenuEdit.qml
new file mode 100644 (file)
index 0000000..dfad4d5
--- /dev/null
@@ -0,0 +1,117 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuEdit.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Menu {
+    title: addMnemonic(qsTr("Edit"),
+                       //: Mnemonic for menu Edit. Leave empty for no mnemonic.
+                       qsTr("E"))
+
+    Pentobi.MenuItem {
+        text: addMnemonic(qsTr("Annotation…"),
+                          //: Mnemonic for menu item Annotation. Leave empty for no mnemonic.
+                          qsTr("A"))
+        enabled: gameModel.moveNumber > 0
+        onTriggered: {
+            var dialog = moveAnnotationDialog.get()
+            dialog.moveNumber = gameModel.moveNumber
+            moveAnnotationDialog.open()
+        }
+    }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        text: addMnemonic(qsTr("Make Main Variation"),
+                          //: Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic.
+                          qsTr("M"))
+        enabled: ! gameModel.isMainVar && ! isRated
+        onTriggered: {
+            gameModel.makeMainVar()
+            Logic.showTemporaryMessage(qsTr("Made main variation"))
+        }
+    }
+    Pentobi.MenuItem {
+        //: Short for Move Variation Up
+        text: addMnemonic(qsTr("Variation Up"),
+                          //: Mnemonic for menu item Variation Up. Leave empty for no mnemonic.
+                          qsTr("U"))
+        enabled: gameModel.hasPrevVar && ! isRated
+        onTriggered: Logic.moveUpVar()
+    }
+    Pentobi.MenuItem {
+        //: Short for Move Variation Down
+        text: addMnemonic(qsTr("Variation Down"),
+                          //: Mnemonic for menu item Variation Down. Leave empty for no mnemonic.
+                          qsTr("W"))
+        enabled: gameModel.hasNextVar && ! isRated
+        onTriggered: Logic.moveDownVar()
+    }
+    Pentobi.MenuItem {
+        text: addMnemonic(qsTr("Delete Variations"),
+                          //: Mnemonic for menu item Delete Variations. Leave empty for no mnemonic.
+                          qsTr("D"))
+        enabled: gameModel.hasVariations && ! isRated
+        onTriggered: Logic.deleteAllVar()
+    }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        text: addMnemonic(qsTr("Truncate"),
+                          //: Mnemonic for menu item Truncate. Leave empty for no mnemonic.
+                          qsTr("T"))
+        enabled: gameModel.canGoBackward && ! isRated
+        onTriggered: Logic.truncate()
+    }
+    Pentobi.MenuItem {
+        text: addMnemonic(qsTr("Truncate Children"),
+                          //: Mnemonic for menu item Truncate Children. Leave empty for no mnemonic.
+                          qsTr("C"))
+        enabled: gameModel.canGoForward && ! isRated
+        onTriggered: Logic.truncateChildren()
+    }
+    Pentobi.MenuItem {
+        text: addMnemonic(qsTr("Keep Position"),
+                          //: Mnemonic for menu item Keep Position. Leave empty for no mnemonic.
+                          qsTr("P"))
+        enabled: ! gameModel.isBoardEmpty && (gameModel.canGoBackward || gameModel.canGoForward) && ! isRated
+        onTriggered: Logic.keepOnlyPosition()
+    }
+    Pentobi.MenuItem {
+        text: addMnemonic(qsTr("Keep Subtree"),
+                          //: Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic.
+                          qsTr("S"))
+        enabled: gameModel.canGoBackward && gameModel.canGoForward && ! isRated
+        onTriggered: Logic.keepOnlySubtree()
+    }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        text: addMnemonic(qsTr("Setup Mode"),
+                          //: Mnemonic for menu item Setup Mode. Leave empty for no mnemonic.
+                          qsTr("O"))
+        checkable: true
+        enabled: ! gameModel.canGoBackward && ! gameModel.canGoForward
+                 && gameModel.moveNumber === 0 && ! isRated
+        checked: gameDisplay.setupMode
+        onTriggered: {
+            checked = ! gameDisplay.setupMode // Workaround for QTBUG-69401
+            gameDisplay.setupMode = checked
+            if (checked)
+                gameDisplay.showPieces()
+            else
+                Logic.setComputerNone()
+        }
+    }
+    Pentobi.MenuItem {
+        text: addMnemonic(qsTr("Next Color"),
+                          //: Mnemonic for menu item Next Color. Leave empty for no mnemonic.
+                          qsTr("N"))
+        enabled: ! isRated
+        onTriggered: {
+            gameDisplay.pickedPiece = null
+            gameModel.nextColor()
+        }
+    }
+}
diff --git a/src/pentobi/qml/MenuExport.qml b/src/pentobi/qml/MenuExport.qml
new file mode 100644 (file)
index 0000000..e479b24
--- /dev/null
@@ -0,0 +1,32 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuExport.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.3
+import "." as Pentobi
+
+Pentobi.Menu {
+    title: addMnemonic(qsTr("Export"),
+                       //: Mnemonic for menu Export. Leave empty for no mnemonic.
+                       qsTr("E"))
+
+    Action {
+        text: addMnemonic(qsTr("Image…"),
+                          //: Mnemonic for menu item Image. Leave empty for no mnemonic.
+                          qsTr("M"))
+        onTriggered: exportImageDialog.open()
+    }
+    Action {
+        text: addMnemonic(qsTr("ASCII Art…"),
+                          //: Mnemonic for menu item ASCII Art. Leave empty for no mnemonic.
+                          qsTr("A"))
+        onTriggered: {
+            var dialog = asciiArtSaveDialog.get()
+            dialog.name = gameModel.suggestFileName(folder, "txt")
+            dialog.selectNameFilter(0)
+            dialog.open()
+        }
+    }
+}
diff --git a/src/pentobi/qml/MenuGame.qml b/src/pentobi/qml/MenuGame.qml
new file mode 100644 (file)
index 0000000..21e78bb
--- /dev/null
@@ -0,0 +1,89 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuGame.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.3
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Menu {
+    title: addMnemonic(qsTr("Game"),
+                       //: Mnemonic for menu Game. Leave empty for no mnemonic.
+                       qsTr("G"))
+
+    Pentobi.MenuItem {
+        action: actionNew
+        text: addMnemonic(actionNew.text,
+                          //: Mnemonic for menu item New. Leave empty for no mnemonic.
+                          qsTr("N"))
+    }
+    Pentobi.MenuItem {
+        action: actionNewRated
+        text: addMnemonic(actionNewRated.text,
+                          //: Mnemonic for menu item Rated Game. Leave empty for no mnemonic.
+                          qsTr("R"))
+    }
+    Pentobi.MenuSeparator { }
+    Action {
+        text: addMnemonic(qsTr("Game Variant…"),
+                          //: Mnemonic for menu item Game Variant. Leave empty for no mnemonic.
+                          qsTr("V"))
+        onTriggered: gameVariantDialog.open()
+    }
+    Pentobi.MenuItem {
+        action: actionGameInfo
+        text: addMnemonic(action.text,
+                          //: Mnemonic for menu item Game Info. Leave empty for no mnemonic.
+                          qsTr("I"))
+    }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        action: actionUndo
+        text: addMnemonic(actionUndo.text,
+                          //: Mnemonic for menu item Undo. Leave empty for no mnemonic.
+                          qsTr("U"))
+    }
+    Pentobi.MenuItem {
+        action: actionFindMove
+        text: addMnemonic(action.text,
+                          //: Mnemonic for menu item Find Move. Leave empty for no mnemonic.
+                          qsTr("F"))
+    }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        action: actionOpen
+        text: addMnemonic(action.text,
+                          //: Mnemonic for menu item Open. Leave empty for no mnemonic.
+                          qsTr("O"))
+    }
+    MenuRecentFiles { }
+    Action {
+        text: addMnemonic(qsTr("Open Clipboard"),
+                          //: Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic.
+                          qsTr("C"))
+        onTriggered: Logic.openClipboard()
+    }
+    Pentobi.MenuItem {
+        action: actionSave
+        enabled: actionSave.enabled && gameModel.file !== ""
+        text: addMnemonic(action.text,
+                          //: Mnemonic for menu item Save. Leave empty for no mnemonic.
+                          qsTr("S"))
+    }
+    Pentobi.MenuItem {
+        action: actionSaveAs
+        text: addMnemonic(action.text,
+                          //: Mnemonic for menu item Save As. Leave empty for no mnemonic.
+                          qsTr("A"))
+    }
+    MenuExport { }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        action: actionQuit
+        text: addMnemonic(action.text,
+                          //: Mnemonic for menu item Quit. Leave empty for no mnemonic.
+                          qsTr("Q"))
+    }
+}
diff --git a/src/pentobi/qml/MenuGo.qml b/src/pentobi/qml/MenuGo.qml
new file mode 100644 (file)
index 0000000..42120a6
--- /dev/null
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuGo.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "." as Pentobi
+
+Pentobi.Menu {
+    title: addMnemonic(qsTr("Go"),
+                       //: Mnemonic for menu Go. Leave empty for no mnemonic.
+                       qsTr("O"))
+
+    Pentobi.MenuItem {
+        action: actionGotoMove
+        text: addMnemonic(action.text,
+                          //: Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic.
+                          qsTr("N"))
+    }
+    Pentobi.MenuItem {
+        action: actionBackToMainVar
+        text: addMnemonic(actionBackToMainVar.text,
+                          //: Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic.
+                          qsTr("M"))
+    }
+    Pentobi.MenuItem {
+        action: actionBeginningOfBranch
+        text: addMnemonic(actionBeginningOfBranch.text,
+                          //: Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic.
+                          qsTr("B"))
+    }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        action: actionNextComment
+        text: addMnemonic(action.text,
+                          //: Mnemonic for menu item Next Comment. Leave empty for no mnemonic.
+                          qsTr("C"))
+    }
+}
diff --git a/src/pentobi/qml/MenuHelp.qml b/src/pentobi/qml/MenuHelp.qml
new file mode 100644 (file)
index 0000000..a358331
--- /dev/null
@@ -0,0 +1,33 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuHelp.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.3
+import "." as Pentobi
+
+Pentobi.Menu {
+    title: addMnemonic(qsTr("Help"),
+                       //: Mnemonic for menu Help. Leave empty for no mnemonic.
+                       qsTr("H"))
+
+    Pentobi.MenuItem {
+        action: actionHelp
+        text: addMnemonic(actionHelp.text,
+                          //: Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic.
+                          qsTr("P"))
+    }
+    Action {
+        text: addMnemonic(qsTr("Report Bug"),
+                          //: Mnemonic for menu item Report Bug. Leave empty for no mnemonic.
+                          qsTr("B"))
+        onTriggered: Qt.openUrlExternally("https://sourceforge.net/p/pentobi/bugs/")
+    }
+    Action {
+        text: addMnemonic(qsTr("About Pentobi"),
+                          //: Mnemonic for menu item About Pentobi. Leave empty for no mnemonic.
+                          qsTr("A"))
+        onTriggered: aboutDialog.open()
+    }
+}
diff --git a/src/pentobi/qml/MenuItem.qml b/src/pentobi/qml/MenuItem.qml
new file mode 100644 (file)
index 0000000..1700fdf
--- /dev/null
@@ -0,0 +1,112 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuItem.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.0
+import QtQuick.Window 2.2
+import "Controls.js" as PentobiControls
+
+// Custom menu item that displays shortcuts (MenuItem in Qt 5.11 does not).
+MenuItem {
+    id: root
+
+    property string shortcut: action && action.shortcut ? action.shortcut : ""
+
+    function addMnemonic(text, mnemonic) {
+        return PentobiControls.addMnemonic(text, mnemonic)
+    }
+
+    property real _anyItemIndicatorWidth: {
+        if (menu)
+            for (var i = 0; i < menu.count; ++i)
+                if (menu.itemAt(i).checkable)
+                    return menu.itemAt(i).indicator.width
+        return 0
+    }
+    property real _anyItemArrowWidth: {
+        if (menu)
+            for (var i = 0; i < menu.count; ++i)
+                if (menu.menuAt(i))
+                    return menu.itemAt(i).arrow.width
+        return 0
+    }
+
+    // Qt 5.12.0 alpha doesn't set the width of menu items
+    width: menu.width
+    implicitHeight:
+        Math.round(font.pixelSize * (isDesktop ? 1.9 : 2.2)
+                   * Screen.devicePixelRatio) / Screen.devicePixelRatio
+    // Explicitly set hoverEnabled to true, otherwise hover highlighting and
+    // submenu opening doesn't work in KDE on Ubuntu 18.10 (bug in Qt?)
+    hoverEnabled: true
+    Keys.onPressed:
+        // Workaround for QTBUG-70181 (disabled items take part in arrow key
+        // navigation). Only handle Up key, the Down key case is already
+        // handled in Pentobi.Menu.onCurrentIndexChanged
+        if (event.key === Qt.Key_Up && menu) {
+            for (var i = menu.currentIndex - 1; i >= 0; --i)
+                if (menu.itemAt(i) instanceof MenuItem && menu.itemAt(i).enabled) {
+                    menu.currentIndex = i
+                    break
+                }
+            event.accepted = true
+        }
+    background: Rectangle {
+        color: {
+            if (! root.highlighted)
+                return "transparent"
+            // Note that MenuItem in Qt 5.11 does neither fully use the system
+            // palette, nor make its actually used colors available in its own
+            // palette.
+            return isDesktop ? palette.highlight : palette.midlight
+        }
+    }
+    contentItem: RowLayout {
+        Label {
+            id: labelText
+
+            text: {
+                if (! isDesktop)
+                    return root.text
+                var pos = root.text.indexOf("&")
+                if (pos < 0 || pos === root.text.length - 1)
+                    return root.text
+                return root.text.substring(0, pos) + "<u>"
+                        + root.text.substring(pos + 1, pos + 2)
+                        + "</u>" + root.text.substring(pos + 2)
+            }
+            color: {
+                // See comment at background
+                if (root.highlighted)
+                    return isDesktop ? palette.highlightedText : palette.buttonText
+                return palette.text
+            }
+            verticalAlignment: Text.AlignVCenter
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            Layout.leftMargin: 0.1 * font.pixelSize + _anyItemIndicatorWidth
+                               + 0.2 * font.pixelSize
+            Layout.rightMargin: 0.4 * font.pixelSize
+        }
+        Label {
+            visible: isDesktop && shortcut !== ""
+            text: {
+                var text = shortcut
+                //: Shortcut modifier key as displayed in menu item text (abbreviate if long)
+                text = text.replace("Ctrl", qsTr("Ctrl"))
+                //: Shortcut modifier key as displayed in menu item text (abbreviate if long)
+                return text.replace("Shift", qsTr("Shift"))
+            }
+            color: labelText.color
+            opacity: 0.6
+            verticalAlignment: Text.AlignVCenter
+            Layout.fillHeight: true
+            Layout.rightMargin: _anyItemArrowWidth > 0 ? _anyItemArrowWidth
+                                                       : 0.1 * font.pixelSize
+        }
+    }
+}
diff --git a/src/pentobi/qml/MenuRecentFiles.qml b/src/pentobi/qml/MenuRecentFiles.qml
new file mode 100644 (file)
index 0000000..6e673f2
--- /dev/null
@@ -0,0 +1,97 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuRecentFiles.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.4
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Menu {
+    title: addMnemonic(qsTr("Open Recent"),
+                       //: Mnemonic for menu Open Recent. Leave empty for no mnemonic.
+                       qsTr("P"))
+    enabled: gameModel.recentFiles.length > 0
+
+    function getText(recentFiles, index) {
+        if (index >= recentFiles.length)
+            return ""
+        var text = recentFiles[index]
+        text = text.substring(text.lastIndexOf("/") + 1)
+        if (isDesktop)
+            //: Format in recent files menu. First argument is the
+            //: file number, second argument the file name.
+            text = addMnemonic(qsTr("%1. %2").arg(index + 1).arg(text),
+                               (index + 1).toString())
+        return text
+    }
+
+    // Instantiator in Menu doesn't work reliably with Qt 5.11 or 5.12.0 alpha
+    Pentobi.MenuItem {
+        visible: gameModel.recentFiles.length > 0
+        // Invisible menu item still use space in Qt 5.11
+        height: visible ? implicitHeight : 0
+        text: getText(gameModel.recentFiles, 0)
+        onTriggered: Logic.openRecentFile(gameModel.recentFiles[0])
+    }
+    Pentobi.MenuItem {
+        visible: gameModel.recentFiles.length > 1
+        height: visible ? implicitHeight : 0
+        text: getText(gameModel.recentFiles, 1)
+        onTriggered: Logic.openRecentFile(gameModel.recentFiles[1])
+    }
+    Pentobi.MenuItem {
+        visible: gameModel.recentFiles.length > 2
+        height: visible ? implicitHeight : 0
+        text: getText(gameModel.recentFiles, 2)
+        onTriggered: Logic.openRecentFile(gameModel.recentFiles[2])
+    }
+    Pentobi.MenuItem {
+        visible: gameModel.recentFiles.length > 3
+        height: visible ? implicitHeight : 0
+        text: getText(gameModel.recentFiles, 3)
+        onTriggered: Logic.openRecentFile(gameModel.recentFiles[3])
+    }
+    Pentobi.MenuItem {
+        visible: gameModel.recentFiles.length > 4
+        height: visible ? implicitHeight : 0
+        text: getText(gameModel.recentFiles, 4)
+        onTriggered: Logic.openRecentFile(gameModel.recentFiles[4])
+    }
+    Pentobi.MenuItem {
+        visible: gameModel.recentFiles.length > 5
+        height: visible ? implicitHeight : 0
+        text: getText(gameModel.recentFiles, 5)
+        onTriggered: Logic.openRecentFile(gameModel.recentFiles[5])
+    }
+    Pentobi.MenuItem {
+        visible: gameModel.recentFiles.length > 6
+        height: visible ? implicitHeight : 0
+        text: getText(gameModel.recentFiles, 6)
+        onTriggered: Logic.openRecentFile(gameModel.recentFiles[6])
+    }
+    Pentobi.MenuItem {
+        visible: gameModel.recentFiles.length > 7
+        height: visible ? implicitHeight : 0
+        text: getText(gameModel.recentFiles, 7)
+        onTriggered: Logic.openRecentFile(gameModel.recentFiles[7])
+    }
+    Pentobi.MenuItem {
+        visible: gameModel.recentFiles.length > 8
+        height: visible ? implicitHeight : 0
+        text: getText(gameModel.recentFiles, 8)
+        onTriggered: Logic.openRecentFile(gameModel.recentFiles[8])
+    }
+
+    Pentobi.MenuSeparator { }
+    Action {
+        //: Menu item for clearing the recent files list
+        text: addMnemonic(qsTr("Clear List"),
+                          //: Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic.
+                          qsTr("C"))
+        onTriggered: Qt.callLater(function() { // QTBUG-69682
+            gameModel.clearRecentFiles()
+        })
+    }
+}
diff --git a/src/pentobi/qml/MenuSeparator.qml b/src/pentobi/qml/MenuSeparator.qml
new file mode 100644 (file)
index 0000000..afb4029
--- /dev/null
@@ -0,0 +1,16 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuSeparator.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.3
+
+MenuSeparator {
+    // Qt 5.12.0 alpha doesn't set the width of menu items
+    width: parent.width
+    // Default implicitWidth is too large. Since we either use fixed-width
+    // menus or compute the menu width from the maximum implicitWidth of the
+    // items, we set implicitWidth of the separators to 0
+    implicitWidth: 0
+}
diff --git a/src/pentobi/qml/MenuTools.qml b/src/pentobi/qml/MenuTools.qml
new file mode 100644 (file)
index 0000000..14d2739
--- /dev/null
@@ -0,0 +1,55 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuTools.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.3
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Menu {
+    title: addMnemonic(qsTr("Tools"),
+                       //: Mnemonic for menu Tools. Leave empty for no mnemonic.
+                       qsTr("T"))
+
+    Pentobi.MenuItem {
+        text: addMnemonic(qsTr("Rating"),
+                          //: Mnemonic for menu item Rating. Leave empty for no mnemonic.
+                          qsTr("R"))
+        onTriggered: Logic.rating()
+    }
+    Action {
+        enabled: ! isRated && ratingModel.numberGames > 0
+        text: addMnemonic(qsTr("Clear Rating"),
+                          //: Mnemonic for menu item Clear Rating. Leave empty for no mnemonic.
+                          qsTr("C"))
+        onTriggered: Logic.clearRating()
+    }
+    Pentobi.MenuSeparator { }
+    Action {
+        enabled: ! isRated && (gameModel.canGoBackward || gameModel.canGoForward)
+        // Text needs to end with ellipsis on desktop because it opens a
+        // dialog asking for analysis speed, but not on Android
+        text: addMnemonic(isAndroid ? qsTr("Analyze Game") : qsTr("Analyze Game…"),
+                          //: Mnemonic for menu item Analyze Game. Leave empty for no mnemonic.
+                          qsTr("A"))
+        onTriggered: {
+            if (isAndroid)
+                Logic.analyzeGame(3000)
+            else
+                analyzeDialog.open()
+        }
+    }
+    Action {
+        enabled: analyzeGameModel.elements.length !== 0
+        text: addMnemonic(qsTr("Clear Analysis"),
+                          //: Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic.
+                          qsTr("E"))
+        onTriggered:
+            Qt.callLater(function() { // QTBUG-69682
+                analyzeGameModel.clear()
+                gameDisplay.deleteAnalysis()
+            })
+    }
+}
diff --git a/src/pentobi/qml/MenuView.qml b/src/pentobi/qml/MenuView.qml
new file mode 100644 (file)
index 0000000..6c786e8
--- /dev/null
@@ -0,0 +1,45 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuView.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.3
+import QtQuick.Window 2.1
+import "." as Pentobi
+
+Pentobi.Menu {
+    title: addMnemonic(qsTr("View"),
+                       //: Mnemonic for menu View. Leave empty for no mnemonic.
+                       qsTr("V"))
+
+    Action {
+        text: addMnemonic(qsTr("Appearance"),
+                          //: Mnemonic for menu Appearance. Leave empty for no mnemonic.
+                          qsTr("A"))
+        onTriggered: appearanceDialog.open()
+    }
+    Pentobi.MenuItem {
+        visible: isDesktop
+        // Invisible menu item still use space in Qt 5.11
+        height: visible ? implicitHeight : 0
+        text: addMnemonic(qsTr("Toolbar"),
+                          //: Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic.
+                          qsTr("T"))
+        checkable: true
+        checked: rootWindow.showToolBar
+        onTriggered: rootWindow.showToolBar = checked
+    }
+    Pentobi.MenuItem {
+        action: actionComment
+        text: addMnemonic(actionComment.text,
+                          //: Mnemonic for menu item View/Comment. Leave empty for no mnemonic.
+                          qsTr("C"))
+    }
+    Pentobi.MenuItem {
+        action: actionFullscreen
+        text: addMnemonic(action.text,
+                          //: Mnemonic for menu item Fullscreen. Leave empty for no mnemonic.
+                          qsTr("F"))
+    }
+}
diff --git a/src/pentobi/qml/MessageDialog.qml b/src/pentobi/qml/MessageDialog.qml
new file mode 100644 (file)
index 0000000..a2efcf3
--- /dev/null
@@ -0,0 +1,33 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MessageDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Dialog {
+    id: root
+
+    property alias text: label.text
+
+    footer: Pentobi.DialogButtonBox { ButtonOk { } }
+
+    Item {
+        implicitWidth:
+            Math.max(Math.min(label.implicitWidth,
+                              font.pixelSize * 25, maxContentWidth),
+                     font.pixelSize * 15, minContentWidth)
+        implicitHeight: label.implicitHeight
+
+        Label {
+            id: label
+
+            anchors.fill: parent
+            wrapMode: Text.Wrap
+        }
+    }
+}
diff --git a/src/pentobi/qml/MoveAnnotationDialog.qml b/src/pentobi/qml/MoveAnnotationDialog.qml
new file mode 100644 (file)
index 0000000..cebe897
--- /dev/null
@@ -0,0 +1,79 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MoveAnnotationDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.2
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Dialog {
+    id: root
+
+    property int moveNumber
+
+    footer: DialogButtonBoxOkCancel { }
+    onOpened: {
+        var annotation = gameModel.getMoveAnnotation(moveNumber)
+        if (annotation === "")
+            comboBox.currentIndex = 0
+        else if (annotation === "‼")
+            comboBox.currentIndex = 1
+        else if (annotation === "!")
+            comboBox.currentIndex = 2
+        else if (annotation === "⁉")
+            comboBox.currentIndex = 3
+        else if (annotation === "⁈")
+            comboBox.currentIndex = 4
+        else if (annotation === "?")
+            comboBox.currentIndex = 5
+        else if (annotation === "⁇")
+            comboBox.currentIndex = 6
+    }
+    onAccepted: {
+        var annotation
+        switch (comboBox.currentIndex) {
+        case 0: annotation = ""; break
+        case 1: annotation = "‼"; break
+        case 2: annotation = "!"; break
+        case 3: annotation = "⁉"; break
+        case 4: annotation = "⁈"; break
+        case 5: annotation = "?"; break
+        case 6: annotation = "⁇"; break
+        }
+        gameModel.setMoveAnnotation(moveNumber, annotation)
+    }
+
+    Item {
+        implicitWidth:
+            Math.max(Math.min(columnLayout.implicitWidth, maxContentWidth),
+                     minContentWidth)
+        implicitHeight: columnLayout.implicitHeight
+
+        ColumnLayout {
+            id: columnLayout
+
+            anchors.fill: parent
+
+            Label { text: qsTr("Move %1").arg(moveNumber) }
+            ComboBox {
+                id: comboBox
+
+                model: [
+                    qsTr("No annotation"),
+                    qsTr("Very good"),
+                    qsTr("Good"),
+                    qsTr("Interesting"),
+                    qsTr("Doubtful"),
+                    qsTr("Bad"),
+                    qsTr("Very Bad")
+                ]
+                Layout.preferredWidth: 15 * font.pixelSize
+                Layout.fillWidth: true
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/NavigationButtons.qml b/src/pentobi/qml/NavigationButtons.qml
new file mode 100644 (file)
index 0000000..121700b
--- /dev/null
@@ -0,0 +1,55 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/NavigationButtons.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.0
+import "." as Pentobi
+
+RowLayout
+{
+    spacing: 0
+
+    Pentobi.Button {
+        id: buttonBeginning
+
+        icon.source: theme.getImage("pentobi-beginning")
+        action: actionBeginning
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+    }
+    Pentobi.Button {
+        icon.source: theme.getImage("pentobi-backward")
+        action: actionBackward
+        autoRepeat: true
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+    }
+    Pentobi.Button {
+        icon.source: theme.getImage("pentobi-forward")
+        action: actionForward
+        autoRepeat: true
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+    }
+    Pentobi.Button {
+        icon.source: theme.getImage("pentobi-end")
+        action: actionEnd
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+    }
+    Pentobi.Button {
+        icon.source: theme.getImage("pentobi-previous-variation")
+        action: actionPrevVar
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+    }
+    Pentobi.Button {
+        icon.source: theme.getImage("pentobi-next-variation")
+        action: actionNextVar
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+    }
+}
diff --git a/src/pentobi/qml/NavigationPanel.qml b/src/pentobi/qml/NavigationPanel.qml
new file mode 100644 (file)
index 0000000..e6f7a88
--- /dev/null
@@ -0,0 +1,32 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/NavigationPanel.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.0
+import QtQuick.Controls 2.2
+
+ColumnLayout {
+    id: root
+
+    function dropCommentFocus() { comment.dropFocus() }
+
+    Comment {
+        id: comment
+
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+    }
+    Label {
+        text: gameModel.positionInfo
+        color: theme.colorText
+        Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+    }
+    NavigationButtons {
+        Layout.fillWidth: true
+        Layout.maximumHeight:
+            Math.min(50, 0.08 * rootWindow.contentItem.height, root.width / 6)
+    }
+}
diff --git a/src/pentobi/qml/NewFolderDialog.qml b/src/pentobi/qml/NewFolderDialog.qml
new file mode 100644 (file)
index 0000000..31c0b3a
--- /dev/null
@@ -0,0 +1,71 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/NewFolderDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.0
+import QtQuick.Controls 2.2
+import "." as Pentobi
+import "Main.js" as Logic
+
+Pentobi.Dialog {
+    id: root
+
+    property url folder
+    property alias name: textField.text
+
+    function returnPressed() {
+        if (! hasButtonFocus())
+            checkAccept()
+    }
+    function checkAccept() {
+        if (! isValidName(name))
+            return
+        if (! gameModel.createFolder(folder + "/" + name)) {
+            Logic.showInfo(gameModel.getError())
+            return
+        }
+        accept()
+    }
+
+    function isValidName(name) { return name.trim().length > 0 }
+
+    footer: Pentobi.DialogButtonBox {
+        ButtonOk {
+            enabled: isValidName(name)
+            onClicked: checkAccept()
+            DialogButtonBox.buttonRole: DialogButtonBox.InvalidRole
+        }
+        ButtonCancel { }
+    }
+    onOpened: {
+        name = gameModel.suggestNewFolderName(folder)
+        textField.selectAll()
+    }
+
+    Item {
+        implicitWidth:
+            Math.max(Math.min(rowLayout.implicitWidth, maxContentWidth),
+                     minContentWidth)
+        implicitHeight: rowLayout.implicitHeight
+
+        ColumnLayout {
+            id: rowLayout
+
+            anchors.fill: parent
+
+            Label { text: qsTr("Folder name:") }
+            TextField {
+                id: textField
+
+                focus: true
+                selectByMouse: true
+                onAccepted: checkAccept()
+                Layout.fillWidth: true
+            }
+            Item { Layout.fillWidth: true }
+        }
+    }
+}
diff --git a/src/pentobi/qml/OpenDialog.qml b/src/pentobi/qml/OpenDialog.qml
new file mode 100644 (file)
index 0000000..b6971cc
--- /dev/null
@@ -0,0 +1,21 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/OpenDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.FileDialog {
+    title: qsTr("Open")
+    nameFilterLabels: [ qsTr("Blokus games") ]
+    nameFilters: [ [ "*.blksgf", "*.BLKSGF" ] ]
+    folder: rootWindow.folder
+    onOpened: name = ""
+    onAccepted: {
+        rootWindow.folder = folder
+        Logic.openFileUrl()
+    }
+}
diff --git a/src/pentobi/qml/PieceCallisto.qml b/src/pentobi/qml/PieceCallisto.qml
new file mode 100644 (file)
index 0000000..1052c9f
--- /dev/null
@@ -0,0 +1,318 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceCallisto.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+import QtQuick.Window 2.2
+
+// See PieceClassic.qml for comments
+Item
+{
+    id: root
+
+    property QtObject pieceModel
+    property var color:
+        switch (pieceModel.color) {
+        case 0: return color0
+        case 1: return color1
+        case 2: return color2
+        case 3: return color3
+        }
+    property Item parentUnplayed
+    property string imageName:
+        "image://pentobi/" +
+        (pieceModel.elements.length === 1 ? "frame" : "square") +
+        "/" + color[0] + "/" + color[1] + "/" + color[2]
+    // Avoid fractional sizes for square piece elements
+    property real scaleUnplayed:
+        parentUnplayed ?
+            Math.floor(0.25 * parentUnplayed.width) / board.gridWidth : 0
+    // We  only use flipX.angle [0..360]
+    property bool flippedX: Math.abs(flipX.angle - 180) < 90
+    // We  only use flipY.angle [0..180]
+    property bool flippedY: flipY.angle > 90
+    property real pieceAngle: {
+        if (! flippedY && ! flippedX) return rotation
+        if (! flippedY && flippedX) return rotation + 90
+        if (flippedX) return rotation + 180
+        return rotation + 270
+    }
+    property real isSmall: scale < 0.5 ? 1 : 0
+    property real imageOpacity0: imageOpacity(pieceAngle, 0) * (1 - isSmall)
+    property real imageOpacity90: imageOpacity(pieceAngle, 90) * (1 - isSmall)
+    property real imageOpacity180: imageOpacity(pieceAngle, 180) * (1 - isSmall)
+    property real imageOpacity270: imageOpacity(pieceAngle, 270) * (1 - isSmall)
+    property real imageOpacitySmall0: imageOpacity(pieceAngle, 0) * isSmall
+    property real imageOpacitySmall90: imageOpacity(pieceAngle, 90) * isSmall
+    property real imageOpacitySmall180: imageOpacity(pieceAngle, 180) * isSmall
+    property real imageOpacitySmall270: imageOpacity(pieceAngle, 270) * isSmall
+
+    transform: [
+        Rotation {
+            id: flipX
+
+            axis { x: 1; y: 0; z: 0 }
+        },
+        Rotation {
+            id: flipY
+
+            axis { x: 0; y: 1; z: 0 }
+        }
+    ]
+
+    function imageOpacity(pieceAngle, imgAngle) {
+        var angle = (pieceAngle - imgAngle + 360) % 360
+        return angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180)
+    }
+
+    Repeater {
+        model: pieceModel.elements
+
+        Item {
+            // Right junction
+            Rectangle {
+                visible: pieceModel.junctionType[index] === 0
+                         || pieceModel.junctionType[index] === 1
+                color: root.color[0]
+                width: board.gridWidth - square.width
+                height: 0.8 * board.gridHeight
+                x: (modelData.x - pieceModel.center.x + 1) * board.gridWidth
+                   - width / 2
+                y: (modelData.y - pieceModel.center.y) * board.gridHeight
+                   + (board.gridHeight - height) / 2
+                antialiasing: true
+            }
+            // Down junction
+            Rectangle {
+                visible: pieceModel.junctionType[index] === 0
+                         || pieceModel.junctionType[index] === 2
+                color: root.color[0]
+                width: 0.8 * board.gridWidth
+                height: board.gridHeight - square.height
+                x: (modelData.x - pieceModel.center.x) * board.gridWidth
+                   + (board.gridWidth - width) / 2
+                y: (modelData.y - pieceModel.center.y + 1) * board.gridHeight
+                   - height / 2
+                antialiasing: true
+            }
+            Square {
+                id: square
+
+                // Avoid fractional piece element size
+                width:
+                    Math.round(0.9 * board.gridWidth * Screen.devicePixelRatio)
+                    / Screen.devicePixelRatio
+                height:
+                    Math.round(0.9 * board.gridHeight * Screen.devicePixelRatio)
+                    / Screen.devicePixelRatio
+                x: (modelData.x - pieceModel.center.x) * board.gridWidth
+                   + (board.gridWidth - width) / 2
+                y: (modelData.y - pieceModel.center.y) * board.gridHeight
+                   + (board.gridHeight - height) / 2
+            }
+        }
+    }
+    Rectangle {
+        opacity: moveMarking == "last_dot" && pieceModel.isLastMove ? 0.5 : 0
+        color: gameModel.showVariations && ! gameModel.isMainVar ? "transparent" : border.color
+        border { width: 0.2 * width; color: root.color[3] }
+        width: 0.3 * board.gridHeight
+        height: width
+        radius: width / 2
+        x: pieceModel.labelPos.x * board.gridWidth - width / 2
+        y: pieceModel.labelPos.y * board.gridHeight - height / 2
+        Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+    }
+    Loader {
+        sourceComponent: moveMarking === "all_number"
+                         || moveMarking === "last_number" || item ?
+                             textComponent : null
+
+        Component {
+            id: textComponent
+
+            Text {
+                text: moveMarking == "all_number"
+                      || (moveMarking == "last_number"
+                          && pieceModel.isLastMove) ?
+                          pieceModel.moveLabel : ""
+                opacity: text === "" ? 0 : 1
+                color: root.color[3]
+                width: board.gridWidth
+                height: board.gridHeight
+                fontSizeMode: Text.Fit
+                font {
+                    pixelSize: 0.5 * board.gridHeight
+                    preferShaping: false
+                }
+                verticalAlignment: Text.AlignVCenter
+                horizontalAlignment: Text.AlignHCenter
+                minimumPixelSize: 5
+                x: pieceModel.labelPos.x * board.gridWidth - width / 2
+                y: pieceModel.labelPos.y * board.gridHeight - height / 2
+                transform: [
+                    Rotation {
+                        origin {
+                            x: board.gridWidth / 2; y: board.gridHeight / 2
+                        }
+                        axis { x: 0; y: 1; z: 0 }
+                        angle: flippedY ? -180 : 0
+                    },
+                    Rotation {
+                        origin {
+                            x: board.gridWidth / 2; y: board.gridHeight / 2
+                        }
+                        axis { x: 1; y: 0; z: 0 }
+                        angle: flippedX ? -180 : 0
+                    },
+                    Rotation {
+                        origin {
+                            x: board.gridWidth / 2; y: board.gridHeight / 2
+                        }
+                        angle: -root.rotation
+                    }
+                ]
+                Behavior on opacity {
+                    NumberAnimation { duration: animationDurationFast }
+                }
+            }
+        }
+    }
+    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: ",rot180Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot90,rot270Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot180,flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot270,rot90Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                enabled: enableAnimations
+
+                PieceRotationAnimation { }
+                PieceFlipAnimation { target: flipX }
+            }
+        ]
+    }
+
+    states: [
+        State {
+            name: "picked"
+            when: root === pickedPiece
+
+            ParentChange {
+                target: root
+                parent: pieceManipulator
+                x: pieceManipulator.width / 2
+                y: pieceManipulator.height / 2
+            }
+        },
+        State {
+            name: "played"
+            when: pieceModel.isPlayed
+
+            ParentChange {
+                target: root
+                parent: board.grabImageTarget
+                x: board.mapFromGameX(pieceModel.gameCoord.x) - board.grabImageTarget.x
+                y: board.mapFromGameY(pieceModel.gameCoord.y) - board.grabImageTarget.y
+            }
+        },
+        State {
+            name: "unplayed"
+            when: parentUnplayed != null
+
+            PropertyChanges {
+                target: root
+                scale: scaleUnplayed
+            }
+            ParentChange {
+                target: root
+                parent: parentUnplayed
+                x: parentUnplayed.width / 2
+                y: parentUnplayed.height / 2
+            }
+        }
+    ]
+    transitions:
+        Transition {
+        from: "unplayed,picked,played"; to: from
+        enabled: enableAnimations
+
+        SequentialAnimation {
+            PropertyAction {
+                target: parentUnplayed.parent
+                property: "z"; value: 1
+            }
+            ParentAnimation {
+                via: isDesktop ? null : gameDisplay
+
+                NumberAnimation {
+                    properties: "x,y,scale"
+                    duration: animationDurationMove
+                    easing.type: Easing.InOutSine
+                }
+            }
+            PropertyAction {
+                target: parentUnplayed.parent
+                property: "z"; value: 0
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/PieceClassic.qml b/src/pentobi/qml/PieceClassic.qml
new file mode 100644 (file)
index 0000000..eca0acb
--- /dev/null
@@ -0,0 +1,275 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceClassic.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Item
+{
+    id: root
+
+    property QtObject pieceModel
+    property var color:
+        switch (pieceModel.color) {
+        case 0: return color0
+        case 1: return color1
+        case 2: return color2
+        case 3: return color3
+        }
+    property Item parentUnplayed
+    property string imageName:
+        "image://pentobi/square/" + color[0] + "/" + color[1] + "/" + color[2]
+    // Avoid fractional sizes for square piece elements
+    property real scaleUnplayed:
+        parentUnplayed ?
+            Math.floor(0.19 * parentUnplayed.width) / board.gridWidth : 0
+    property bool flippedX: Math.abs(flipX.angle - 180) < 90
+    property bool flippedY: flipY.angle > 90
+    property real pieceAngle: {
+        if (! flippedY && ! flippedX) return rotation
+        if (! flippedY && flippedX) return rotation + 90
+        if (flippedX) return rotation + 180
+        return rotation + 270
+    }
+    property real isSmall: scale < 0.5 ? 1 : 0
+    property real imageOpacity0: imageOpacity(pieceAngle, 0) * (1 - isSmall)
+    property real imageOpacity90: imageOpacity(pieceAngle, 90) * (1 - isSmall)
+    property real imageOpacity180: imageOpacity(pieceAngle, 180) * (1 - isSmall)
+    property real imageOpacity270: imageOpacity(pieceAngle, 270) * (1 - isSmall)
+    property real imageOpacitySmall0: imageOpacity(pieceAngle, 0) * isSmall
+    property real imageOpacitySmall90: imageOpacity(pieceAngle, 90) * isSmall
+    property real imageOpacitySmall180: imageOpacity(pieceAngle, 180) * isSmall
+    property real imageOpacitySmall270: imageOpacity(pieceAngle, 270) * isSmall
+
+    transform: [
+        Rotation {
+            id: flipX
+
+            axis { x: 1; y: 0; z: 0 }
+        },
+        Rotation {
+            id: flipY
+
+            axis { x: 0; y: 1; z: 0 }
+        }
+    ]
+
+    function imageOpacity(pieceAngle, imgAngle) {
+        var angle = (pieceAngle - imgAngle + 360) % 360
+        return angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180)
+    }
+
+    Repeater {
+        model: pieceModel.elements
+
+        Square {
+            x: (modelData.x - pieceModel.center.x) * board.gridWidth
+            y: (modelData.y - pieceModel.center.y) * board.gridHeight
+        }
+    }
+    Rectangle {
+        opacity: moveMarking == "last_dot" && pieceModel.isLastMove ? 0.5 : 0
+        color: gameModel.showVariations && ! gameModel.isMainVar ? "transparent" : border.color
+        border { width: 0.2 * width; color: root.color[3] }
+        width: 0.3 * board.gridHeight
+        height: width
+        radius: width / 2
+        x: pieceModel.labelPos.x * board.gridWidth - width / 2
+        y: pieceModel.labelPos.y * board.gridHeight - height / 2
+        Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+    }
+    Loader {
+        sourceComponent: moveMarking === "all_number"
+                         || moveMarking === "last_number" || item ?
+                             textComponent : null
+
+        Component {
+            id: textComponent
+
+            Text {
+                text: moveMarking == "all_number"
+                      || (moveMarking == "last_number"
+                          && pieceModel.isLastMove) ?
+                          pieceModel.moveLabel : ""
+                opacity: text === "" ? 0 : 1
+                color: root.color[3]
+                width: board.gridWidth
+                height: board.gridHeight
+                fontSizeMode: Text.Fit
+                font {
+                    pixelSize: 0.5 * board.gridHeight
+                    preferShaping: false
+                }
+                minimumPixelSize: 5
+                verticalAlignment: Text.AlignVCenter
+                horizontalAlignment: Text.AlignHCenter
+                x: pieceModel.labelPos.x * board.gridWidth - width / 2
+                y: pieceModel.labelPos.y * board.gridHeight - height / 2
+                transform: [
+                    Rotation {
+                        origin {
+                            x: board.gridWidth / 2; y: board.gridHeight / 2
+                        }
+                        axis { x: 0; y: 1; z: 0 }
+                        angle: flippedY ? -180 : 0
+                    },
+                    Rotation {
+                        origin {
+                            x: board.gridWidth / 2; y: board.gridHeight / 2
+                        }
+                        axis { x: 1; y: 0; z: 0 }
+                        angle: flippedX ? -180 : 0
+                    },
+                    Rotation {
+                        origin {
+                            x: board.gridWidth / 2; y: board.gridHeight / 2
+                        }
+                        angle: -root.rotation
+                    }
+                ]
+                Behavior on opacity {
+                    NumberAnimation { duration: animationDurationFast }
+                }
+            }
+        }
+    }
+    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: ",rot180Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot90,rot270Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot180,flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot270,rot90Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                enabled: enableAnimations
+
+                PieceRotationAnimation { }
+                PieceFlipAnimation { target: flipX }
+            }
+        ]
+    }
+
+    states: [
+        State {
+            name: "picked"
+            when: root === pickedPiece
+
+            ParentChange {
+                target: root
+                parent: pieceManipulator
+                x: pieceManipulator.width / 2
+                y: pieceManipulator.height / 2
+            }
+        },
+        State {
+            name: "played"
+            when: pieceModel.isPlayed
+
+            ParentChange {
+                target: root
+                parent: board.grabImageTarget
+                x: board.mapFromGameX(pieceModel.gameCoord.x) - board.grabImageTarget.x
+                y: board.mapFromGameY(pieceModel.gameCoord.y) - board.grabImageTarget.y
+            }
+        },
+        State {
+            name: "unplayed"
+            when: parentUnplayed != null
+
+            PropertyChanges {
+                target: root
+                scale: scaleUnplayed
+            }
+            ParentChange {
+                target: root
+                parent: parentUnplayed
+                x: parentUnplayed.width / 2
+                y: parentUnplayed.height / 2
+            }
+        }
+    ]
+    transitions:
+        Transition {
+            from: "unplayed,picked,played"; to: from
+            enabled: enableAnimations
+
+            SequentialAnimation {
+                // Avoid piece being overlapped by pieces of different color
+                // during ParentAnimation
+                PropertyAction {
+                    target: parentUnplayed.parent
+                    property: "z"; value: 1
+                }
+                ParentAnimation {
+                    via: isDesktop ? null : gameDisplay
+
+                    NumberAnimation {
+                        properties: "x,y,scale"
+                        duration: animationDurationMove
+                        easing.type: Easing.InOutSine
+                    }
+                }
+                PropertyAction {
+                    target: parentUnplayed.parent
+                    property: "z"; value: 0
+                }
+            }
+    }
+}
diff --git a/src/pentobi/qml/PieceFlipAnimation.qml b/src/pentobi/qml/PieceFlipAnimation.qml
new file mode 100644 (file)
index 0000000..1935dd6
--- /dev/null
@@ -0,0 +1,13 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceFlipAnimation.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+RotationAnimation {
+    duration: animationDurationMove
+    direction: RotationAnimation.Shortest
+    property: "angle"
+}
diff --git a/src/pentobi/qml/PieceGembloQ.qml b/src/pentobi/qml/PieceGembloQ.qml
new file mode 100644 (file)
index 0000000..90ea1bb
--- /dev/null
@@ -0,0 +1,276 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceGembloQ.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+
+// Piece for GembloQ. See PieceClassic for comments.
+Item
+{
+    id: root
+
+    property QtObject pieceModel
+    property var color:
+        switch (pieceModel.color) {
+        case 0: return color0
+        case 1: return color1
+        case 2: return color2
+        case 3: return color3
+        }
+    property Item parentUnplayed
+    property string imageName:
+        "image://pentobi/quarter-square/" + color[0] + "/" + color[1] + "/" +
+    color[2]
+    // Avoid fractional sizes for square piece elements
+    property real scaleUnplayed:
+        parentUnplayed ? Math.floor(0.08 * 2 * parentUnplayed.width)
+                         / (2 * board.gridWidth) : 0
+    property string imageNameBottom:
+        "image://pentobi/quarter-square-bottom/" + color[0] + "/" + color[1] +
+    "/" + color[2]
+    property bool flippedX: Math.abs(flipX.angle - 180) < 90
+    property real pieceAngle: flippedX ? rotation + 180 : rotation
+    property real isSmall: scale < 0.5 ? 1 : 0
+    property real imageOpacity0: imageOpacity(pieceAngle, 0) * (1 - isSmall)
+    property real imageOpacity90: imageOpacity(pieceAngle, 90) * (1 - isSmall)
+    property real imageOpacity180: imageOpacity(pieceAngle, 180) * (1 - isSmall)
+    property real imageOpacity270: imageOpacity(pieceAngle, 270) * (1 - isSmall)
+    property real imageOpacitySmall0: imageOpacity(pieceAngle, 0) * isSmall
+    property real imageOpacitySmall90: imageOpacity(pieceAngle, 90) * isSmall
+    property real imageOpacitySmall180: imageOpacity(pieceAngle, 180) * isSmall
+    property real imageOpacitySmall270: imageOpacity(pieceAngle, 270) * isSmall
+
+    function imageOpacity(pieceAngle, imgAngle) {
+        var angle = (pieceAngle - imgAngle + 360) % 360
+        if (angle <= 90) return 0
+        if (angle <= 180) return -Math.cos(angle * Math.PI / 180)
+        if (angle <= 270) return 1
+        return -Math.sin(angle * Math.PI / 180)
+    }
+
+    transform: [
+        Rotation {
+            id: flipX
+
+            axis { x: 1; y: 0; z: 0 }
+        },
+        Rotation {
+            id: flipY
+
+            axis { x: 0; y: 1; z: 0 }
+        }
+    ]
+
+    Repeater {
+        model: pieceModel.elements
+
+        QuarterSquare {
+            x: (modelData.x - pieceModel.center.x) * board.gridWidth
+            y: (modelData.y - pieceModel.center.y) * board.gridHeight
+            pointType: {
+                var t = modelData.x
+                if (modelData.y % 2 != 0) t += 2
+                return (t % 4 + 4) % 4
+            }
+        }
+    }
+    Rectangle {
+        opacity: moveMarking == "last_dot" && pieceModel.isLastMove ? 0.5 : 0
+        color: gameModel.showVariations && ! gameModel.isMainVar ? "transparent" : border.color
+        border { width: 0.2 * width; color: root.color[3] }
+        width: 0.45 * board.gridHeight
+        height: width
+        radius: width / 2
+        x: pieceModel.labelPos.x * board.gridWidth - width / 2
+        y: pieceModel.labelPos.y * board.gridHeight - height / 2
+        Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+    }
+    Loader {
+        sourceComponent: moveMarking === "all_number"
+                         || moveMarking === "last_number" || item ?
+                             textComponent : null
+
+        Component {
+            id: textComponent
+
+            Text {
+                property bool flippedY: Math.abs(flipY.angle - 180) < 90
+
+                text: moveMarking == "all_number"
+                      || (moveMarking == "last_number"
+                          && pieceModel.isLastMove) ?
+                          pieceModel.moveLabel : ""
+                opacity: text === "" ? 0 : 1
+                color: root.color[3]
+                width: 2 * board.gridWidth
+                height: 2 * board.gridHeight
+                fontSizeMode: Text.Fit
+                font {
+                    pixelSize: 0.7 * board.gridHeight
+                    preferShaping: false
+                }
+                minimumPixelSize: 5
+                verticalAlignment: Text.AlignVCenter
+                horizontalAlignment: Text.AlignHCenter
+                x: pieceModel.labelPos.x * board.gridWidth - width / 2
+                y: pieceModel.labelPos.y * board.gridHeight - height / 2
+                transform: [
+                    Rotation {
+                        origin { x: board.gridWidth; y: board.gridHeight }
+                        axis { x: 0; y: 1; z: 0 }
+                        angle: flippedY ? -180 : 0
+                    },
+                    Rotation {
+                        origin { x: board.gridWidth; y: board.gridHeight }
+                        axis { x: 1; y: 0; z: 0 }
+                        angle: flippedX ? -180 : 0
+                    },
+                    Rotation {
+                        origin { x: board.gridWidth; y: board.gridHeight }
+                        angle: -root.rotation
+                    }
+                ]
+                Behavior on opacity {
+                    NumberAnimation { duration: animationDurationFast }
+                }
+            }
+        }
+    }
+    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: ",rot180Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot90,rot270Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot180,flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot270,rot90Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                enabled: enableAnimations
+
+                PieceRotationAnimation { }
+                PieceFlipAnimation { target: flipX }
+            }
+        ]
+    }
+
+    states: [
+        State {
+            name: "picked"
+            when: root === pickedPiece
+
+            ParentChange {
+                target: root
+                parent: pieceManipulator
+                x: pieceManipulator.width / 2
+                y: pieceManipulator.height / 2
+            }
+        },
+        State {
+            name: "played"
+            when: pieceModel.isPlayed
+
+            ParentChange {
+                target: root
+                parent: board.grabImageTarget
+                x: board.mapFromGameX(pieceModel.gameCoord.x) - board.grabImageTarget.x
+                y: board.mapFromGameY(pieceModel.gameCoord.y) - board.grabImageTarget.y
+            }
+        },
+        State {
+            name: "unplayed"
+            when: parentUnplayed != null
+
+            PropertyChanges {
+                target: root
+                scale: scaleUnplayed
+            }
+            ParentChange {
+                target: root
+                parent: parentUnplayed
+                x: parentUnplayed.width / 2
+                y: parentUnplayed.height / 2
+            }
+        }
+    ]
+    transitions:
+        Transition {
+        from: "unplayed,picked,played"; to: from
+        enabled: enableAnimations
+
+        SequentialAnimation {
+            PropertyAction {
+                target: parentUnplayed.parent
+                property: "z"; value: 1
+            }
+            ParentAnimation {
+                via: isDesktop ? null : gameDisplay
+
+                NumberAnimation {
+                    properties: "x,y,scale"
+                    duration: animationDurationMove
+                    easing.type: Easing.InOutSine
+                }
+            }
+            PropertyAction {
+                target: parentUnplayed.parent
+                property: "z"; value: 0
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/PieceList.qml b/src/pentobi/qml/PieceList.qml
new file mode 100644 (file)
index 0000000..b33a308
--- /dev/null
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceList.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Grid {
+    id: root
+
+    property var pieces
+
+    signal piecePicked(var piece)
+
+    // Show unplayed pieces in slightly less bright colors, such that they
+    // don't distract from the pieces on board, but not in desktop mode, where
+    // the unplayed pieces are relatively small, or if the background is bright
+    // to avoid bad contrast with yellow pieces.
+    opacity:
+        isDesktop ? 1 :
+                    Math.min(0.9 + 0.1 * theme.colorBackground.hslLightness, 1)
+
+    Repeater {
+        model: pieces
+
+        MouseArea {
+            id: mouseArea
+
+            width: root.width / columns; height: width
+            visible: ! modelData.pieceModel.isPlayed
+            onClicked: {
+                gameDisplay.dropCommentFocus()
+                piecePicked(modelData)
+            }
+            Component.onCompleted: modelData.parentUnplayed = mouseArea
+        }
+    }
+}
diff --git a/src/pentobi/qml/PieceManipulator.qml b/src/pentobi/qml/PieceManipulator.qml
new file mode 100644 (file)
index 0000000..c0a9d97
--- /dev/null
@@ -0,0 +1,122 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceManipulator.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Item {
+    id: root
+
+    property QtObject pieceModel
+    // True if piece manipulator is at a board location that is a legal move
+    property bool legal
+
+    // Fast move animation to make sure that the next grid cell is reached
+    // before the next auto-repeat keyboard command
+    property bool fastMove: false
+
+    // Manipulator buttons are smaller on desktop with mouse usage
+    property real buttonSize: (isDesktop ? 0.12 : 0.17) * root.width
+
+    property real animationDuration:
+        ! pieceModel || ! gameDisplay.enableAnimations ?
+            0 : fastMove ? 50 : animationDurationMove
+
+    signal piecePlayed
+
+    enabled: pieceModel
+
+    Image {
+        anchors.fill: root
+        source: isDesktop ? theme.getImage("piece-manipulator-desktop")
+                          : theme.getImage("piece-manipulator")
+        sourceSize { width: width; height: height }
+        opacity: pieceModel && ! legal ? 0.7 : 0
+
+        Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+    }
+    Image {
+        anchors.fill: root
+        source: isDesktop ? theme.getImage("piece-manipulator-desktop-legal")
+                          : theme.getImage("piece-manipulator-legal")
+        sourceSize { width: width; height: height }
+        opacity: pieceModel && legal ? 0.55 : 0
+
+        Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+    }
+    MouseArea {
+        id: dragArea
+
+        anchors.centerIn: root
+        // Make drag area a bit larger than image to avoid accidental
+        // flicking in PieceSelectorMobile when wanting to drag
+        width: root.width + buttonSize; height: width
+        drag {
+            target: root
+            filterChildren: true
+            minimumX: -root.width / 2
+            maximumX: root.parent.width - root.width / 2
+            minimumY: -root.height / 2
+            maximumY: root.parent.height - root.height / 2
+        }
+        // Consume mouse hover events in case it is over toolbar
+        hoverEnabled: isDesktop
+
+        MouseArea {
+            anchors.centerIn: dragArea
+            width: 0.9 * (root.width - 2 * buttonSize); height: width
+            onClicked: piecePlayed()
+        }
+        MouseArea {
+            anchors {
+                top: dragArea.top
+                margins: (dragArea.width - root.width) / 2
+                horizontalCenter: dragArea.horizontalCenter
+            }
+            width: buttonSize; height: width
+            onClicked: pieceModel.rotateRight()
+        }
+        MouseArea {
+            anchors {
+                right: dragArea.right
+                margins: (dragArea.width - root.width) / 2
+                verticalCenter: dragArea.verticalCenter
+            }
+            width: buttonSize; height: width
+            onClicked: pieceModel.flipAcrossX()
+        }
+        MouseArea {
+            anchors {
+                bottom: dragArea.bottom
+                margins: (dragArea.width - root.width) / 2
+                horizontalCenter: dragArea.horizontalCenter
+            }
+            width: buttonSize; height: width
+            onClicked: pieceModel.flipAcrossY()
+        }
+        MouseArea {
+            anchors {
+                left: dragArea.left
+                margins: (dragArea.width - root.width) / 2
+                verticalCenter: dragArea.verticalCenter
+            }
+            width: buttonSize; height: width
+            onClicked: pieceModel.rotateLeft()
+        }
+    }
+
+    Behavior on x {
+        NumberAnimation {
+            duration: animationDuration
+            easing.type: Easing.InOutSine
+        }
+    }
+    Behavior on y {
+        NumberAnimation {
+            duration: animationDuration
+            easing.type: Easing.InOutSine
+        }
+    }
+}
diff --git a/src/pentobi/qml/PieceNexos.qml b/src/pentobi/qml/PieceNexos.qml
new file mode 100644 (file)
index 0000000..61966fa
--- /dev/null
@@ -0,0 +1,325 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceNexos.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+
+// Piece for Nexos. See PieceClassic for comments.
+Item
+{
+    id: root
+
+    property QtObject pieceModel
+    property var color:
+        switch (pieceModel.color) {
+        case 0: return color0
+        case 1: return color1
+        case 2: return color2
+        case 3: return color3
+        }
+    property Item parentUnplayed
+    property string imageName:
+        "image://pentobi/square/" + color[0] + "/" + color[1] + "/" + color[2]
+    // Avoid fractional sizes for square piece elements
+    property real scaleUnplayed:
+        parentUnplayed ?
+            Math.floor(0.12 * parentUnplayed.width) / board.gridWidth : 0
+    property bool flippedX: Math.abs(flipX.angle - 180) < 90
+    property bool flippedY: flipY.angle > 90
+    property real pieceAngle: {
+        if (! flippedY && ! flippedX) return rotation
+        if (! flippedY && flippedX) return rotation + 90
+        if (flippedX) return rotation + 180
+        return rotation + 270
+    }
+    property real isSmall: scale < 0.5 ? 1 : 0
+    property real imageOpacity0: imageOpacity(pieceAngle, 0) * (1 - isSmall)
+    property real imageOpacity90: imageOpacity(pieceAngle, 90) * (1 - isSmall)
+    property real imageOpacity180: imageOpacity(pieceAngle, 180) * (1 - isSmall)
+    property real imageOpacity270: imageOpacity(pieceAngle, 270) * (1 - isSmall)
+    property real imageOpacitySmall0: imageOpacity(pieceAngle, 0) * isSmall
+    property real imageOpacitySmall90: imageOpacity(pieceAngle, 90) * isSmall
+    property real imageOpacitySmall180: imageOpacity(pieceAngle, 180) * isSmall
+    property real imageOpacitySmall270: imageOpacity(pieceAngle, 270) * isSmall
+
+    transform: [
+        Rotation {
+            id: flipX
+
+            axis { x: 1; y: 0; z: 0 }
+        },
+        Rotation {
+            id: flipY
+
+            axis { x: 0; y: 1; z: 0 }
+        }
+    ]
+
+    function isHorizontal(pos) { return pos.x % 2 != 0 }
+    function imageOpacity(pieceAngle, imgAngle) {
+        var angle = (pieceAngle - imgAngle + 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 * board.gridWidth
+            height: 0.5 * board.gridHeight
+            x: (modelData.x - pieceModel.center.x - 0.25) * board.gridWidth
+            y: (modelData.y - pieceModel.center.y + 0.25) * board.gridHeight
+        }
+    }
+    Repeater {
+        model: pieceModel.junctions
+
+        Image {
+            source: {
+                switch (pieceModel.junctionType[index]) {
+                case 0:
+                    return  "image://pentobi/junction-all/" + color[0]
+                case 1:
+                case 2:
+                case 3:
+                case 4:
+                    return  "image://pentobi/junction-t/" + color[0]
+                case 5:
+                case 6:
+                    return  "image://pentobi/junction-straight/" + color[0]
+                default:
+                    return  "image://pentobi/junction-right/" + color[0]
+                }
+            }
+            rotation: {
+                switch (pieceModel.junctionType[index]) {
+                case 1:
+                case 9:
+                    return 270
+                case 2:
+                case 6:
+                case 8:
+                    return 90
+                case 4:
+                case 7:
+                    return 180
+                default:
+                    return 0
+                }
+            }
+            width: 0.5 * board.gridWidth
+            height: 0.5 * board.gridHeight
+            x: (modelData.x - pieceModel.center.x + 0.25) * board.gridWidth
+            y: (modelData.y - pieceModel.center.y + 0.25) * board.gridHeight
+            sourceSize {
+                width: imageSourceSize.width / 3
+                height: imageSourceSize.height
+            }
+            antialiasing: true
+        }
+    }
+    Rectangle {
+        opacity: moveMarking == "last_dot" && pieceModel.isLastMove ? 0.5 : 0
+        color: gameModel.showVariations && ! gameModel.isMainVar ? "transparent" : border.color
+        border { width: 0.2 * width; color: root.color[3] }
+        width: 0.3 * board.gridHeight
+        height: width
+        radius: width / 2
+        x: pieceModel.labelPos.x * board.gridWidth - width / 2
+        y: pieceModel.labelPos.y * board.gridHeight - height / 2
+        Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+    }
+    Loader {
+        sourceComponent: moveMarking === "all_number"
+                         || moveMarking === "last_number" || item ?
+                             textComponent : null
+
+        Component {
+            id: textComponent
+
+            Text {
+                text: moveMarking == "all_number"
+                      || (moveMarking == "last_number"
+                          && pieceModel.isLastMove) ?
+                          pieceModel.moveLabel : ""
+                opacity: text === "" ? 0 : 1
+                color: root.color[3]
+                width: board.gridWidth
+                height: board.gridHeight
+                fontSizeMode: Text.Fit
+                font {
+                    pixelSize: 0.5 * board.gridHeight
+                    preferShaping: false
+                }
+                minimumPixelSize: Math.max(3, 0.3 * board.gridHeight)
+                verticalAlignment: Text.AlignVCenter
+                horizontalAlignment: Text.AlignHCenter
+                x: pieceModel.labelPos.x * board.gridWidth - width / 2
+                y: pieceModel.labelPos.y * board.gridHeight - height / 2
+                transform: [
+                    Rotation {
+                        origin {
+                            x: board.gridWidth / 2; y: board.gridHeight / 2
+                        }
+                        axis { x: 0; y: 1; z: 0 }
+                        angle: flippedY ? -180 : 0
+                    },
+                    Rotation {
+                        origin {
+                            x: board.gridWidth / 2; y: board.gridHeight / 2
+                        }
+                        axis { x: 1; y: 0; z: 0 }
+                        angle: flippedX ? -180 : 0
+                    },
+                    Rotation {
+                        origin {
+                            x: board.gridWidth / 2; y: board.gridHeight / 2
+                        }
+                        angle: -root.rotation
+                    }
+                ]
+                Behavior on opacity {
+                    NumberAnimation { duration: animationDurationFast }
+                }
+            }
+        }
+    }
+    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: ",rot180Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot90,rot270Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot180,flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot270,rot90Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                enabled: enableAnimations
+
+                PieceRotationAnimation { }
+                PieceFlipAnimation { target: flipX }
+            }
+        ]
+    }
+
+    states: [
+        State {
+            name: "picked"
+            when: root === pickedPiece
+
+            ParentChange {
+                target: root
+                parent: pieceManipulator
+                x: pieceManipulator.width / 2
+                y: pieceManipulator.height / 2
+            }
+        },
+        State {
+            name: "played"
+            when: pieceModel.isPlayed
+
+            ParentChange {
+                target: root
+                parent: board.grabImageTarget
+                x: board.mapFromGameX(pieceModel.gameCoord.x) - board.grabImageTarget.x
+                y: board.mapFromGameY(pieceModel.gameCoord.y) - board.grabImageTarget.y
+            }
+        },
+        State {
+            name: "unplayed"
+            when: parentUnplayed != null
+
+            PropertyChanges {
+                target: root
+                scale: scaleUnplayed
+            }
+            ParentChange {
+                target: root
+                parent: parentUnplayed
+                x: parentUnplayed.width / 2
+                y: parentUnplayed.height / 2
+            }
+        }
+    ]
+    transitions:
+        Transition {
+            from: "unplayed,picked,played"; to: from
+            enabled: enableAnimations
+
+            SequentialAnimation {
+                PropertyAction {
+                    target: parentUnplayed.parent
+                    property: "z"; value: 1
+                }
+                ParentAnimation {
+                    via: isDesktop ? null : gameDisplay
+
+                    NumberAnimation {
+                        properties: "x,y,scale"
+                        duration: animationDurationMove
+                        easing.type: Easing.InOutSine
+                    }
+                }
+                PropertyAction {
+                    target: parentUnplayed.parent
+                    property: "z"; value: 0
+                }
+            }
+    }
+}
diff --git a/src/pentobi/qml/PieceRotationAnimation.qml b/src/pentobi/qml/PieceRotationAnimation.qml
new file mode 100644 (file)
index 0000000..134d7d9
--- /dev/null
@@ -0,0 +1,12 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceRotationAnimation.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+RotationAnimation {
+    duration: animationDurationMove
+    direction: RotationAnimation.Shortest
+}
diff --git a/src/pentobi/qml/PieceSelectorDesktop.qml b/src/pentobi/qml/PieceSelectorDesktop.qml
new file mode 100644 (file)
index 0000000..c15b0c0
--- /dev/null
@@ -0,0 +1,134 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceSelectorDesktop.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Item {
+    id: root
+
+    property alias pieces0: pieceList0.pieces
+    property alias pieces1: pieceList1.pieces
+    property alias pieces2: pieceList2.pieces
+    property alias pieces3: pieceList3.pieces
+    property int columns: pieces0 ? Math.ceil(pieces0.length / 2) : 11
+    property alias transitionsEnabled: transition.enabled
+
+    signal piecePicked(var piece)
+
+    property real toPlayIndicatorWidth:
+        Math.max(Math.min(parent.width / columns, parent.height / 8.3) / 10, 2)
+
+    Row {
+        // Set size sich that width/height ration fits the number of columns,
+        // taking toPlayIndicator and column spacing into account
+        width: Math.min(parent.width - toPlayIndicatorWidth,
+                        parent.height / 8.3 * columns)
+        height:
+            Math.min(parent.height,
+                     (parent.width - toPlayIndicatorWidth) / columns * 8.3)
+        anchors.centerIn: parent
+
+        Rectangle {
+            id: toPlayIndicator
+
+            opacity: gameModel.isGameOver ? 0 : 0.3
+            x: 0
+            width: toPlayIndicatorWidth
+            radius: width / 2
+            color: theme.colorText
+        }
+        Column {
+            id: column
+
+            width: parent.width - toPlayIndicatorWidth
+            spacing: parent.height / 8.3 * 0.1
+
+            PieceList {
+                id: pieceList0
+
+                width: parent.width
+                columns: root.columns
+                onPiecePicked: root.piecePicked(piece)
+            }
+            PieceList {
+                id: pieceList1
+
+                width: parent.width
+                columns: root.columns
+                onPiecePicked: root.piecePicked(piece)
+            }
+            PieceList {
+                id: pieceList2
+
+                width: parent.width
+                columns: root.columns
+                onPiecePicked: root.piecePicked(piece)
+            }
+            PieceList {
+                id: pieceList3
+
+                width: parent.width
+                columns: root.columns
+                onPiecePicked: root.piecePicked(piece)
+            }
+        }
+    }
+
+    // It would be much simpler to use bindings for y/height and a Behavior for
+    // the y animation, but I haven't found a way to disable the animation if a
+    // game loaded at startup has toPlay != 0
+    states: [
+        State {
+            name: "toPlay0"
+            when: gameModel.toPlay === 0
+
+            PropertyChanges {
+                target: toPlayIndicator
+                y: column.mapToItem(parent, 0, pieceList0.y).y
+                height: pieceList0.height
+            }
+        },
+        State {
+            name: "toPlay1"
+            when: gameModel.toPlay === 1
+
+            PropertyChanges {
+                target: toPlayIndicator
+                y: column.mapToItem(parent, 0, pieceList1.y).y
+                height: pieceList1.height
+            }
+        },
+        State {
+            name: "toPlay2"
+            when: gameModel.toPlay === 2
+
+            PropertyChanges {
+                target: toPlayIndicator
+                y: column.mapToItem(parent, 0, pieceList2.y).y
+                height: pieceList2.height
+            }
+        },
+        State {
+            name: "toPlay3"
+            when: gameModel.toPlay === 3
+
+            PropertyChanges {
+                target: toPlayIndicator
+                y: column.mapToItem(parent, 0, pieceList3.y).y
+                height: pieceList0.height
+            }
+        }
+    ]
+    transitions: Transition {
+        id: transition
+
+        NumberAnimation {
+            target: toPlayIndicator
+            property: "y"
+            duration: 0.6 * animationDurationFast
+        }
+    }
+}
diff --git a/src/pentobi/qml/PieceSelectorMobile.qml b/src/pentobi/qml/PieceSelectorMobile.qml
new file mode 100644 (file)
index 0000000..e09609f
--- /dev/null
@@ -0,0 +1,251 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceSelectorMobile.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+
+Flickable {
+    id: root
+
+    property alias pieces0: pieceList0.pieces
+    property alias pieces1: pieceList1.pieces
+    property alias pieces2: pieceList2.pieces
+    property alias pieces3: pieceList3.pieces
+    property int columns
+    property real rowSpacing
+    property alias transitionsEnabled: transition.enabled
+
+    signal piecePicked(var piece)
+
+    flickableDirection: Flickable.VerticalFlick
+    contentHeight: Math.max(pieceList0.y + pieceList0.height,
+                            pieceList1.y + pieceList1.height,
+                            pieceList2.y + pieceList2.height,
+                            pieceList3.y + pieceList3.height)
+    clip: true
+
+    Behavior on contentY { NumberAnimation { duration: animationDurationFast } }
+
+    PieceList {
+        id: pieceList0
+
+        width: root.width
+        columns: root.columns
+        rowSpacing: root.rowSpacing
+        onPiecePicked: root.piecePicked(piece)
+    }
+    PieceList {
+        id: pieceList1
+
+        width: root.width
+        columns: root.columns
+        rowSpacing: root.rowSpacing
+        onPiecePicked: root.piecePicked(piece)
+    }
+    PieceList {
+        id: pieceList2
+
+        width: root.width
+        columns: root.columns
+        rowSpacing: root.rowSpacing
+        onPiecePicked: root.piecePicked(piece)
+    }
+    PieceList {
+        id: pieceList3
+
+        width: root.width
+        columns: root.columns
+        rowSpacing: root.rowSpacing
+        onPiecePicked: root.piecePicked(piece)
+    }
+
+    // States order the piece lists such that the color to play is on top. If a
+    // player plays two colors, their second color follows, such that at least
+    // at the end of the game all of their remaining pieces should be in the
+    // visible area. Otherwise the colors are in order of play.
+    states: [
+        State {
+            name: "toPlay0"
+            when: gameModel.toPlay === 0
+
+            PropertyChanges {
+                target: pieceList0
+                y: 0.5 * rowSpacing
+            }
+            PropertyChanges {
+                target: pieceList1
+                y: {
+                    if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+                        return pieceList0.height + pieceList2.height
+                                + 2.5 * rowSpacing
+                    return pieceList0.height + 1.5 * rowSpacing
+                }
+            }
+            PropertyChanges {
+                target: pieceList2
+                y: {
+                    if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+                        return pieceList0.height + 1.5 * rowSpacing
+                    return pieceList0.height + pieceList1.height
+                            + 2.5 * rowSpacing
+                }
+            }
+            PropertyChanges {
+                target: pieceList3
+                y: pieceList0.height + pieceList1.height + pieceList2.height
+                   + 3.5 * rowSpacing
+            }
+        },
+        State {
+            name: "toPlay1"
+            when: gameModel.toPlay === 1
+
+            PropertyChanges {
+                target: pieceList1
+                y: 0.5 * rowSpacing
+            }
+            PropertyChanges {
+                target: pieceList2
+                y: {
+                    if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+                        return pieceList1.height + pieceList3.height
+                                + 2.5 * rowSpacing
+                    return pieceList1.height + 1.5 * rowSpacing
+                }
+            }
+            PropertyChanges {
+                target: pieceList3
+                y: {
+                    if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+                        return pieceList1.height + 1.5 * rowSpacing
+                    return pieceList1.height + pieceList2.height
+                            + 2.5 * rowSpacing
+                }
+            }
+            PropertyChanges {
+                target: pieceList0
+                y: {
+                    if (gameModel.nuColors === 2)
+                        return pieceList1.height + 1.5 * rowSpacing
+                    if (gameModel.nuColors === 3)
+                        return pieceList1.height + pieceList2.height
+                                + 2.5 * rowSpacing
+                    return pieceList1.height + pieceList2.height
+                            + pieceList3.height + 3.5 * rowSpacing
+                }
+            }
+        },
+        State {
+            name: "toPlay2"
+            when: gameModel.toPlay === 2
+
+            PropertyChanges {
+                target: pieceList2
+                y: 0.5 * rowSpacing
+            }
+            PropertyChanges {
+                target: pieceList3
+                y: {
+                    if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+                        return pieceList2.height + pieceList0.height
+                                + 2.5 * rowSpacing
+                    return pieceList2.height + 1.5 * rowSpacing
+                }
+            }
+            PropertyChanges {
+                target: pieceList0
+                y: {
+                    if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+                        return pieceList2.height + 1.5 * rowSpacing
+                    if (gameModel.nuColors === 3)
+                        return pieceList2.height + pieceList3.height
+                                + 2.5 * rowSpacing
+                    return pieceList2.height + pieceList3.height
+                            + 2.5 * rowSpacing
+                }
+            }
+            PropertyChanges {
+                target: pieceList1
+                y: {
+                    if (gameModel.nuColors === 3)
+                        return pieceList2.height + pieceList0.height
+                                + 2.5 * rowSpacing
+                    return pieceList2.height + pieceList3.height
+                            + pieceList0.height + 3.5 * rowSpacing
+                }
+            }
+        },
+        State {
+            name: "toPlay3"
+            when: gameModel.toPlay === 3
+
+            PropertyChanges {
+                target: pieceList3
+                y: 0.5 * rowSpacing
+            }
+            PropertyChanges {
+                target: pieceList0
+                y: {
+                    if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+                        return pieceList3.height + pieceList1.height
+                                + 2.5 * rowSpacing
+                    return pieceList3.height + 1.5 * rowSpacing
+                }
+            }
+            PropertyChanges {
+                target: pieceList1
+                y: {
+                    if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+                        return pieceList3.height + 1.5 * rowSpacing
+                    return pieceList3.height + pieceList0.height
+                            + 2.5 * rowSpacing
+                }
+            }
+            PropertyChanges {
+                target: pieceList2
+                y: pieceList3.height + pieceList0.height + pieceList1.height
+                   + 3.5 * rowSpacing
+            }
+        }
+    ]
+    transitions:
+        Transition {
+            id: transition
+
+            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:
+                        Math.max(animationDurationMove - animationDurationFast,
+                                 0)
+                }
+                NumberAnimation {
+                    target: root
+                    property: "opacity"
+                    to: 0
+                    duration: animationDurationFast
+                }
+                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: animationDurationFast
+                }
+            }
+    }
+}
diff --git a/src/pentobi/qml/PieceSwitchedFlipAnimation.qml b/src/pentobi/qml/PieceSwitchedFlipAnimation.qml
new file mode 100644 (file)
index 0000000..c67017b
--- /dev/null
@@ -0,0 +1,20 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceSwitchedFlipAnimation.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+// Helper animation for pieces.
+// Unique piece 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.
+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 }
+}
diff --git a/src/pentobi/qml/PieceTrigon.qml b/src/pentobi/qml/PieceTrigon.qml
new file mode 100644 (file)
index 0000000..7a9f607
--- /dev/null
@@ -0,0 +1,312 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceTrigon.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+// See PieceClassic.qml for comments
+Item
+{
+    id: root
+
+    property QtObject pieceModel
+    property var color:
+        switch (pieceModel.color) {
+        case 0: return color0
+        case 1: return color1
+        case 2: return color2
+        case 3: return color3
+        }
+    property Item parentUnplayed
+    property string imageName:
+        "image://pentobi/triangle/" + color[0] + "/" + color[1] + "/"
+        + color[2]
+    property string imageNameDownward:
+        "image://pentobi/triangle-down/" + color[0] + "/" + color[1] + "/"
+        + color[2]
+    property real scaleUnplayed:
+        parentUnplayed ? 0.14 * parentUnplayed.width / board.gridWidth : 0
+    property bool flippedX: Math.abs(flipX.angle - 180) < 90
+    property real pieceAngle: flippedX ? rotation + 180 : rotation
+    property real isSmall: scale < 0.5 ? 1 : 0
+    property real imageOpacity0: imageOpacity(pieceAngle, 0) * (1 - isSmall)
+    property real imageOpacity60: imageOpacity(pieceAngle, 60) * (1 - isSmall)
+    property real imageOpacity120: imageOpacity(pieceAngle, 120) * (1 - isSmall)
+    property real imageOpacity180: imageOpacity(pieceAngle, 180) * (1 - isSmall)
+    property real imageOpacity240: imageOpacity(pieceAngle, 240) * (1 - isSmall)
+    property real imageOpacity300: imageOpacity(pieceAngle, 300) * (1 - isSmall)
+    property real imageOpacitySmall0: imageOpacity(pieceAngle, 0) * isSmall
+    property real imageOpacitySmall60: imageOpacity(pieceAngle, 60) * isSmall
+    property real imageOpacitySmall120: imageOpacity(pieceAngle, 120) * isSmall
+    property real imageOpacitySmall180: imageOpacity(pieceAngle, 180) * isSmall
+    property real imageOpacitySmall240: imageOpacity(pieceAngle, 240) * isSmall
+    property real imageOpacitySmall300: imageOpacity(pieceAngle, 300) * isSmall
+
+    transform: [
+        Rotation {
+            id: flipX
+
+            axis { x: 1; y: 0; z: 0 }
+        },
+        Rotation {
+            id: flipY
+
+            axis { x: 0; y: 1; z: 0 }
+        }
+    ]
+
+    function _isDownward(pos) { return (pos.x % 2 == 0) != (pos.y % 2 == 0) }
+    function imageOpacity(pieceAngle, imgAngle) {
+        var angle = (pieceAngle - imgAngle + 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 * board.gridWidth
+            height: board.gridHeight
+            x: (modelData.x - pieceModel.center.x - 0.5) * board.gridWidth
+            y: (modelData.y - pieceModel.center.y) * board.gridHeight
+        }
+    }
+    Rectangle {
+        opacity: moveMarking == "last_dot" && pieceModel.isLastMove ? 0.5 : 0
+        color: gameModel.showVariations && ! gameModel.isMainVar ? "transparent" : border.color
+        border { width: 0.2 * width; color: root.color[3] }
+        width: 0.3 * board.gridHeight
+        height: width
+        radius: width / 2
+        x: pieceModel.labelPos.x * board.gridWidth - width / 2
+        y: pieceModel.labelPos.y * board.gridHeight - height / 2
+
+        Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+    }
+    Loader {
+        sourceComponent: moveMarking === "all_number"
+                         || moveMarking === "last_number" || item ?
+                             textComponent : null
+
+        Component {
+            id: textComponent
+
+            Text {
+                property bool flippedY: Math.abs(flipY.angle - 180) < 90
+
+                text: moveMarking == "all_number"
+                      || (moveMarking == "last_number"
+                          && pieceModel.isLastMove) ?
+                          pieceModel.moveLabel : ""
+                opacity: text === "" ? 0 : 1
+                color: root.color[3]
+                width: board.gridWidth
+                height: board.gridHeight
+                fontSizeMode: Text.Fit
+                font {
+                    pixelSize: 0.5 * board.gridHeight
+                    preferShaping: false
+                }
+                minimumPixelSize: 5
+                verticalAlignment: Text.AlignVCenter
+                horizontalAlignment: Text.AlignHCenter
+                x: pieceModel.labelPos.x * board.gridWidth - width / 2
+                y: pieceModel.labelPos.y * board.gridHeight - height / 2
+                transform: [
+                    Rotation {
+                        origin {
+                            x: board.gridWidth / 2; y: board.gridHeight / 2
+                        }
+                        axis { x: 0; y: 1; z: 0 }
+                        angle: flippedY ? -180 : 0
+                    },
+                    Rotation {
+                        origin {
+                            x: board.gridWidth / 2; y: board.gridHeight / 2
+                        }
+                        axis { x: 1; y: 0; z: 0 }
+                        angle: flippedX ? -180 : 0
+                    },
+                    Rotation {
+                        origin {
+                            x: board.gridWidth / 2; y: board.gridHeight / 2
+                        }
+                        angle: -root.rotation
+                    }
+                ]
+                Behavior on opacity {
+                    NumberAnimation { duration: animationDurationFast }
+                }
+            }
+        }
+    }
+    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: ",rot180Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot60,rot240Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot120,rot300Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot180,flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot240,rot60Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "rot300,rot120Flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                enabled: enableAnimations
+
+                PieceRotationAnimation { }
+                PieceFlipAnimation { target: flipX }
+            }
+        ]
+    }
+
+    states: [
+        State {
+            name: "picked"
+            when: root === pickedPiece
+
+            ParentChange {
+                target: root
+                parent: pieceManipulator
+                x: pieceManipulator.width / 2
+                y: pieceManipulator.height / 2
+            }
+        },
+        State {
+            name: "played"
+            when: pieceModel.isPlayed
+
+            ParentChange {
+                target: root
+                parent: board.grabImageTarget
+                x: board.mapFromGameX(pieceModel.gameCoord.x) - board.grabImageTarget.x
+                y: board.mapFromGameY(pieceModel.gameCoord.y) - board.grabImageTarget.y
+            }
+        },
+        State {
+            name: "unplayed"
+            when: parentUnplayed != null
+
+            PropertyChanges {
+                target: root
+                scale: scaleUnplayed
+            }
+            ParentChange {
+                target: root
+                parent: parentUnplayed
+                x: parentUnplayed.width / 2
+                y: parentUnplayed.height / 2
+            }
+        }
+    ]
+
+    transitions:
+        Transition {
+        from: "unplayed,picked,played"; to: from
+        enabled: enableAnimations
+
+        SequentialAnimation {
+            PropertyAction {
+                target: parentUnplayed.parent
+                property: "z"; value: 1
+            }
+            ParentAnimation {
+                via: isDesktop ? null : gameDisplay
+
+                NumberAnimation {
+                    properties: "x,y,scale"
+                    duration: animationDurationMove
+                    easing.type: Easing.InOutSine
+                }
+            }
+            PropertyAction {
+                target: parentUnplayed.parent
+                property: "z"; value: 0
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/QuarterSquare.qml b/src/pentobi/qml/QuarterSquare.qml
new file mode 100644 (file)
index 0000000..8f0505c
--- /dev/null
@@ -0,0 +1,136 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/QuarterSquare.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+
+// Piece element used in GembloQ. See Square.qml for comments
+Item {
+    property int pointType
+
+    Loader {
+        opacity: switch (pointType) {
+                 case 0: return imageOpacity0
+                 case 1: return imageOpacity180
+                 case 2: return imageOpacity90
+                 case 3: return imageOpacity270
+                 }
+        sourceComponent: opacity > 0 || item ? componentTop : null
+
+        Component {
+            id: componentTop
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                // Don't set antialiasing, vertex antialiasing causes unwanted
+                // seams between edges of the quarter squares
+                antialiasing: false
+                rotation: switch (pointType) {
+                          case 1: return 180
+                          case 2: return 270
+                          case 3: return 90
+                          default: return 0
+                          }
+                x: pointType == 1 || pointType == 3 ? -width / 2 : 0
+            }
+        }
+    }
+    Loader {
+        opacity: switch (pointType) {
+                 case 0: return imageOpacitySmall0
+                 case 1: return imageOpacitySmall180
+                 case 2: return imageOpacitySmall90
+                 case 3: return imageOpacitySmall270
+                 }
+        sourceComponent: opacity > 0 || item ? componentSmallTop : null
+
+        Component {
+            id: componentSmallTop
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                // Don't set antialiasing, see above
+                antialiasing: false
+                rotation: switch (pointType) {
+                          case 1: return 180
+                          case 2: return 270
+                          case 3: return 90
+                          default: return 0
+                          }
+                x: pointType == 1 || pointType == 3 ? -width / 2 : 0
+            }
+        }
+    }
+    Loader {
+        opacity: switch (pointType) {
+                 case 0: return imageOpacity180
+                 case 1: return imageOpacity0
+                 case 2: return imageOpacity270
+                 case 3: return imageOpacity90
+                 }
+        sourceComponent: opacity > 0 || item ? componentBottom : null
+
+        Component {
+            id: componentBottom
+
+            Image {
+                source: imageNameBottom
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                // Don't set antialiasing, see above
+                antialiasing: false
+                rotation: switch (pointType) {
+                          case 1: return 180
+                          case 2: return 270
+                          case 3: return 90
+                          default: return 0
+                          }
+                x: pointType == 1 || pointType == 3 ? -width / 2 : 0
+            }
+        }
+    }
+    Loader {
+        opacity: switch (pointType) {
+                 case 0: return imageOpacitySmall180
+                 case 1: return imageOpacitySmall0
+                 case 2: return imageOpacitySmall270
+                 case 3: return imageOpacitySmall90
+                 }
+        sourceComponent: opacity > 0 || item ? componentSmallBottom : null
+
+        Component {
+            id: componentSmallBottom
+
+            Image {
+                source: imageNameBottom
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                // Don't set antialiasing, see above
+                antialiasing: false
+                rotation: switch (pointType) {
+                          case 1: return 180
+                          case 2: return 270
+                          case 3: return 90
+                          default: return 0
+                          }
+                x: pointType == 1 || pointType == 3 ? -width / 2 : 0
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/QuestionDialog.qml b/src/pentobi/qml/QuestionDialog.qml
new file mode 100644 (file)
index 0000000..7f88d85
--- /dev/null
@@ -0,0 +1,40 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/QuestionDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Dialog {
+    id: root
+
+    function openWithCallback(text, acceptedFunc) {
+        label.text = text
+        _acceptedFunc = acceptedFunc
+        open()
+    }
+
+    property var _acceptedFunc
+
+    footer: DialogButtonBoxOkCancel { }
+    onAccepted: _acceptedFunc()
+
+    Item {
+        implicitWidth:
+            Math.max(Math.min(label.implicitWidth,
+                              font.pixelSize * 25, maxContentWidth),
+                     font.pixelSize * 15, minContentWidth)
+        implicitHeight: label.implicitHeight
+
+        Label {
+            id: label
+
+            anchors.fill: parent
+            wrapMode: Text.Wrap
+        }
+    }
+}
diff --git a/src/pentobi/qml/RatingDialog.qml b/src/pentobi/qml/RatingDialog.qml
new file mode 100644 (file)
index 0000000..e7633c5
--- /dev/null
@@ -0,0 +1,251 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/RatingDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.0
+import "." as Pentobi
+import "Main.js" as Logic
+
+Pentobi.Dialog {
+    property int numberGames: ratingModel.numberGames
+    property var history: ratingModel.history
+
+    footer: Pentobi.DialogButtonBox { ButtonClose { } }
+
+    Item {
+        implicitWidth: Math.max(Math.min(font.pixelSize * 22, maxContentWidth),
+                                minContentWidth)
+        implicitHeight: columnLayout.implicitHeight
+
+        ColumnLayout
+        {
+            id: columnLayout
+
+            anchors.fill: parent
+
+            GridLayout {
+                columns: 2
+
+                Label {
+                    id: labelYourRating
+
+                    text: qsTr("Your rating:")
+                }
+                Label {
+                    text: ratingModel.numberGames === 0 ?
+                              "" : Math.round(ratingModel.rating).toString()
+                    Layout.fillWidth: true
+                    font.bold: true
+                }
+                Label { text: qsTr("Game variant:") }
+                Label {
+                    text: switch (ratingModel.gameVariant) {
+                          case "classic_2":
+                              //: Short for Classic (2 players)
+                              return qsTr("Classic (2)")
+                          case "classic_3":
+                              //: Short for Classic (3 players)
+                              return qsTr("Classic (3)")
+                          case "classic":
+                              //: Short for Classic (4 players)
+                              return qsTr("Classic (4)")
+                          case "duo":
+                              return qsTr("Duo")
+                          case "junior":
+                              return qsTr("Junior")
+                          case "trigon_2":
+                              //: Short for Trigon (2 players)
+                              return qsTr("Trigon (2)")
+                          case "trigon_3":
+                              //: Short for Trigon (3 players)
+                              return qsTr("Trigon (3)")
+                          case "trigon":
+                              //: Short for Trigon (4 players)
+                              return qsTr("Trigon (4)")
+                          case "nexos_2":
+                              //: Short for Nexos (2 players)
+                              return qsTr("Nexos (2)")
+                          case "nexos":
+                              //: Short for Nexos (4 players)
+                              return qsTr("Nexos (4)")
+                          case "callisto_2":
+                              //: Short for Callisto (2 players, 2 colors)
+                              return qsTr("Callisto (2)")
+                          case "callisto_2_4":
+                              //: Short for Callisto (2 players, 4 colors)
+                              return qsTr("Callisto (2/4)")
+                          case "callisto_3":
+                              //: Short for Callisto (3 players)
+                              return qsTr("Callisto (3)")
+                          case "callisto":
+                              //: Short for Callisto (4 players)
+                              return qsTr("Callisto (4)")
+                          case "gembloq":
+                              //: Short for GembloQ (4 players)
+                              return qsTr("GembloQ (4)")
+                          case "gembloq_2":
+                              //: Short for GembloQ (2 players, 2 colors)
+                              return qsTr("GembloQ (2)")
+                          case "gembloq_2_4":
+                              //: Short for GembloQ (2 players, 4 colors)
+                              return qsTr("GembloQ (2/4)")
+                          case "gembloq_3":
+                              //: Short for GembloQ (3 players)
+                              return qsTr("GembloQ (3)")
+                          default: return ""
+                          }
+                    Layout.fillWidth: true
+                }
+                Label { text: qsTr("Rated games:") }
+                Label {
+                    text: numberGames
+                    Layout.fillWidth: true
+                }
+                Label {
+                    visible: numberGames > 1
+                    text: qsTr("Best previous rating:")
+                }
+                Label {
+                    visible: numberGames > 1
+                    text: Math.round(ratingModel.bestRating).toString()
+                    Layout.fillWidth: true
+                }
+            }
+            ColumnLayout {
+                Layout.fillWidth: true
+
+                Label {
+                    visible: history.length > 1
+                    text: qsTr("Recent development:")
+                }
+                RatingGraph {
+                    visible: history.length > 1
+                    history: ratingModel.history
+                    Layout.preferredHeight:
+                        Math.min(font.pixelSize * 8,
+                                 0.22 * rootWindow.contentItem.width,
+                                 0.22 * rootWindow.contentItem.height)
+                    Layout.fillWidth: true
+                }
+            }
+            ScrollView
+            {
+                visible: history.length > 0
+                clip: true
+                Layout.fillWidth: true
+                Layout.preferredHeight:
+                    Math.min(font.pixelSize * 8,
+                             0.22 * rootWindow.contentItem.width,
+                             0.22 * rootWindow.contentItem.height)
+
+                Item
+                {
+                    implicitHeight: grid.height
+                    implicitWidth: grid.width
+
+                    GridLayout {
+                        id: grid
+
+                        rows: history.length + 1
+                        flow: Grid.TopToBottom
+
+                        Label {
+                            id: gameHeader
+
+                            font.underline: true
+                            text: qsTr("Game")
+                        }
+                        Repeater {
+                            id: gameRepeater
+
+                            model: history
+
+                            Label { text: modelData.number }
+                        }
+                        Label { font.underline: true; text: qsTr("Result") }
+                        Repeater {
+                            model: history
+
+                            Label {
+                                text: switch (modelData.result) {
+                                      case 1:
+                                          //: Result of rated game is a win
+                                          return qsTr("Win")
+                                      case 0:
+                                          //: Result of rated game is a loss
+                                          return qsTr("Loss")
+                                      case 0.5:
+                                          //: Result of rated game is a tie. Abbreviate long translations to
+                                          //: ensure that all columns of rated games list are visible on
+                                          //: mobile devices with small screens.
+                                          return qsTr("Tie")
+                                      }
+                            }
+                        }
+                        Label { font.underline: true; text: qsTr("Level") }
+                        Repeater {
+                            model: history
+
+                            Label { text: modelData.level }
+                        }
+                        Label { font.underline: true; text: qsTr("Your Color") }
+                        Repeater {
+                            model: history
+
+                            Label { text: gameModel.getPlayerString(modelData.color) }
+                        }
+                        Label { font.underline: true; text: qsTr("Date") }
+                        Repeater {
+                            model: history
+
+                            Label { text: modelData.date }
+                        }
+                    }
+                    MouseArea {
+                        function openMenu(x, y) {
+                            if (y < gameHeader.height)
+                                return
+                            var n = history.length
+                            var i
+                            for (i = 1; i < n; ++i)
+                                if (y < gameRepeater.itemAt(i).y)
+                                    break
+                            menu.row = i - 1
+                            menu.popup(mouseX, mouseY)
+                        }
+
+                        anchors.fill: grid
+                        acceptedButtons: Qt.LeftButton | Qt.RightButton
+                        onClicked: openMenu(mouseX, mouseY)
+                        onPressAndHold: openMenu(mouseX, mouseY)
+
+                        Pentobi.Menu {
+                            id: menu
+
+                            property int row
+
+                            width:
+                                Math.min(font.pixelSize * 14, maxContentWidth)
+
+                            Pentobi.MenuItem {
+                                width: parent.width
+                                text: history && menu.row < history.length ?
+                                          qsTr("Open Game %1").arg(history[menu.row].number) : ""
+                                onTriggered: {
+                                    Logic.openFile(
+                                                ratingModel.getFile(
+                                                    history[menu.row].number))
+                                    close()
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/RatingGraph.qml b/src/pentobi/qml/RatingGraph.qml
new file mode 100644 (file)
index 0000000..4172520
--- /dev/null
@@ -0,0 +1,77 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/RatingGraph.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Canvas {
+    property var history
+
+    antialiasing: true
+    onHistoryChanged: requestPaint()
+    onPaint: {
+        var w = width
+        var h = height
+        var ctx = getContext("2d")
+        ctx.fillStyle = "white"
+        ctx.fillRect(0, 0, w, h)
+        if (history === null)
+            return
+        var n = history.length
+        if (n === 0)
+            return
+        var margin = w / 30
+        ctx.save()
+        ctx.translate(margin, margin)
+        w -= 2 * margin
+        h -= 2 * margin
+        var i
+        var minX = Number.POSITIVE_INFINITY
+        var maxX = Number.NEGATIVE_INFINITY
+        var minY = Number.POSITIVE_INFINITY
+        var maxY = Number.NEGATIVE_INFINITY
+        var info
+        for (i = 0; i < n; ++i) {
+            info = ratingModel.history[i]
+            minX = Math.min(minX, info.number)
+            maxX = Math.max(maxX, info.number)
+            minY = Math.min(minY, info.rating)
+            maxY = Math.max(maxY, info.rating)
+        }
+        maxX = minX + Math.ceil((maxX - minX) * 1.2)
+        minY = Math.floor(minY / 100) * 100
+        maxY = Math.ceil(maxY / 100) * 100
+        if (maxY - minY < 100)
+            maxY = minY + 100
+
+        ctx.beginPath()
+        var top =  0
+        ctx.moveTo(0, top)
+        ctx.lineTo(w, top)
+        var bottom =  h
+        ctx.moveTo(0, bottom)
+        ctx.lineTo(w, bottom)
+        ctx.strokeStyle = "gray"
+        ctx.stroke()
+
+        ctx.font = Math.ceil(0.15 * h) + "px sans-serif"
+        ctx.fillStyle = "gray"
+        ctx.textAlign = "right"
+        ctx.fillText(minY, w, h - w / 60)
+        ctx.textBaseline = "top"
+        ctx.fillText(maxY, w, w / 60)
+
+        ctx.beginPath()
+        for (i = 0; i < n; ++i) {
+            info = ratingModel.history[i]
+            ctx.lineTo((info.number - minX)  / (maxX - minX) * w,
+                       h - (info.rating - minY)  / (maxY - minY) * h)
+        }
+        ctx.strokeStyle = "red"
+        ctx.stroke()
+
+        ctx.restore()
+    }
+}
diff --git a/src/pentobi/qml/SaveDialog.qml b/src/pentobi/qml/SaveDialog.qml
new file mode 100644 (file)
index 0000000..b4f6524
--- /dev/null
@@ -0,0 +1,21 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/SaveDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.FileDialog {
+    title: qsTr("Save")
+    selectExisting: false
+    nameFilterLabels: [ qsTr("Blokus games") ]
+    nameFilters: [ [ "*.blksgf", "*.BLKSGF" ] ]
+    folder: rootWindow.folder
+    onAccepted: {
+        rootWindow.folder = folder
+        Logic.saveFile(Logic.getFileFromUrl(fileUrl))
+    }
+}
diff --git a/src/pentobi/qml/ScoreDisplay.qml b/src/pentobi/qml/ScoreDisplay.qml
new file mode 100644 (file)
index 0000000..646b65b
--- /dev/null
@@ -0,0 +1,131 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ScoreDisplay.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.0
+
+Item {
+    id: root
+
+    property int altPlayer: gameModel.altPlayer
+    property real points0: gameModel.points0
+    property real points1: gameModel.points1
+    property real points2: gameModel.points2
+    property real points3: gameModel.points3
+    property real bonus0: gameModel.bonus0
+    property real bonus1: gameModel.bonus1
+    property real bonus2: gameModel.bonus2
+    property real bonus3: gameModel.bonus3
+    property bool hasMoves0: gameModel.hasMoves0
+    property bool hasMoves1: gameModel.hasMoves1
+    property bool hasMoves2: gameModel.hasMoves2
+    property bool hasMoves3: gameModel.hasMoves3
+
+    RowLayout {
+        id: rowLayout
+
+        width: root.width
+        height: Math.min(root.height, 0.047 * root.width)
+        anchors.centerIn: parent
+        spacing: 0
+
+        Item { Layout.fillWidth: true }
+        ScoreElement2 {
+            id: playerScore0
+
+            visible: gameModel.nuColors === 4 && gameModel.nuPlayers === 2
+            value: points0 + points2
+            isFinal: ! hasMoves0 && ! hasMoves2
+            fontSize: rowLayout.height
+            color1: gameDisplay.color0[0]
+            color2: gameDisplay.color2[0]
+            // Avoid position changes unless score text gets really long
+            Layout.minimumWidth: 3 * fontSize
+        }
+        Item { visible: playerScore0.visible; Layout.fillWidth: true }
+        ScoreElement2 {
+            id: playerScore1
+
+            visible: gameModel.nuColors === 4 && gameModel.nuPlayers === 2
+            value: points1 + points3
+            isFinal: ! hasMoves1 && ! hasMoves3
+            fontSize: rowLayout.height
+            color1: gameDisplay.color1[0]
+            color2: gameDisplay.color3[0]
+            // Avoid position changes unless score text gets really long
+            Layout.minimumWidth: 3 * fontSize
+        }
+        Item { visible: playerScore1.visible; Layout.fillWidth: true }
+        ScoreElement {
+            id: colorScore0
+
+            value: points0
+            bonus: bonus0
+            isFinal: ! hasMoves0
+            fontSize: rowLayout.height
+            color: color0[0]
+            // Avoid position changes unless score text gets really long
+            Layout.minimumWidth: 2.3 * fontSize
+        }
+        Item { visible: colorScore0.visible; Layout.fillWidth: true }
+        ScoreElement {
+            id: colorScore1
+
+            value: points1
+            bonus: bonus1
+            isFinal: ! hasMoves1
+            fontSize: rowLayout.height
+            color: color1[0]
+            // Avoid position changes unless score text gets really long
+            Layout.minimumWidth: 2.3 * fontSize
+        }
+        Item { visible: colorScore1.visible; Layout.fillWidth: true }
+        ScoreElement {
+            id: colorScore2
+
+            visible: gameModel.nuColors > 2
+            value: points2
+            bonus: bonus2
+            isFinal: ! hasMoves2
+            fontSize: rowLayout.height
+            color: color2[0]
+            // Avoid position changes unless score text gets really long
+            Layout.minimumWidth: 2.3 * fontSize
+        }
+        Item { visible: colorScore2.visible; Layout.fillWidth: true }
+        ScoreElement {
+            id: colorScore3
+
+            visible: gameModel.nuColors > 3
+                     && gameModel.gameVariant !== "classic_3"
+            value: points3
+            bonus: bonus3
+            isFinal: ! hasMoves3
+            fontSize: rowLayout.height
+            color: color3[0]
+            // Avoid position changes unless score text gets really long
+            Layout.minimumWidth: 2.3 * fontSize
+        }
+        Item { visible: colorScore3.visible; Layout.fillWidth: true }
+        ScoreElement2 {
+            id: altColorIndicator
+
+            visible: gameModel.gameVariant === "classic_3" && hasMoves3
+            value: points3
+            isAltColor: true
+            isFinal: ! hasMoves3
+            fontSize: rowLayout.height
+            color1: theme.colorGreen[0]
+            color2:
+                switch (altPlayer) {
+                case 0: return gameDisplay.color0[0]
+                case 1: return gameDisplay.color1[0]
+                case 2: return gameDisplay.color2[0]
+                }
+        }
+        Item { visible: altColorIndicator.visible; Layout.fillWidth: true }
+    }
+}
diff --git a/src/pentobi/qml/ScoreElement.qml b/src/pentobi/qml/ScoreElement.qml
new file mode 100644 (file)
index 0000000..cf17dd4
--- /dev/null
@@ -0,0 +1,43 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ScoreElement.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Item {
+    property alias color: point.color
+    property bool isFinal
+    property real value
+    property real bonus
+    property alias fontSize: text.font.pixelSize
+
+    implicitWidth: point.implicitWidth + text.implicitWidth
+                   + text.anchors.leftMargin
+    implicitHeight: Math.max(point.implicitHeight, text.implicitHeight)
+
+    Rectangle {
+        id: point
+
+        anchors.verticalCenter: parent.verticalCenter
+        implicitWidth: 0.7 * fontSize
+        implicitHeight: 0.7 * fontSize
+        radius: width / 2
+    }
+    Text {
+        id: text
+
+        anchors {
+            verticalCenter: parent.verticalCenter
+            left: point.right
+            leftMargin: 0.14 * font.pixelSize
+        }
+        text: ! isFinal ?
+                  "%L1".arg(value) :
+                  "%1<u>%L2</u>".arg(bonus > 0 ? "★" : "").arg(value)
+        color: theme.colorText
+        opacity: 0.8
+        font.preferShaping: false
+    }
+}
diff --git a/src/pentobi/qml/ScoreElement2.qml b/src/pentobi/qml/ScoreElement2.qml
new file mode 100644 (file)
index 0000000..872f107
--- /dev/null
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ScoreElement2.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Item {
+    id: root
+
+    property color color1
+    property color color2
+    property bool isFinal
+    property bool isAltColor
+    property real value
+    property alias fontSize: text.font.pixelSize
+
+    implicitWidth: point1.implicitWidth + point1.implicitWidth
+                   + text.implicitWidth + text.anchors.leftMargin
+    implicitHeight: Math.max(point1.implicitHeight, point2.implicitHeight,
+                             text.implicitHeight)
+
+    Rectangle {
+        id: point1
+
+        anchors.verticalCenter: parent.verticalCenter
+        implicitWidth: 0.7 * fontSize
+        implicitHeight: 0.7 * fontSize
+        color: color1
+        opacity: isAltColor && isFinal ? 0 : 1
+        radius: width / 2
+    }
+    Rectangle {
+        id: point2
+
+        anchors {
+            verticalCenter: parent.verticalCenter
+            left: point1.right
+        }
+        implicitWidth: 0.7 * fontSize
+        implicitHeight: 0.7 * fontSize
+        color: isAltColor && isFinal ? color1 : color2
+        radius: width / 2
+    }
+    Text {
+        id: text
+
+        anchors {
+            verticalCenter: parent.verticalCenter
+            left: point2.right
+            leftMargin: 0.14 * font.pixelSize
+        }
+        text: isAltColor ? ""
+                         : isFinal ? "<u>%L1</u>".arg(value) : "%L1".arg(value)
+        color: theme.colorText
+        opacity: 0.8
+        font.preferShaping: false
+    }
+}
diff --git a/src/pentobi/qml/Square.qml b/src/pentobi/qml/Square.qml
new file mode 100644 (file)
index 0000000..18b2c5a
--- /dev/null
@@ -0,0 +1,163 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Square.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+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. All
+// images have two versions with the sourceSize optimzed for the statically
+// displayed states on the board and in the piece selector, which produces
+// better results than using Image.mipmap and avoids a mipmap bug with Nvidia
+// cards (QTBUG-57845).
+Item {
+    Loader {
+        opacity: imageOpacity0
+        sourceComponent: opacity > 0 || item ? component0 : null
+
+        Component {
+            id: component0
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacitySmall0
+        sourceComponent: opacity > 0 || item ? componentSmall0 : null
+
+        Component {
+            id: componentSmall0
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                antialiasing: true
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacity90
+        sourceComponent: opacity > 0 || item ? component90 : null
+
+        Component {
+            id: component90
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+                rotation: -90
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacitySmall90
+        sourceComponent: opacity > 0 || item ? componentSmall90 : null
+
+        Component {
+            id: componentSmall90
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                antialiasing: true
+                rotation: -90
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacity180
+        sourceComponent: opacity > 0 || item ? component180 : null
+
+        Component {
+            id: component180
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+                rotation: -180
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacitySmall180
+        sourceComponent: opacity > 0 || item ? componentSmall180 : null
+
+        Component {
+            id: componentSmall180
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                antialiasing: true
+                rotation: -180
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacity270
+        sourceComponent: opacity > 0 || item ? component270 : null
+
+        Component {
+            id: component270
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+                rotation: -270
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacitySmall270
+        sourceComponent: opacity > 0 || item ? componentSmall270 : null
+
+        Component {
+            id: componentSmall270
+
+            Image {
+                source: imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                antialiasing: true
+                rotation: -270
+            }
+        }
+    }
+}
diff --git a/src/pentobi/qml/ToolBar.qml b/src/pentobi/qml/ToolBar.qml
new file mode 100644 (file)
index 0000000..cf2666d
--- /dev/null
@@ -0,0 +1,348 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ToolBar.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.0
+import QtQuick.Layouts 1.1
+import "." as Pentobi
+import "Main.js" as Logic
+
+Item {
+    id: root
+
+    // Show toolbar content (menu button is always shown)
+    property bool showContent: true
+
+    function clickMenuButton() {
+        menuButton.checked = true
+        menuButton.onClicked()
+        menu.item.currentIndex = 0
+    }
+
+    implicitWidth: rowLayout.implicitWidth
+    implicitHeight: rowLayout.implicitHeight
+
+    RowLayout {
+        id: rowLayout
+
+        anchors.fill: parent
+        spacing: 0
+
+        // Like the label used for desktop after the toolbuttons, but with
+        // shorter text for small smartphone screens
+        Label {
+            id: mobileLabel
+
+            visible: ! isDesktop && showContent
+            color: theme.colorText
+            opacity: isRated ? 0.6 : 0.8
+            elide: Text.ElideRight
+            text: Logic.getGameLabel(gameDisplay.setupMode, isRated,
+                                     gameModel.file, gameModel.isModified, true)
+            // There is a bug in Qt 5.11 that in some situations elides the
+            // text even if there is enough room for it. It doesn't occur if
+            // we use implicitWidth + 1 instead if implicitWidth
+            Layout.maximumWidth: implicitWidth + 1
+            Layout.fillWidth: true
+            Layout.leftMargin: root.height / 10
+
+            MouseArea {
+                anchors.fill: parent
+                onClicked: if (mobileLabel.truncated) ToolTip.visible = true
+                ToolTip.text: mobileLabel.text
+                ToolTip.timeout: 2000
+            }
+        }
+        Item {
+            visible: ! isDesktop
+            Layout.fillWidth: true
+        }
+        Pentobi.Button {
+            id: newGame
+
+            icon.source: theme.getImage("pentobi-newgame")
+            action: actionNew
+            visible: showContent && (isDesktop || enabled)
+        }
+        Pentobi.Button {
+            id: newGameRated
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-rated-game")
+            action: actionNewRated
+        }
+        Pentobi.Button {
+            id: undo
+
+            icon.source: theme.getImage("pentobi-undo")
+            action: actionUndo
+            visible: showContent && (isDesktop || enabled)
+            autoRepeat: true
+            autoRepeatInterval:
+                rootWindow.gameDisplay.item ?
+                    2 * rootWindow.gameDisplay.item.animationDuration : 400
+
+        }
+        Pentobi.Button {
+            id: computerSettings
+
+            icon.source: theme.getImage("pentobi-computer-colors")
+            action: actionComputerSettings
+            visible: showContent && (isDesktop || enabled)
+        }
+        Pentobi.Button {
+            id: play
+
+            icon.source: theme.getImage("pentobi-play")
+            action: actionPlay
+            visible: showContent && (isDesktop || enabled)
+            autoRepeat: true
+            autoRepeatInterval:
+                rootWindow.gameDisplay.item ?
+                    rootWindow.gameDisplay.item.animationDuration : 200
+        }
+        Pentobi.Button {
+            id: stop
+
+            icon.source: theme.getImage("pentobi-stop")
+            action: actionStop
+            visible: showContent && (isDesktop || ! isRated)
+        }
+        Item {
+            visible: isDesktop
+            Layout.fillWidth: true
+            Layout.maximumWidth: 0.3 * parent.height
+        }
+        Pentobi.Button {
+            id: beginning
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-beginning")
+            action: actionBeginning
+        }
+        Pentobi.Button {
+            id: backward10
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-backward10")
+            action: actionBackward10
+            autoRepeat: true
+            autoRepeatInterval:
+                rootWindow.gameDisplay.item ?
+                    rootWindow.gameDisplay.item.animationDuration : 200
+        }
+        Pentobi.Button {
+            id: backward
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-backward")
+            action: actionBackward
+            autoRepeat: true
+        }
+        Pentobi.Button {
+            id: forward
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-forward")
+            action: actionForward
+            autoRepeat: true
+        }
+        Pentobi.Button {
+            id: forward10
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-forward10")
+            action: actionForward10
+            autoRepeat: true
+            autoRepeatInterval:
+                rootWindow.gameDisplay.item ?
+                    rootWindow.gameDisplay.item.animationDuration : 200
+        }
+        Pentobi.Button {
+            id: end
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-end")
+            action: actionEnd
+        }
+        Item {
+            visible: isDesktop
+            Layout.fillWidth: true
+            Layout.maximumWidth: 0.3 * parent.height
+        }
+        Pentobi.Button {
+            id: prevVar
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-previous-variation")
+            action: actionPrevVar
+            autoRepeat: true
+            autoRepeatInterval:
+                rootWindow.gameDisplay.item ?
+                    2 * rootWindow.gameDisplay.item.animationDuration : 400
+        }
+        Pentobi.Button {
+            id: nextVar
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-next-variation")
+            action: actionNextVar
+            autoRepeat: true
+            autoRepeatInterval:
+                rootWindow.gameDisplay.item ?
+                    2 * rootWindow.gameDisplay.item.animationDuration : 400
+        }
+        Item {
+            visible: isDesktop
+            Layout.fillWidth: true
+            Layout.maximumWidth: 0.3 * parent.height
+        }
+        Label {
+            visible: showContent && isDesktop
+            text: Logic.getGameLabel(gameDisplay.setupMode, isRated,
+                                     gameModel.file, gameModel.isModified, false)
+            color: theme.colorText
+            opacity: 0.8
+            elide: Text.ElideRight
+            // See comment at Layout.maximumWidth of first label
+            Layout.maximumWidth: implicitWidth + 1
+            Layout.fillWidth: true
+
+            MouseArea {
+                anchors.fill: parent
+                hoverEnabled: true
+                ToolTip.text: Logic.getFileInfo(isRated, gameModel.file,
+                                                gameModel.isModified)
+                ToolTip.visible: containsMouse && ! gameDisplay.setupMode
+                                 && (gameModel.file !== "" || isRated)
+                ToolTip.delay: 1000
+                ToolTip.timeout: 7000
+            }
+        }
+        Item {
+            Layout.fillWidth: true
+            Layout.maximumWidth: isDesktop ? root.width : 0.3 * parent.height
+        }
+        Pentobi.Button {
+            id: menuButton
+
+            icon.source: theme.getImage("menu")
+            down: pressed || (isDesktop && menu.item && menu.item.opened)
+            onClicked: {
+                if (! menu.item)
+                    menu.sourceComponent = menuComponent
+                if (menu.item.opened)
+                    menu.item.close()
+                else {
+                    gameDisplay.dropCommentFocus()
+                    menu.item.popup(0, isDesktop ? height : 0)
+                }
+            }
+
+            Loader {
+                id: menu
+
+                // Having the loader fill the button together with
+                // CloseOnPressOutsideParent and the function used in onClicked
+                // seems to be the only way to make a click on the button close
+                // the menu if it is already open. Is there a better way?
+                anchors.fill: parent
+
+                Component {
+                    id: menuComponent
+
+                    Pentobi.Menu {
+                        dynamicWidth: false
+                        width: Math.min(font.pixelSize * (isDesktop ? 11 : 18),
+                                        rootWindow.contentItem.width)
+                        closePolicy: Popup.CloseOnPressOutsideParent
+                                     | Popup.CloseOnEscape
+
+                        MenuGame { }
+                        MenuGo { }
+                        MenuEdit { }
+                        MenuView { }
+                        MenuComputer { }
+                        MenuTools { }
+                        MenuHelp { }
+                    }
+                }
+            }
+        }
+    }
+    ButtonToolTip {
+        button: newGame
+        ToolTip.text: qsTr("Start a new game")
+    }
+    ButtonToolTip {
+        button: newGameRated
+        ToolTip.text: qsTr("Start a rated game")
+    }
+    ButtonToolTip {
+        button: undo
+        //: Tooltip for Undo button
+        ToolTip.text: qsTr("Undo move")
+    }
+    ButtonToolTip {
+        button: computerSettings
+        ToolTip.text: qsTr("Set the colors played by the computer")
+    }
+    ButtonToolTip {
+        button: play
+        ToolTip.text: {
+            var toPlay = gameModel.toPlay
+            if (gameModel.gameVariant === "classic_3" && toPlay === 3)
+                toPlay = gameModel.altPlayer
+            if ((computerPlays0 && toPlay === 0)
+                    || (computerPlays1 && toPlay === 1)
+                    || (computerPlays2 && toPlay === 2)
+                    || (computerPlays3 && toPlay === 3))
+                return qsTr("Make the computer continue to play the current color")
+            return qsTr("Make the computer play the current color")
+        }
+    }
+    ButtonToolTip {
+        button: stop
+        ToolTip.text: analyzeGameModel.isRunning ?
+                          qsTr("Abort game analysis")
+                        : qsTr("Abort computer move")
+    }
+    ButtonToolTip {
+        button: beginning
+        ToolTip.text: qsTr("Go to beginning of game")
+    }
+    ButtonToolTip {
+        button: backward10
+        ToolTip.text: qsTr("Go ten moves backward")
+    }
+    ButtonToolTip {
+        button: backward
+        ToolTip.text: qsTr("Go one move backward")
+    }
+    ButtonToolTip {
+        button: forward
+        ToolTip.text: qsTr("Go one move forward")
+    }
+    ButtonToolTip {
+        button: forward10
+        ToolTip.text: qsTr("Go ten moves forward")
+    }
+    ButtonToolTip {
+        button: end
+        ToolTip.text: qsTr("Go to end of moves")
+    }
+    ButtonToolTip {
+        button: prevVar
+        ToolTip.text: qsTr("Go to previous variation")
+    }
+    ButtonToolTip {
+        button: nextVar
+        ToolTip.text: qsTr("Go to next variation")
+    }
+    ButtonToolTip {
+        button: menuButton
+    }
+}
diff --git a/src/pentobi/qml/Triangle.qml b/src/pentobi/qml/Triangle.qml
new file mode 100644 (file)
index 0000000..1352170
--- /dev/null
@@ -0,0 +1,293 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Triangle.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+
+// Piece element used in Trigon. See Square.qml for comments
+Item {
+    property bool isDownward
+
+    Loader {
+        opacity: imageOpacity0
+        sourceComponent: opacity > 0 || item ? component0 : null
+
+        Component {
+            id: component0
+
+            Image {
+                source: isDownward ? imageNameDownward : imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacitySmall0
+        sourceComponent: opacity > 0 || item ? componentSmall0 : null
+
+        Component {
+            id: componentSmall0
+
+            Image {
+                source: isDownward ? imageNameDownward : imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                antialiasing: true
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacity60
+        sourceComponent: opacity > 0 || item ? component60 : null
+
+        Component {
+            id: component60
+
+            Image {
+                source: isDownward ? imageName : imageNameDownward
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                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 {
+        opacity: imageOpacitySmall60
+        sourceComponent: opacity > 0 || item ? componentSmall60 : null
+
+        Component {
+            id: componentSmall60
+
+            Image {
+                source: isDownward ? imageName : imageNameDownward
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                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 {
+        opacity: imageOpacity120
+        sourceComponent: opacity > 0 || item ? component120 : null
+
+        Component {
+            id: component120
+
+            Image {
+                source: isDownward ? imageNameDownward : imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+                transform: Rotation {
+                    angle: -120
+                    origin {
+                        x: width / 2
+                        y: isDownward ? height / 3 : 2 * height / 3
+                    }
+                }
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacitySmall120
+        sourceComponent: opacity > 0 || item ? componentSmall120 : null
+
+        Component {
+            id: componentSmall120
+
+            Image {
+                source: isDownward ? imageNameDownward : imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                antialiasing: true
+                transform: Rotation {
+                    angle: -120
+                    origin {
+                        x: width / 2
+                        y: isDownward ? height / 3 : 2 * height / 3
+                    }
+                }
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacity180
+        sourceComponent: opacity > 0 || item ? component180 : null
+
+        Component {
+            id: component180
+
+            Image {
+                source: isDownward ? imageName : imageNameDownward
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+                rotation: -180
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacitySmall180
+        sourceComponent: opacity > 0 || item ? componentSmall180 : null
+
+        Component {
+            id: componentSmall180
+
+            Image {
+                source: isDownward ? imageName : imageNameDownward
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                antialiasing: true
+                rotation: -180
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacity240
+        sourceComponent: opacity > 0 || item ? component240 : null
+
+        Component {
+            id: component240
+
+            Image {
+                source: isDownward ? imageNameDownward : imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+                transform: Rotation {
+                    angle: -240
+                    origin {
+                        x: width / 2
+                        y: isDownward ? height / 3 : 2 * height / 3
+                    }
+                }
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacitySmall240
+        sourceComponent: opacity > 0 || item ? componentSmall240 : null
+
+        Component {
+            id: componentSmall240
+
+            Image {
+                source: isDownward ? imageNameDownward : imageName
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                antialiasing: true
+                transform: Rotation {
+                    angle: -240
+                    origin {
+                        x: width / 2
+                        y: isDownward ? height / 3 : 2 * height / 3
+                    }
+                }
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacity300
+        sourceComponent: opacity > 0 || item ? component300 : null
+
+        Component {
+            id: component300
+
+            Image {
+                source: isDownward ? imageName : imageNameDownward
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize: imageSourceSize
+                antialiasing: true
+                transform: [
+                    Rotation {
+                        angle: -300
+                        origin {
+                            x: width / 2
+                            y: isDownward ? 2 * height / 3 : height / 3
+                        }
+                    },
+                    Translate { y:  isDownward ? -height / 3 : height / 3 }
+                ]
+            }
+        }
+    }
+    Loader {
+        opacity: imageOpacitySmall300
+        sourceComponent: opacity > 0 || item ? componentSmall300 : null
+
+        Component {
+            id: componentSmall300
+
+            Image {
+                source: isDownward ? imageName : imageNameDownward
+                width: imageSourceSize.width
+                height: imageSourceSize.height
+                sourceSize {
+                    width: scaleUnplayed * imageSourceSize.width
+                    height: scaleUnplayed * imageSourceSize.height
+                }
+                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/i18n/qml_de.ts b/src/pentobi/qml/i18n/qml_de.ts
new file mode 100644 (file)
index 0000000..bfb5c31
--- /dev/null
@@ -0,0 +1,1744 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="de">
+<context>
+    <name>AboutDialog</name>
+    <message>
+        <source>Copyright © 2011–%1 Markus Enzenberger</source>
+        <translation>Copyright © 2011–%1 Markus Enzenberger</translation>
+    </message>
+    <message>
+        <source>Computer opponent for the board game Blokus</source>
+        <translation>Computer-Gegner für das Brettspiel Blokus</translation>
+    </message>
+    <message>
+        <source>Pentobi %1</source>
+        <extracomment>The argument is the application version.</extracomment>
+        <translation>Pentobi %1</translation>
+    </message>
+</context>
+<context>
+    <name>Actions</name>
+    <message>
+        <source>Main Variation</source>
+        <translation type="vanished">Hauptvariante</translation>
+    </message>
+    <message>
+        <source>Beginning of Branch</source>
+        <translation type="vanished">Verzweigungsanfang</translation>
+    </message>
+    <message>
+        <source>Settings…</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation type="vanished">Einstellungen …</translation>
+    </message>
+    <message>
+        <source>Find Move</source>
+        <translation type="vanished">Zug finden</translation>
+    </message>
+    <message>
+        <source>Next Comment</source>
+        <translation type="vanished">Nächster Kommentar</translation>
+    </message>
+    <message>
+        <source>Fullscreen</source>
+        <translation type="vanished">Vollbild</translation>
+    </message>
+    <message>
+        <source>Move Number…</source>
+        <translation type="vanished">Zugnummer …</translation>
+    </message>
+    <message>
+        <source>Pentobi Help</source>
+        <translation type="vanished">Pentobi-Hilfe</translation>
+    </message>
+    <message>
+        <source>New</source>
+        <translation type="vanished">Neu</translation>
+    </message>
+    <message>
+        <source>Rated Game</source>
+        <translation type="vanished">Gewertetes Spiel</translation>
+    </message>
+    <message>
+        <source>Open…</source>
+        <translation type="vanished">Öffnen …</translation>
+    </message>
+    <message>
+        <source>Play</source>
+        <translation type="vanished">Spielen</translation>
+    </message>
+    <message>
+        <source>Play Move</source>
+        <extracomment>Play a single move</extracomment>
+        <translation type="vanished">Zug spielen</translation>
+    </message>
+    <message>
+        <source>Quit</source>
+        <translation type="vanished">Beenden</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation type="vanished">Speichern</translation>
+    </message>
+    <message>
+        <source>Save As…</source>
+        <translation type="vanished">Speichern unter …</translation>
+    </message>
+    <message>
+        <source>Stop</source>
+        <translation type="vanished">Stopp</translation>
+    </message>
+    <message>
+        <source>Undo Move</source>
+        <translation type="vanished">Zug rückgängig</translation>
+    </message>
+    <message>
+        <source>Game Info</source>
+        <translation type="vanished">Spielinformation</translation>
+    </message>
+    <message>
+        <source>Comment</source>
+        <translation type="vanished">Kommentar</translation>
+    </message>
+    <message>
+        <source>Settings</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation type="vanished">Einstellungen</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeDialog</name>
+    <message>
+        <source>Analysis speed:</source>
+        <translation>Analysegeschwindigkeit:</translation>
+    </message>
+    <message>
+        <source>Fast</source>
+        <translation>Schnell</translation>
+    </message>
+    <message>
+        <source>Normal</source>
+        <translation>Normal</translation>
+    </message>
+    <message>
+        <source>Slow</source>
+        <translation>Langsam</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeGame</name>
+    <message>
+        <source>(No analysis)</source>
+        <translation>(Keine Analyse)</translation>
+    </message>
+</context>
+<context>
+    <name>AppearanceDialog</name>
+    <message>
+        <source>Coordinates</source>
+        <translation>Koordinaten</translation>
+    </message>
+    <message>
+        <source>Show variations</source>
+        <translation>Varianten zeigen</translation>
+    </message>
+    <message>
+        <source>Light</source>
+        <translation>Hell</translation>
+    </message>
+    <message>
+        <source>Dark</source>
+        <translation>Dunkel</translation>
+    </message>
+    <message>
+        <source>Colorblind light</source>
+        <translation>Farbenblind hell</translation>
+    </message>
+    <message>
+        <source>Colorblind dark</source>
+        <translation>Farbenblind dunkel</translation>
+    </message>
+    <message>
+        <source>System</source>
+        <extracomment>Name of theme using default system colors</extracomment>
+        <translation>System</translation>
+    </message>
+    <message>
+        <source>Move marking:</source>
+        <translation>Zugmarkierung:</translation>
+    </message>
+    <message>
+        <source>Last with dot</source>
+        <translation>Letzter mit Punkt</translation>
+    </message>
+    <message>
+        <source>Last with number</source>
+        <translation>Letzter mit Nummer</translation>
+    </message>
+    <message>
+        <source>All with number</source>
+        <translation>Alle mit Nummer</translation>
+    </message>
+    <message>
+        <source>None</source>
+        <extracomment>Move marking/None</extracomment>
+        <translation>Keine</translation>
+    </message>
+    <message>
+        <source>Animations</source>
+        <translation>Animationen</translation>
+    </message>
+    <message>
+        <source>Show comment:</source>
+        <translation>Kommentar zeigen:</translation>
+    </message>
+    <message>
+        <source>Always</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Immer</translation>
+    </message>
+    <message>
+        <source>As needed</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Bei Bedarf</translation>
+    </message>
+    <message>
+        <source>Never</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Nie</translation>
+    </message>
+    <message>
+        <source>Color theme:</source>
+        <translation>Farbthema:</translation>
+    </message>
+    <message>
+        <source>Move number</source>
+        <extracomment>Check box in appearance dialog whether to show the move number in the status bar.</extracomment>
+        <translation>Zugnummer</translation>
+    </message>
+</context>
+<context>
+    <name>AsciiArtSaveDialog</name>
+    <message>
+        <source>Export ASCII Art</source>
+        <translation>ASCII-Art exportieren</translation>
+    </message>
+    <message>
+        <source>Text files</source>
+        <translation>Textdateien</translation>
+    </message>
+</context>
+<context>
+    <name>BoardContextMenu</name>
+    <message>
+        <source>Go to Move %1</source>
+        <translation>Gehe zu Zug %1</translation>
+    </message>
+    <message>
+        <source>Move Annotation</source>
+        <translation>Annotierung</translation>
+    </message>
+    <message>
+        <source>Move Annotation (%1)</source>
+        <extracomment>The argument is the annotation symbol for the current move</extracomment>
+        <translation>Annotierung (%1)</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonApply</name>
+    <message>
+        <source>Apply</source>
+        <translation>Anwenden</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonCancel</name>
+    <message>
+        <source>Cancel</source>
+        <translation>Abbrechen</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonClose</name>
+    <message>
+        <source>Close</source>
+        <translation>Schließen</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonOk</name>
+    <message>
+        <source>OK</source>
+        <translation>OK</translation>
+    </message>
+</context>
+<context>
+    <name>ComputerDialog</name>
+    <message>
+        <source>Computer plays:</source>
+        <translation>Computer spielt:</translation>
+    </message>
+    <message>
+        <source>Blue/Red</source>
+        <translation>Blau/Rot</translation>
+    </message>
+    <message>
+        <source>Purple</source>
+        <translation>Lila</translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation>Grün</translation>
+    </message>
+    <message>
+        <source>Blue</source>
+        <translation>Blau</translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation>Gelb/Grün</translation>
+    </message>
+    <message>
+        <source>Orange</source>
+        <translation>Orange</translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation>Gelb</translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation>Rot</translation>
+    </message>
+    <message>
+        <source>Level %1</source>
+        <translation>Stufe %1</translation>
+    </message>
+</context>
+<context>
+    <name>ExportImageDialog</name>
+    <message>
+        <source>Image width:</source>
+        <translation>Bildbreite:</translation>
+    </message>
+</context>
+<context>
+    <name>FileDialog</name>
+    <message>
+        <source>Overwrite file?</source>
+        <translation>Datei überschreiben?</translation>
+    </message>
+    <message>
+        <source>Open</source>
+        <translation>Öffnen</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>Speichern</translation>
+    </message>
+    <message>
+        <source>All files</source>
+        <translation>Alle Dateien</translation>
+    </message>
+</context>
+<context>
+    <name>GameDisplayDesktop</name>
+    <message>
+        <source>Computer is thinking…</source>
+        <translation>Computer denkt …</translation>
+    </message>
+    <message>
+        <source>Running game analysis…</source>
+        <translation>Spiel wird analysiert …</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>
+</context>
+<context>
+    <name>GameInfoDialog</name>
+    <message>
+        <source>Player Blue/Red:</source>
+        <translation>Spieler Blau/Rot:</translation>
+    </message>
+    <message>
+        <source>Player Purple:</source>
+        <translation>Spieler Lila:</translation>
+    </message>
+    <message>
+        <source>Player Green:</source>
+        <translation>Spieler Grün:</translation>
+    </message>
+    <message>
+        <source>Player Blue:</source>
+        <translation>Spieler Blau:</translation>
+    </message>
+    <message>
+        <source>Player Yellow/Green:</source>
+        <translation>Spieler Gelb/Grün:</translation>
+    </message>
+    <message>
+        <source>Player Orange:</source>
+        <translation>Spieler Orange:</translation>
+    </message>
+    <message>
+        <source>Player Yellow:</source>
+        <translation>Spieler Gelb:</translation>
+    </message>
+    <message>
+        <source>Player Red:</source>
+        <translation>Spieler Rot:</translation>
+    </message>
+    <message>
+        <source>Date:</source>
+        <translation>Datum:</translation>
+    </message>
+    <message>
+        <source>Time:</source>
+        <translation>Bedenkzeit:</translation>
+    </message>
+    <message>
+        <source>Event:</source>
+        <translation>Veranstaltung:</translation>
+    </message>
+    <message>
+        <source>Round:</source>
+        <translation>Runde:</translation>
+    </message>
+</context>
+<context>
+    <name>GameModel</name>
+    <message>
+        <source>Blue/Red</source>
+        <translation>Blau/Rot</translation>
+    </message>
+    <message>
+        <source>Purple</source>
+        <translation>Lila</translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation>Grün</translation>
+    </message>
+    <message>
+        <source>Blue</source>
+        <translation>Blau</translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation>Gelb/Grün</translation>
+    </message>
+    <message>
+        <source>Orange</source>
+        <translation>Orange</translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation>Gelb</translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation>Rot</translation>
+    </message>
+    <message>
+        <source>Purple wins with 1 point.</source>
+        <translation>Lila gewinnt mit 1 Punkt.</translation>
+    </message>
+    <message>
+        <source>Purple wins with %L1 points.</source>
+        <translation>Lila gewinnt mit %L1 Punkten.</translation>
+    </message>
+    <message>
+        <source>Orange wins with 1 point.</source>
+        <translation>Orange gewinnt mit 1 Punkt.</translation>
+    </message>
+    <message>
+        <source>Orange wins with %L1 points.</source>
+        <translation>Orange gewinnt mit %L1 Punkten.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie.</source>
+        <translation>Spiel endet unentschieden.</translation>
+    </message>
+    <message>
+        <source>Green wins with 1 point.</source>
+        <translation>Grün gewinnt mit 1 Punkt.</translation>
+    </message>
+    <message>
+        <source>Green wins with %L1 points.</source>
+        <translation>Grün gewinnt mit %L1 Punkten.</translation>
+    </message>
+    <message>
+        <source>Blue wins with 1 point.</source>
+        <translation>Blau gewinnt mit 1 Punkt.</translation>
+    </message>
+    <message>
+        <source>Blue wins with %L1 points.</source>
+        <translation>Blau gewinnt mit %L1 Punkten.</translation>
+    </message>
+    <message>
+        <source>Green wins (tie resolved).</source>
+        <translation>Grün gewinnt (Unentschieden aufgelöst).</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 %L1 points.</source>
+        <translation>Blau/Rot gewinnt mit %L1 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 %L1 points.</source>
+        <translation>Gelb/Grün gewinnt mit %L1 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 unentschieden zwischen Blau und Gelb.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Red.</source>
+        <translation>Spiel endet unentschieden zwischen Blau und Rot.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow and Red.</source>
+        <translation>Spiel endet unentschieden zwischen Gelb und Rot.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between all players.</source>
+        <translation>Spiel endet 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 unentschieden zwischen Blau, Gelb und Rot.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Green.</source>
+        <translation>Spiel endet 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 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 unentschieden zwischen Gelb, Rot und Grün.</translation>
+    </message>
+    <message>
+        <source>Invalid Blokus SGF file. (%1)</source>
+        <translation>Ungültige Blokus-SGF-Datei. (%1)</translation>
+    </message>
+    <message>
+        <source>Clipboard is empty.</source>
+        <translation>Zwischenablage ist leer.</translation>
+    </message>
+    <message>
+        <source>Untitled</source>
+        <translation>Unbenannt</translation>
+    </message>
+    <message>
+        <source>Untitled %1</source>
+        <extracomment>The argument is a number, which will be increased if a file with the same name already exists</extracomment>
+        <translation>Unbenannt %1</translation>
+    </message>
+    <message>
+        <source>New Folder</source>
+        <translation>Neuer Ordner</translation>
+    </message>
+    <message>
+        <source>New Folder %1</source>
+        <extracomment>The argument is a number, which will be increased if a folder with the same name already exists</extracomment>
+        <translation>Neuer Ordner %1</translation>
+    </message>
+    <message>
+        <source>(Setup)</source>
+        <translation>(Stellung)</translation>
+    </message>
+    <message>
+        <source>(No moves)</source>
+        <translation>(Keine Züge)</translation>
+    </message>
+    <message>
+        <source>Move %1</source>
+        <extracomment>The argument is the current move number.</extracomment>
+        <translation>Zug %1</translation>
+    </message>
+    <message>
+        <source>Unsupported character set</source>
+        <translation>Zeichensatz nicht unterstützt</translation>
+    </message>
+</context>
+<context>
+    <name>GameVariantDialog</name>
+    <message>
+        <source>Classic</source>
+        <translation>Klassisch</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>Duo</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>Junior</translation>
+    </message>
+    <message>
+        <source>Trigon</source>
+        <translation>Trigon</translation>
+    </message>
+    <message>
+        <source>Nexos</source>
+        <translation>Nexos</translation>
+    </message>
+    <message>
+        <source>GembloQ</source>
+        <translation>GembloQ</translation>
+    </message>
+    <message>
+        <source>Callisto</source>
+        <translation>Callisto</translation>
+    </message>
+    <message>
+        <source>Players:</source>
+        <translation>Spieler:</translation>
+    </message>
+    <message>
+        <source>Colors:</source>
+        <translation>Farben:</translation>
+    </message>
+</context>
+<context>
+    <name>GotoMoveDialog</name>
+    <message>
+        <source>Move number:</source>
+        <translation>Zugnummer:</translation>
+    </message>
+</context>
+<context>
+    <name>HelpWindow</name>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Pentobi-Hilfe</translation>
+    </message>
+</context>
+<context>
+    <name>ImageSaveDialog</name>
+    <message>
+        <source>Save Image</source>
+        <translation>Grafik speichern</translation>
+    </message>
+    <message>
+        <source>PNG image files</source>
+        <translation>PNG-Bilddateien</translation>
+    </message>
+    <message>
+        <source>JPEG image files</source>
+        <translation>JPEG-Bilddateien</translation>
+    </message>
+</context>
+<context>
+    <name>InitialRatingDialog</name>
+    <message>
+        <source>Initialize your rating for this game variant.</source>
+        <translation>Initialisieren Sie Ihre Wertung für diese Spielvariante.</translation>
+    </message>
+    <message>
+        <source>Initial rating:</source>
+        <translation>Anfangswertung:</translation>
+    </message>
+    <message>
+        <source>Beginner</source>
+        <translation>Anfänger</translation>
+    </message>
+    <message>
+        <source>Expert</source>
+        <translation>Experte</translation>
+    </message>
+</context>
+<context>
+    <name>Main</name>
+    <message>
+        <source>Pentobi</source>
+        <extracomment>Window title if no file is loaded.</extracomment>
+        <translation>Pentobi</translation>
+    </message>
+    <message>
+        <source>Game analysis is only possible in main variation.</source>
+        <translation>Spielanalyse ist nur in Hauptvariante möglich.</translation>
+    </message>
+    <message>
+        <source>Autosaved game was changed by another instance of Pentobi. Overwrite?</source>
+        <translation>Automatisch gespeichertes Spiel wurde von einer anderen Instanz von Pentobi geändert. Überschreiben?</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 has decreased from %1 to %2.</source>
+        <translation>Ihre Wertung hat sich von %1 auf %2 verringert.</translation>
+    </message>
+    <message>
+        <source>Your rating stays at %1.</source>
+        <translation>Ihre Wertung bleibt bei %1.</translation>
+    </message>
+    <message>
+        <source>No permission to access storage</source>
+        <translation>Keine Berechtigung zu Zugriff auf Speicher</translation>
+    </message>
+    <message>
+        <source>Delete all rating information for the current game variant?</source>
+        <translation>Alle Wertungsinformationen für die gegenwärtige Spielvariante löschen?</translation>
+    </message>
+    <message>
+        <source>Delete all variations?</source>
+        <translation>Alle Varianten löschen?</translation>
+    </message>
+    <message>
+        <source>Save failed.</source>
+        <translation>Speichern fehlgeschlagen.</translation>
+    </message>
+    <message>
+        <source>End of tree was reached. Continue search from start of the tree?</source>
+        <translation>Ende des Spielbaums erreicht. Suche vom Start des Spielbaums fortsetzen?</translation>
+    </message>
+    <message>
+        <source>No comment found</source>
+        <translation>Kein Kommentar gefunden</translation>
+    </message>
+    <message>
+        <source>%1 (modified)</source>
+        <translation>%1 (geändert)</translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Reload?</source>
+        <translation>Datei wurde von einer anderen Anwendung bearbeitet. Neu laden?</translation>
+    </message>
+    <message>
+        <source>Continue computer move?</source>
+        <translation>Computer-Zug fortsetzen?</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>Open failed.</source>
+        <translation>Öffnen fehlgeschlagen.</translation>
+    </message>
+    <message>
+        <source>Start rated game with Purple against Pentobi level %1?</source>
+        <translation>Gewertetes Spiel mit Lila gegen Pentobi Stufe %1 beginnen?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Green against Pentobi level %1?</source>
+        <translation>Gewertetes Spiel mit Grün gegen Pentobi Stufe %1 beginnen?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue/Red against Pentobi level %1?</source>
+        <translation>Gewertetes Spiel mit Blau/Rot gegen Pentobi Stufe %1 beginnen?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue against Pentobi level %1?</source>
+        <translation>Gewertetes Spiel mit Blau gegen Pentobi Stufe %1 beginnen?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Orange against Pentobi level %1?</source>
+        <translation>Gewertetes Spiel mit Orange gegen Pentobi Stufe %1 beginnen?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow/Green against Pentobi level %1?</source>
+        <translation>Gewertetes Spiel mit Gelb/Grün gegen Pentobi Stufe %1 beginnen?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow against Pentobi level %1?</source>
+        <translation>Gewertetes Spiel mit Gelb gegen Pentobi Stufe %1 beginnen?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Red against Pentobi level %1?</source>
+        <translation>Gewertetes Spiel mit Rot gegen Pentobi Stufe %1 beginnen?</translation>
+    </message>
+    <message>
+        <source>You have not yet played rated games in this game variant.</source>
+        <translation>Sie haben noch keine gewerteten Spiele in dieser Spielvariante gespielt.</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>Discard game?</source>
+        <translation>Spiel verwerfen?</translation>
+    </message>
+    <message>
+        <source>Pentobi %1 (level %2)</source>
+        <extracomment>Player name for game info in rated game. First argument is version of Pentobi, second argument is level.</extracomment>
+        <translation>Pentobi %1 (Stufe %2)</translation>
+    </message>
+    <message>
+        <source>Human</source>
+        <extracomment>Player name for game info in rated game.</extracomment>
+        <translation>Mensch</translation>
+    </message>
+    <message>
+        <source>Rated game</source>
+        <translation>Gewertetes Spiel</translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Overwrite?</source>
+        <translation>Datei wurde von einer anderen Anwendung bearbeitet. Überschreiben?</translation>
+    </message>
+    <message>
+        <source>%1 - Pentobi</source>
+        <extracomment>Window title if file is loaded. The argument is the file name prepended with a star if the file has been modified.</extracomment>
+        <translation>%1 - Pentobi</translation>
+    </message>
+    <message>
+        <source>Not enough memory</source>
+        <translation>Nicht genügend Speicher</translation>
+    </message>
+    <message>
+        <source>Game analysis aborted</source>
+        <translation>Spielanalyse abgebrochen</translation>
+    </message>
+    <message>
+        <source>Computer move aborted</source>
+        <translation>Computer-Zug abgebrochen</translation>
+    </message>
+    <message>
+        <source>Rating information deleted</source>
+        <translation>Wertungsinformationen gelöscht</translation>
+    </message>
+    <message>
+        <source>Variations deleted</source>
+        <translation>Varianten gelöscht</translation>
+    </message>
+    <message>
+        <source>File saved</source>
+        <translation>Datei gespeichert</translation>
+    </message>
+    <message>
+        <source>Saving image failed or unsupported image format</source>
+        <translation>Grafik konnte nicht gespeichert werden oder Bildformat nicht unterstützt</translation>
+    </message>
+    <message>
+        <source>Image saved</source>
+        <translation>Grafik gespeichert</translation>
+    </message>
+    <message>
+        <source>Creating image failed</source>
+        <translation>Grafik konnte nicht erzeugt werden</translation>
+    </message>
+    <message>
+        <source>Continuing rated game</source>
+        <translation>Gewertetes Spiel wird fortgesetzt</translation>
+    </message>
+    <message>
+        <source>Kept only position</source>
+        <translation>Nur Brettstellung behalten</translation>
+    </message>
+    <message>
+        <source>Kept only subtree</source>
+        <translation>Nur Teilbaum behalten</translation>
+    </message>
+    <message>
+        <source>Variation is now %1</source>
+        <translation>Variante ist jetzt %1</translation>
+    </message>
+    <message>
+        <source>Children truncated</source>
+        <translation>Kindknoten abgeschnitten</translation>
+    </message>
+    <message>
+        <source>Setup</source>
+        <extracomment>Small-screen label for setup mode (short for &quot;Setup Mode&quot;).</extracomment>
+        <translation>Aufbau</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Stellungsaufbau</translation>
+    </message>
+    <message>
+        <source>Rated</source>
+        <extracomment>Label for ongoing rated game</extracomment>
+        <translation>Gewertet</translation>
+    </message>
+    <message>
+        <source>Rated %1</source>
+        <extracomment>Small-screen label for finished rated game (short for &quot;Rated Game&quot;). The argument is the game number.</extracomment>
+        <translation>Gewertet %1</translation>
+    </message>
+    <message>
+        <source>Rated Game %1</source>
+        <extracomment>Label for rated game. The argument is the game number.</extracomment>
+        <translation>Gewertetes Spiel %1</translation>
+    </message>
+    <message>
+        <source>Main Variation</source>
+        <translation>Hauptvariante</translation>
+    </message>
+    <message>
+        <source>Beginning of Branch</source>
+        <translation>Verzweigungsanfang</translation>
+    </message>
+    <message>
+        <source>Comment</source>
+        <translation>Kommentar</translation>
+    </message>
+    <message>
+        <source>Settings</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation>Einstellungen</translation>
+    </message>
+    <message>
+        <source>Find Move</source>
+        <translation>Zug finden</translation>
+    </message>
+    <message>
+        <source>Next Comment</source>
+        <translation>Nächster Kommentar</translation>
+    </message>
+    <message>
+        <source>Fullscreen</source>
+        <translation>Vollbild</translation>
+    </message>
+    <message>
+        <source>Game Info</source>
+        <translation>Spielinformation</translation>
+    </message>
+    <message>
+        <source>Move Number…</source>
+        <translation>Zugnummer …</translation>
+    </message>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Pentobi-Hilfe</translation>
+    </message>
+    <message>
+        <source>New</source>
+        <translation>Neu</translation>
+    </message>
+    <message>
+        <source>Rated Game</source>
+        <translation>Gewertetes Spiel</translation>
+    </message>
+    <message>
+        <source>Open…</source>
+        <translation>Öffnen …</translation>
+    </message>
+    <message>
+        <source>Play</source>
+        <translation>Spielen</translation>
+    </message>
+    <message>
+        <source>Play Move</source>
+        <extracomment>Play a single move</extracomment>
+        <translation>Zug spielen</translation>
+    </message>
+    <message>
+        <source>Quit</source>
+        <translation>Beenden</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>Speichern</translation>
+    </message>
+    <message>
+        <source>Save As…</source>
+        <translation>Speichern unter …</translation>
+    </message>
+    <message>
+        <source>Stop</source>
+        <translation>Stopp</translation>
+    </message>
+    <message>
+        <source>Undo Move</source>
+        <translation>Zug rückgängig</translation>
+    </message>
+</context>
+<context>
+    <name>MenuComputer</name>
+    <message>
+        <source>Computer</source>
+        <translation>Computer</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu Computer. Leave empty for no mnemonic.</extracomment>
+        <translation>C</translation>
+    </message>
+    <message>
+        <source>S</source>
+        <extracomment>Mnemonic for menu item Computer Settings. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu item Play. Leave empty for no mnemonic.</extracomment>
+        <translation>S</translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Play Move. Leave empty for no mnemonic.</extracomment>
+        <translation>Z</translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu item Stop. Leave empty for no mnemonic.</extracomment>
+        <translation>O</translation>
+    </message>
+</context>
+<context>
+    <name>MenuEdit</name>
+    <message>
+        <source>Edit</source>
+        <translation>Bearbeiten</translation>
+    </message>
+    <message>
+        <source>E</source>
+        <extracomment>Mnemonic for menu Edit. Leave empty for no mnemonic.</extracomment>
+        <translation>B</translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic.</extracomment>
+        <translation>H</translation>
+    </message>
+    <message>
+        <source>Make Main Variation</source>
+        <translation>Zu Hauptvariante machen</translation>
+    </message>
+    <message>
+        <source>Variation Up</source>
+        <extracomment>Short for Move Variation Up</extracomment>
+        <translation>Variante nach oben</translation>
+    </message>
+    <message>
+        <source>U</source>
+        <extracomment>Mnemonic for menu item Variation Up. Leave empty for no mnemonic.</extracomment>
+        <translation>O</translation>
+    </message>
+    <message>
+        <source>Variation Down</source>
+        <extracomment>Short for Move Variation Down</extracomment>
+        <translation>Variante nach unten</translation>
+    </message>
+    <message>
+        <source>W</source>
+        <extracomment>Mnemonic for menu item Variation Down. Leave empty for no mnemonic.</extracomment>
+        <translation>U</translation>
+    </message>
+    <message>
+        <source>Delete Variations</source>
+        <translation>Varianten löschen</translation>
+    </message>
+    <message>
+        <source>D</source>
+        <extracomment>Mnemonic for menu item Delete Variations. Leave empty for no mnemonic.</extracomment>
+        <translation>V</translation>
+    </message>
+    <message>
+        <source>Truncate</source>
+        <translation>Abschneiden</translation>
+    </message>
+    <message>
+        <source>T</source>
+        <extracomment>Mnemonic for menu item Truncate. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>Truncate Children</source>
+        <translation>Kindknoten abschneiden</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Truncate Children. Leave empty for no mnemonic.</extracomment>
+        <translation>K</translation>
+    </message>
+    <message>
+        <source>Keep Position</source>
+        <translation>Brettstellung behalten</translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu item Keep Position. Leave empty for no mnemonic.</extracomment>
+        <translation>B</translation>
+    </message>
+    <message>
+        <source>Keep Subtree</source>
+        <translation>Teilbaum behalten</translation>
+    </message>
+    <message>
+        <source>S</source>
+        <extracomment>Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic.</extracomment>
+        <translation>T</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Stellungsaufbau</translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu item Setup Mode. Leave empty for no mnemonic.</extracomment>
+        <translation>S</translation>
+    </message>
+    <message>
+        <source>Next Color</source>
+        <translation>Nächste Farbe</translation>
+    </message>
+    <message>
+        <source>N</source>
+        <extracomment>Mnemonic for menu item Next Color. Leave empty for no mnemonic.</extracomment>
+        <translation>F</translation>
+    </message>
+    <message>
+        <source>Annotation…</source>
+        <translation>Annotierung …</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item Annotation. Leave empty for no mnemonic.</extracomment>
+        <translation>N</translation>
+    </message>
+    <message>
+        <source>Made main variation</source>
+        <translation>Zu Hauptvariante gemacht</translation>
+    </message>
+</context>
+<context>
+    <name>MenuExport</name>
+    <message>
+        <source>Export</source>
+        <translation>Exportieren</translation>
+    </message>
+    <message>
+        <source>E</source>
+        <extracomment>Mnemonic for menu Export. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Image. Leave empty for no mnemonic.</extracomment>
+        <translation>G</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item ASCII Art. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>Image…</source>
+        <translation>Grafik …</translation>
+    </message>
+    <message>
+        <source>ASCII Art…</source>
+        <translation>ASCII-Art …</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGame</name>
+    <message>
+        <source>Game</source>
+        <translation>Spiel</translation>
+    </message>
+    <message>
+        <source>G</source>
+        <extracomment>Mnemonic for menu Game. Leave empty for no mnemonic.</extracomment>
+        <translation>S</translation>
+    </message>
+    <message>
+        <source>N</source>
+        <extracomment>Mnemonic for menu item New. Leave empty for no mnemonic.</extracomment>
+        <translation>N</translation>
+    </message>
+    <message>
+        <source>R</source>
+        <extracomment>Mnemonic for menu item Rated Game. Leave empty for no mnemonic.</extracomment>
+        <translation>W</translation>
+    </message>
+    <message>
+        <source>Game Variant…</source>
+        <translation>Spielvariante …</translation>
+    </message>
+    <message>
+        <source>V</source>
+        <extracomment>Mnemonic for menu item Game Variant. Leave empty for no mnemonic.</extracomment>
+        <translation>V</translation>
+    </message>
+    <message>
+        <source>I</source>
+        <extracomment>Mnemonic for menu item Game Info. Leave empty for no mnemonic.</extracomment>
+        <translation>O</translation>
+    </message>
+    <message>
+        <source>U</source>
+        <extracomment>Mnemonic for menu item Undo. Leave empty for no mnemonic.</extracomment>
+        <translation>R</translation>
+    </message>
+    <message>
+        <source>F</source>
+        <extracomment>Mnemonic for menu item Find Move. Leave empty for no mnemonic.</extracomment>
+        <translation>Z</translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu item Open. Leave empty for no mnemonic.</extracomment>
+        <translation>f</translation>
+    </message>
+    <message>
+        <source>Open Clipboard</source>
+        <translation>Zwischenablage öffnen</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>S</source>
+        <extracomment>Mnemonic for menu item Save. Leave empty for no mnemonic.</extracomment>
+        <translation>S</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item Save As. Leave empty for no mnemonic.</extracomment>
+        <translation>U</translation>
+    </message>
+    <message>
+        <source>Q</source>
+        <extracomment>Mnemonic for menu item Quit. Leave empty for no mnemonic.</extracomment>
+        <translation>B</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGo</name>
+    <message>
+        <source>Go</source>
+        <translation>Gehe zu</translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu Go. Leave empty for no mnemonic.</extracomment>
+        <translation>G</translation>
+    </message>
+    <message>
+        <source>N</source>
+        <extracomment>Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic.</extracomment>
+        <translation>N</translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic.</extracomment>
+        <translation>H</translation>
+    </message>
+    <message>
+        <source>B</source>
+        <extracomment>Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic.</extracomment>
+        <translation>V</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Next Comment. Leave empty for no mnemonic.</extracomment>
+        <translation>K</translation>
+    </message>
+</context>
+<context>
+    <name>MenuHelp</name>
+    <message>
+        <source>Help</source>
+        <translation>Hilfe</translation>
+    </message>
+    <message>
+        <source>H</source>
+        <extracomment>Mnemonic for menu Help. Leave empty for no mnemonic.</extracomment>
+        <translation>H</translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic.</extracomment>
+        <translation>P</translation>
+    </message>
+    <message>
+        <source>About Pentobi</source>
+        <translation>Über Pentobi</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item About Pentobi. Leave empty for no mnemonic.</extracomment>
+        <translation>B</translation>
+    </message>
+    <message>
+        <source>Report Bug</source>
+        <translation>Fehler melden</translation>
+    </message>
+    <message>
+        <source>B</source>
+        <extracomment>Mnemonic for menu item Report Bug. Leave empty for no mnemonic.</extracomment>
+        <translation>F</translation>
+    </message>
+</context>
+<context>
+    <name>MenuItem</name>
+    <message>
+        <source>Ctrl</source>
+        <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+        <translation>Strg</translation>
+    </message>
+    <message>
+        <source>Shift</source>
+        <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+        <translation>Umschalt</translation>
+    </message>
+</context>
+<context>
+    <name>MenuRecentFiles</name>
+    <message>
+        <source>Open Recent</source>
+        <translation>Zuletzt benutzt</translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu Open Recent. Leave empty for no mnemonic.</extracomment>
+        <translation>T</translation>
+    </message>
+    <message>
+        <source>%1. %2</source>
+        <extracomment>Format in recent files menu. First argument is the file number, second argument the file name.</extracomment>
+        <translation>%1. %2</translation>
+    </message>
+    <message>
+        <source>Clear List</source>
+        <extracomment>Menu item for clearing the recent files list</extracomment>
+        <translation>Liste leeren</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic.</extracomment>
+        <translation>L</translation>
+    </message>
+</context>
+<context>
+    <name>MenuTools</name>
+    <message>
+        <source>Tools</source>
+        <translation>Extras</translation>
+    </message>
+    <message>
+        <source>T</source>
+        <extracomment>Mnemonic for menu Tools. Leave empty for no mnemonic.</extracomment>
+        <translation>X</translation>
+    </message>
+    <message>
+        <source>Rating</source>
+        <translation>Wertung</translation>
+    </message>
+    <message>
+        <source>R</source>
+        <extracomment>Mnemonic for menu item Rating. Leave empty for no mnemonic.</extracomment>
+        <translation>W</translation>
+    </message>
+    <message>
+        <source>Clear Rating</source>
+        <translation>Wertung löschen</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Clear Rating. Leave empty for no mnemonic.</extracomment>
+        <translation>L</translation>
+    </message>
+    <message>
+        <source>Analyze Game</source>
+        <translation>Spiel analysieren</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item Analyze Game. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>Clear Analysis</source>
+        <translation>Analyse löschen</translation>
+    </message>
+    <message>
+        <source>E</source>
+        <extracomment>Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic.</extracomment>
+        <translation>C</translation>
+    </message>
+    <message>
+        <source>Analyze Game…</source>
+        <translation>Spiel analysieren …</translation>
+    </message>
+</context>
+<context>
+    <name>MenuView</name>
+    <message>
+        <source>View</source>
+        <translation>Ansicht</translation>
+    </message>
+    <message>
+        <source>V</source>
+        <extracomment>Mnemonic for menu View. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>Appearance…</source>
+        <translation type="vanished">Erscheinungsbild …</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu Appearance. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+    <message>
+        <source>F</source>
+        <extracomment>Mnemonic for menu item Fullscreen. Leave empty for no mnemonic.</extracomment>
+        <translation>V</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item View/Comment. Leave empty for no mnemonic.</extracomment>
+        <translation>K</translation>
+    </message>
+    <message>
+        <source>Appearance</source>
+        <translation>Erscheinungsbild</translation>
+    </message>
+    <message>
+        <source>Toolbar</source>
+        <translation>Werkzeugleiste</translation>
+    </message>
+    <message>
+        <source>T</source>
+        <extracomment>Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic.</extracomment>
+        <translation>W</translation>
+    </message>
+</context>
+<context>
+    <name>MoveAnnotationDialog</name>
+    <message>
+        <source>Move %1</source>
+        <translation>Zug %1</translation>
+    </message>
+    <message>
+        <source>Very good</source>
+        <translation>Sehr gut</translation>
+    </message>
+    <message>
+        <source>Good</source>
+        <translation>Gut</translation>
+    </message>
+    <message>
+        <source>Interesting</source>
+        <translation>Interessant</translation>
+    </message>
+    <message>
+        <source>Doubtful</source>
+        <translation>Zweifelhaft</translation>
+    </message>
+    <message>
+        <source>Bad</source>
+        <translation>Schlecht</translation>
+    </message>
+    <message>
+        <source>Very Bad</source>
+        <translation>Sehr schlecht</translation>
+    </message>
+    <message>
+        <source>No annotation</source>
+        <translation>Keine Annotierung</translation>
+    </message>
+</context>
+<context>
+    <name>NewFolderDialog</name>
+    <message>
+        <source>Folder name:</source>
+        <translation>Name des Ordners:</translation>
+    </message>
+</context>
+<context>
+    <name>OpenDialog</name>
+    <message>
+        <source>Open</source>
+        <translation>Öffnen</translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation>Blokus-Partien</translation>
+    </message>
+</context>
+<context>
+    <name>RatingDialog</name>
+    <message>
+        <source>Your rating:</source>
+        <translation>Ihre Wertung:</translation>
+    </message>
+    <message>
+        <source>Game variant:</source>
+        <translation>Spielvariante:</translation>
+    </message>
+    <message>
+        <source>Classic (2)</source>
+        <extracomment>Short for Classic (2 players)</extracomment>
+        <translation>Klassisch (2)</translation>
+    </message>
+    <message>
+        <source>Classic (3)</source>
+        <extracomment>Short for Classic (3 players)</extracomment>
+        <translation>Klassisch (3)</translation>
+    </message>
+    <message>
+        <source>Classic (4)</source>
+        <extracomment>Short for Classic (4 players)</extracomment>
+        <translation>Klassisch (4)</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>Duo</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>Junior</translation>
+    </message>
+    <message>
+        <source>Trigon (2)</source>
+        <extracomment>Short for Trigon (2 players)</extracomment>
+        <translation>Trigon (2)</translation>
+    </message>
+    <message>
+        <source>Trigon (3)</source>
+        <extracomment>Short for Trigon (3 players)</extracomment>
+        <translation>Trigon (3)</translation>
+    </message>
+    <message>
+        <source>Trigon (4)</source>
+        <extracomment>Short for Trigon (4 players)</extracomment>
+        <translation>Trigon (4)</translation>
+    </message>
+    <message>
+        <source>Nexos (2)</source>
+        <extracomment>Short for Nexos (2 players)</extracomment>
+        <translation>Nexos (2)</translation>
+    </message>
+    <message>
+        <source>Nexos (4)</source>
+        <extracomment>Short for Nexos (4 players)</extracomment>
+        <translation>Nexos (4)</translation>
+    </message>
+    <message>
+        <source>Callisto (2)</source>
+        <extracomment>Short for Callisto (2 players, 2 colors)</extracomment>
+        <translation>Callisto (2)</translation>
+    </message>
+    <message>
+        <source>Callisto (2/4)</source>
+        <extracomment>Short for Callisto (2 players, 4 colors)</extracomment>
+        <translation>Callisto (2/4)</translation>
+    </message>
+    <message>
+        <source>Callisto (3)</source>
+        <extracomment>Short for Callisto (3 players)</extracomment>
+        <translation>Callisto (3)</translation>
+    </message>
+    <message>
+        <source>Callisto (4)</source>
+        <extracomment>Short for Callisto (4 players)</extracomment>
+        <translation>Callisto (4)</translation>
+    </message>
+    <message>
+        <source>GembloQ (4)</source>
+        <extracomment>Short for GembloQ (4 players)</extracomment>
+        <translation>GembloQ (4)</translation>
+    </message>
+    <message>
+        <source>GembloQ (2)</source>
+        <extracomment>Short for GembloQ (2 players, 2 colors)</extracomment>
+        <translation>GembloQ (2)</translation>
+    </message>
+    <message>
+        <source>GembloQ (2/4)</source>
+        <extracomment>Short for GembloQ (2 players, 4 colors)</extracomment>
+        <translation>GembloQ (2/4)</translation>
+    </message>
+    <message>
+        <source>GembloQ (3)</source>
+        <extracomment>Short for GembloQ (3 players)</extracomment>
+        <translation>GembloQ (3)</translation>
+    </message>
+    <message>
+        <source>Rated games:</source>
+        <translation>Gewertete Spiele:</translation>
+    </message>
+    <message>
+        <source>Best previous rating:</source>
+        <translation>Beste frühere Wertung:</translation>
+    </message>
+    <message>
+        <source>Recent development:</source>
+        <translation>Aktuelle Entwicklung:</translation>
+    </message>
+    <message>
+        <source>Game</source>
+        <translation>Spiel</translation>
+    </message>
+    <message>
+        <source>Result</source>
+        <translation>Ergebnis</translation>
+    </message>
+    <message>
+        <source>Win</source>
+        <extracomment>Result of rated game is a win</extracomment>
+        <translation>Gewinn</translation>
+    </message>
+    <message>
+        <source>Loss</source>
+        <extracomment>Result of rated game is a loss</extracomment>
+        <translation>Verlust</translation>
+    </message>
+    <message>
+        <source>Tie</source>
+        <extracomment>Result of rated game is a tie. Abbreviate long translations to ensure that all columns of rated games list are visible on mobile devices with small screens.</extracomment>
+        <translation>Unentsch.</translation>
+    </message>
+    <message>
+        <source>Level</source>
+        <translation>Stufe</translation>
+    </message>
+    <message>
+        <source>Your Color</source>
+        <translation>Ihre Farbe</translation>
+    </message>
+    <message>
+        <source>Date</source>
+        <translation>Datum</translation>
+    </message>
+    <message>
+        <source>Open Game %1</source>
+        <translation>Spiel %1 öffnen</translation>
+    </message>
+</context>
+<context>
+    <name>SaveDialog</name>
+    <message>
+        <source>Save</source>
+        <translation>Speichern</translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation>Blokus-Partien</translation>
+    </message>
+</context>
+<context>
+    <name>ToolBar</name>
+    <message>
+        <source>Start a new game</source>
+        <translation>Ein neues Spiel beginnen</translation>
+    </message>
+    <message>
+        <source>Start a rated game</source>
+        <translation>Ein gewertetes Spiel beginnen</translation>
+    </message>
+    <message>
+        <source>Set the colors played by the computer</source>
+        <translation>Die vom Computer gespielten Farben festlegen</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>Go to beginning of game</source>
+        <translation>Zum Anfang des Spiels gehen</translation>
+    </message>
+    <message>
+        <source>Go ten moves backward</source>
+        <translation>Zehn Züge zurück gehen</translation>
+    </message>
+    <message>
+        <source>Go one move backward</source>
+        <translation>Einen Zug zurück gehen</translation>
+    </message>
+    <message>
+        <source>Go one move forward</source>
+        <translation>Einen Zug vorwärts gehen</translation>
+    </message>
+    <message>
+        <source>Go ten moves forward</source>
+        <translation>Zehn Züge vorwärts gehen</translation>
+    </message>
+    <message>
+        <source>Go to end of moves</source>
+        <translation>Zum Ende der Züge gehen</translation>
+    </message>
+    <message>
+        <source>Go to previous variation</source>
+        <translation>Zur vorherigen Variante gehen</translation>
+    </message>
+    <message>
+        <source>Go to next variation</source>
+        <translation>Zur nächsten Variante gehen</translation>
+    </message>
+    <message>
+        <source>Abort game analysis</source>
+        <translation>Spielanalyse abbrechen</translation>
+    </message>
+    <message>
+        <source>Abort computer move</source>
+        <translation>Computer-Zug abbrechen</translation>
+    </message>
+    <message>
+        <source>Undo move</source>
+        <extracomment>Tooltip for Undo button</extracomment>
+        <translation>Zug rückgängig</translation>
+    </message>
+</context>
+</TS>
diff --git a/src/pentobi/qml/i18n/qml_en.ts b/src/pentobi/qml/i18n/qml_en.ts
new file mode 100644 (file)
index 0000000..476e945
--- /dev/null
@@ -0,0 +1,1650 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+    <name>AboutDialog</name>
+    <message>
+        <source>Copyright © 2011–%1 Markus Enzenberger</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Computer opponent for the board game Blokus</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Pentobi %1</source>
+        <extracomment>The argument is the application version.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeDialog</name>
+    <message>
+        <source>Analysis speed:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Fast</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Normal</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Slow</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeGame</name>
+    <message>
+        <source>(No analysis)</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>AppearanceDialog</name>
+    <message>
+        <source>Coordinates</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Show variations</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Light</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Dark</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Colorblind light</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Colorblind dark</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>System</source>
+        <extracomment>Name of theme using default system colors</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Move marking:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Last with dot</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Last with number</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>All with number</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>None</source>
+        <extracomment>Move marking/None</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Animations</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Show comment:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Always</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>As needed</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Never</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Color theme:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Move number</source>
+        <extracomment>Check box in appearance dialog whether to show the move number in the status bar.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>AsciiArtSaveDialog</name>
+    <message>
+        <source>Export ASCII Art</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Text files</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>BoardContextMenu</name>
+    <message>
+        <source>Go to Move %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Move Annotation</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Move Annotation (%1)</source>
+        <extracomment>The argument is the annotation symbol for the current move</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ButtonApply</name>
+    <message>
+        <source>Apply</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ButtonCancel</name>
+    <message>
+        <source>Cancel</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ButtonClose</name>
+    <message>
+        <source>Close</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ButtonOk</name>
+    <message>
+        <source>OK</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ComputerDialog</name>
+    <message>
+        <source>Computer plays:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Blue/Red</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Purple</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Blue</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Orange</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Level %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ExportImageDialog</name>
+    <message>
+        <source>Image width:</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>FileDialog</name>
+    <message>
+        <source>Overwrite file?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Open</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>All files</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>GameDisplayDesktop</name>
+    <message>
+        <source>Computer is thinking…</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Running game analysis…</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Computer is thinking… (up to %1 seconds remaining)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Computer is thinking… (up to %1 minutes remaining)</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>GameInfoDialog</name>
+    <message>
+        <source>Player Blue/Red:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Player Purple:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Player Green:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Player Blue:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Player Yellow/Green:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Player Orange:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Player Yellow:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Player Red:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Date:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Time:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Event:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Round:</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>GameModel</name>
+    <message>
+        <source>Blue/Red</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Purple</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Blue</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Orange</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Purple wins with 1 point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Purple wins with %L1 points.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Orange wins with 1 point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Orange wins with %L1 points.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game ends in a tie.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Green wins with 1 point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Green wins with %L1 points.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Blue wins with 1 point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Blue wins with %L1 points.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Green wins (tie resolved).</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with 1 point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with %L1 points.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with 1 point.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with %L1 points.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins (tie resolved).</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Blue wins.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Yellow wins.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Red wins.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Red wins (tie resolved).</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Yellow wins (tie resolved).</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Yellow.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Red.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow and Red.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between all players.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Green wins.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Red.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Green.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Red and Green.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow, Red and Green.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Invalid Blokus SGF file. (%1)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Clipboard is empty.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Untitled</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Untitled %1</source>
+        <extracomment>The argument is a number, which will be increased if a file with the same name already exists</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>New Folder</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>New Folder %1</source>
+        <extracomment>The argument is a number, which will be increased if a folder with the same name already exists</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>(Setup)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>(No moves)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Move %1</source>
+        <extracomment>The argument is the current move number.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Unsupported character set</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>GameVariantDialog</name>
+    <message>
+        <source>Classic</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Trigon</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Nexos</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>GembloQ</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Callisto</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Players:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Colors:</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>GotoMoveDialog</name>
+    <message>
+        <source>Move number:</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>HelpWindow</name>
+    <message>
+        <source>Pentobi Help</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ImageSaveDialog</name>
+    <message>
+        <source>Save Image</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>PNG image files</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>JPEG image files</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>InitialRatingDialog</name>
+    <message>
+        <source>Initialize your rating for this game variant.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Initial rating:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Beginner</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Expert</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>Main</name>
+    <message>
+        <source>Pentobi</source>
+        <extracomment>Window title if no file is loaded.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game analysis is only possible in main variation.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Autosaved game was changed by another instance of Pentobi. Overwrite?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Your rating has increased from %1 to %2.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Your rating has decreased from %1 to %2.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Your rating stays at %1.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>No permission to access storage</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Delete all rating information for the current game variant?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Delete all variations?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Save failed.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>End of tree was reached. Continue search from start of the tree?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>No comment found</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>%1 (modified)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Reload?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Continue computer move?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Keep only position?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Keep only subtree?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Open failed.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Start rated game with Purple against Pentobi level %1?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Start rated game with Green against Pentobi level %1?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue/Red against Pentobi level %1?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue against Pentobi level %1?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Start rated game with Orange against Pentobi level %1?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow/Green against Pentobi level %1?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow against Pentobi level %1?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Start rated game with Red against Pentobi level %1?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>You have not yet played rated games in this game variant.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Truncate this subtree?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Truncate children?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Discard game?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Pentobi %1 (level %2)</source>
+        <extracomment>Player name for game info in rated game. First argument is version of Pentobi, second argument is level.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Human</source>
+        <extracomment>Player name for game info in rated game.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Rated game</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Overwrite?</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>%1 - Pentobi</source>
+        <extracomment>Window title if file is loaded. The argument is the file name prepended with a star if the file has been modified.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Not enough memory</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game analysis aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Computer move aborted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Rating information deleted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Variations deleted</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>File saved</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Saving image failed or unsupported image format</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Image saved</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Creating image failed</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Continuing rated game</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Kept only position</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Kept only subtree</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Variation is now %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Children truncated</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Setup</source>
+        <extracomment>Small-screen label for setup mode (short for &quot;Setup Mode&quot;).</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Rated</source>
+        <extracomment>Label for ongoing rated game</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Rated %1</source>
+        <extracomment>Small-screen label for finished rated game (short for &quot;Rated Game&quot;). The argument is the game number.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Rated Game %1</source>
+        <extracomment>Label for rated game. The argument is the game number.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Main Variation</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Beginning of Branch</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Comment</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Settings</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Find Move</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Next Comment</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Fullscreen</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game Info</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Move Number…</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Pentobi Help</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>New</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Rated Game</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Open…</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Play</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Play Move</source>
+        <extracomment>Play a single move</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Quit</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Save As…</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Stop</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Undo Move</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuComputer</name>
+    <message>
+        <source>Computer</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu Computer. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>S</source>
+        <extracomment>Mnemonic for menu item Computer Settings. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu item Play. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Play Move. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu item Stop. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuEdit</name>
+    <message>
+        <source>Edit</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>E</source>
+        <extracomment>Mnemonic for menu Edit. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Make Main Variation</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Variation Up</source>
+        <extracomment>Short for Move Variation Up</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>U</source>
+        <extracomment>Mnemonic for menu item Variation Up. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Variation Down</source>
+        <extracomment>Short for Move Variation Down</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>W</source>
+        <extracomment>Mnemonic for menu item Variation Down. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Delete Variations</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>D</source>
+        <extracomment>Mnemonic for menu item Delete Variations. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Truncate</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>T</source>
+        <extracomment>Mnemonic for menu item Truncate. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Truncate Children</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Truncate Children. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Keep Position</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu item Keep Position. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Keep Subtree</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>S</source>
+        <extracomment>Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu item Setup Mode. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Next Color</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>N</source>
+        <extracomment>Mnemonic for menu item Next Color. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Annotation…</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item Annotation. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Made main variation</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuExport</name>
+    <message>
+        <source>Export</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>E</source>
+        <extracomment>Mnemonic for menu Export. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Image. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item ASCII Art. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Image…</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>ASCII Art…</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuGame</name>
+    <message>
+        <source>Game</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>G</source>
+        <extracomment>Mnemonic for menu Game. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>N</source>
+        <extracomment>Mnemonic for menu item New. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>R</source>
+        <extracomment>Mnemonic for menu item Rated Game. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game Variant…</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>V</source>
+        <extracomment>Mnemonic for menu item Game Variant. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>I</source>
+        <extracomment>Mnemonic for menu item Game Info. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>U</source>
+        <extracomment>Mnemonic for menu item Undo. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>F</source>
+        <extracomment>Mnemonic for menu item Find Move. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu item Open. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Open Clipboard</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>S</source>
+        <extracomment>Mnemonic for menu item Save. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item Save As. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Q</source>
+        <extracomment>Mnemonic for menu item Quit. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuGo</name>
+    <message>
+        <source>Go</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu Go. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>N</source>
+        <extracomment>Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>B</source>
+        <extracomment>Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Next Comment. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuHelp</name>
+    <message>
+        <source>Help</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>H</source>
+        <extracomment>Mnemonic for menu Help. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>About Pentobi</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item About Pentobi. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Report Bug</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>B</source>
+        <extracomment>Mnemonic for menu item Report Bug. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuItem</name>
+    <message>
+        <source>Ctrl</source>
+        <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Shift</source>
+        <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuRecentFiles</name>
+    <message>
+        <source>Open Recent</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu Open Recent. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>%1. %2</source>
+        <extracomment>Format in recent files menu. First argument is the file number, second argument the file name.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Clear List</source>
+        <extracomment>Menu item for clearing the recent files list</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuTools</name>
+    <message>
+        <source>Tools</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>T</source>
+        <extracomment>Mnemonic for menu Tools. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Rating</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>R</source>
+        <extracomment>Mnemonic for menu item Rating. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Clear Rating</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Clear Rating. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Analyze Game</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item Analyze Game. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Clear Analysis</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>E</source>
+        <extracomment>Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Analyze Game…</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuView</name>
+    <message>
+        <source>View</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>V</source>
+        <extracomment>Mnemonic for menu View. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu Appearance. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>F</source>
+        <extracomment>Mnemonic for menu item Fullscreen. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item View/Comment. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Appearance</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Toolbar</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>T</source>
+        <extracomment>Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MoveAnnotationDialog</name>
+    <message>
+        <source>Move %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Very good</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Good</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Interesting</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Doubtful</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Bad</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Very Bad</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>No annotation</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>NewFolderDialog</name>
+    <message>
+        <source>Folder name:</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>OpenDialog</name>
+    <message>
+        <source>Open</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>RatingDialog</name>
+    <message>
+        <source>Your rating:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game variant:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Classic (2)</source>
+        <extracomment>Short for Classic (2 players)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Classic (3)</source>
+        <extracomment>Short for Classic (3 players)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Classic (4)</source>
+        <extracomment>Short for Classic (4 players)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Trigon (2)</source>
+        <extracomment>Short for Trigon (2 players)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Trigon (3)</source>
+        <extracomment>Short for Trigon (3 players)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Trigon (4)</source>
+        <extracomment>Short for Trigon (4 players)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Nexos (2)</source>
+        <extracomment>Short for Nexos (2 players)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Nexos (4)</source>
+        <extracomment>Short for Nexos (4 players)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Callisto (2)</source>
+        <extracomment>Short for Callisto (2 players, 2 colors)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Callisto (2/4)</source>
+        <extracomment>Short for Callisto (2 players, 4 colors)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Callisto (3)</source>
+        <extracomment>Short for Callisto (3 players)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Callisto (4)</source>
+        <extracomment>Short for Callisto (4 players)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>GembloQ (4)</source>
+        <extracomment>Short for GembloQ (4 players)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>GembloQ (2)</source>
+        <extracomment>Short for GembloQ (2 players, 2 colors)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>GembloQ (2/4)</source>
+        <extracomment>Short for GembloQ (2 players, 4 colors)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>GembloQ (3)</source>
+        <extracomment>Short for GembloQ (3 players)</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Rated games:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Best previous rating:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Recent development:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Game</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Result</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Win</source>
+        <extracomment>Result of rated game is a win</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Loss</source>
+        <extracomment>Result of rated game is a loss</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Tie</source>
+        <extracomment>Result of rated game is a tie. Abbreviate long translations to ensure that all columns of rated games list are visible on mobile devices with small screens.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Level</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Your Color</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Date</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Open Game %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>SaveDialog</name>
+    <message>
+        <source>Save</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ToolBar</name>
+    <message>
+        <source>Start a new game</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Start a rated game</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Set the colors played by the computer</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Make the computer continue to play the current color</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Make the computer play the current color</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Go to beginning of game</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Go ten moves backward</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Go one move backward</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Go one move forward</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Go ten moves forward</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Go to end of moves</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Go to previous variation</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Go to next variation</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Abort game analysis</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Abort computer move</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Undo move</source>
+        <extracomment>Tooltip for Undo button</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+</TS>
diff --git a/src/pentobi/qml/i18n/qml_fr.ts b/src/pentobi/qml/i18n/qml_fr.ts
new file mode 100644 (file)
index 0000000..2998d71
--- /dev/null
@@ -0,0 +1,1744 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="fr">
+<context>
+    <name>AboutDialog</name>
+    <message>
+        <source>Copyright © 2011–%1 Markus Enzenberger</source>
+        <translation>Copyright © 2011–%1 Markus Enzenberger</translation>
+    </message>
+    <message>
+        <source>Computer opponent for the board game Blokus</source>
+        <translation>Un adversaire d’ordinateur pour le jeu Blokus</translation>
+    </message>
+    <message>
+        <source>Pentobi %1</source>
+        <extracomment>The argument is the application version.</extracomment>
+        <translation>Pentobi %1</translation>
+    </message>
+</context>
+<context>
+    <name>Actions</name>
+    <message>
+        <source>Main Variation</source>
+        <translation type="vanished">Variation principale</translation>
+    </message>
+    <message>
+        <source>Beginning of Branch</source>
+        <translation type="vanished">Au début de la branche</translation>
+    </message>
+    <message>
+        <source>Settings…</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation type="vanished">Configuration…</translation>
+    </message>
+    <message>
+        <source>Find Move</source>
+        <translation type="vanished">Trouver un coup</translation>
+    </message>
+    <message>
+        <source>Next Comment</source>
+        <translation type="vanished">Commentaire suivant</translation>
+    </message>
+    <message>
+        <source>Fullscreen</source>
+        <translation type="vanished">Plein écran</translation>
+    </message>
+    <message>
+        <source>Move Number…</source>
+        <translation type="vanished">Numéro de coup…</translation>
+    </message>
+    <message>
+        <source>Pentobi Help</source>
+        <translation type="vanished">Aide de Pentobi</translation>
+    </message>
+    <message>
+        <source>New</source>
+        <translation type="vanished">Nouveau</translation>
+    </message>
+    <message>
+        <source>Rated Game</source>
+        <translation type="vanished">Partie classée</translation>
+    </message>
+    <message>
+        <source>Open…</source>
+        <translation type="vanished">Ouvrir…</translation>
+    </message>
+    <message>
+        <source>Play</source>
+        <translation type="vanished">Jouer</translation>
+    </message>
+    <message>
+        <source>Play Move</source>
+        <extracomment>Play a single move</extracomment>
+        <translation type="vanished">Jouer un coup</translation>
+    </message>
+    <message>
+        <source>Quit</source>
+        <translation type="vanished">Quitter</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation type="vanished">Enregistrer</translation>
+    </message>
+    <message>
+        <source>Save As…</source>
+        <translation type="vanished">Enregistrer sous…</translation>
+    </message>
+    <message>
+        <source>Stop</source>
+        <translation type="vanished">Arrêter</translation>
+    </message>
+    <message>
+        <source>Undo Move</source>
+        <translation type="vanished">Annuler le coup</translation>
+    </message>
+    <message>
+        <source>Game Info</source>
+        <translation type="vanished">Info sur la partie</translation>
+    </message>
+    <message>
+        <source>Comment</source>
+        <translation type="vanished">Commentaire</translation>
+    </message>
+    <message>
+        <source>Settings</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation type="vanished">Configuration</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeDialog</name>
+    <message>
+        <source>Analysis speed:</source>
+        <translation>Vitesse d’analyse :</translation>
+    </message>
+    <message>
+        <source>Fast</source>
+        <translation>Rapide</translation>
+    </message>
+    <message>
+        <source>Normal</source>
+        <translation>Normal</translation>
+    </message>
+    <message>
+        <source>Slow</source>
+        <translation>Lente</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeGame</name>
+    <message>
+        <source>(No analysis)</source>
+        <translation>(Pas d’analyse)</translation>
+    </message>
+</context>
+<context>
+    <name>AppearanceDialog</name>
+    <message>
+        <source>Coordinates</source>
+        <translation>Coordonnées</translation>
+    </message>
+    <message>
+        <source>Show variations</source>
+        <translation>Afficher les variations</translation>
+    </message>
+    <message>
+        <source>Light</source>
+        <translation>Clair</translation>
+    </message>
+    <message>
+        <source>Dark</source>
+        <translation>Noir</translation>
+    </message>
+    <message>
+        <source>Colorblind light</source>
+        <translation>Daltonien clair</translation>
+    </message>
+    <message>
+        <source>Colorblind dark</source>
+        <translation>Daltonien noir</translation>
+    </message>
+    <message>
+        <source>System</source>
+        <extracomment>Name of theme using default system colors</extracomment>
+        <translation>Système</translation>
+    </message>
+    <message>
+        <source>Move marking:</source>
+        <translation>Marquage de coups :</translation>
+    </message>
+    <message>
+        <source>Last with dot</source>
+        <translation>Dernier avec point</translation>
+    </message>
+    <message>
+        <source>Last with number</source>
+        <translation>Dernier avec numéro</translation>
+    </message>
+    <message>
+        <source>All with number</source>
+        <translation>Tous avec numéro</translation>
+    </message>
+    <message>
+        <source>None</source>
+        <extracomment>Move marking/None</extracomment>
+        <translation>Aucune</translation>
+    </message>
+    <message>
+        <source>Animations</source>
+        <translation>Animations</translation>
+    </message>
+    <message>
+        <source>Show comment:</source>
+        <translation>Afficher le commentaire :</translation>
+    </message>
+    <message>
+        <source>Always</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Toujours</translation>
+    </message>
+    <message>
+        <source>As needed</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Comme requis</translation>
+    </message>
+    <message>
+        <source>Never</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Jamais</translation>
+    </message>
+    <message>
+        <source>Color theme:</source>
+        <translation>Thème de couleur :</translation>
+    </message>
+    <message>
+        <source>Move number</source>
+        <extracomment>Check box in appearance dialog whether to show the move number in the status bar.</extracomment>
+        <translation>Numéro de coup</translation>
+    </message>
+</context>
+<context>
+    <name>AsciiArtSaveDialog</name>
+    <message>
+        <source>Export ASCII Art</source>
+        <translation>Exporter art ASCII</translation>
+    </message>
+    <message>
+        <source>Text files</source>
+        <translation>Fichiers texte</translation>
+    </message>
+</context>
+<context>
+    <name>BoardContextMenu</name>
+    <message>
+        <source>Go to Move %1</source>
+        <translation>Aller au coup %1</translation>
+    </message>
+    <message>
+        <source>Move Annotation</source>
+        <translation>Annotation courante</translation>
+    </message>
+    <message>
+        <source>Move Annotation (%1)</source>
+        <extracomment>The argument is the annotation symbol for the current move</extracomment>
+        <translation>Annotation courante (%1)</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonApply</name>
+    <message>
+        <source>Apply</source>
+        <translation>Appliquer</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonCancel</name>
+    <message>
+        <source>Cancel</source>
+        <translation>Annuler</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonClose</name>
+    <message>
+        <source>Close</source>
+        <translation>Fermer</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonOk</name>
+    <message>
+        <source>OK</source>
+        <translation>OK</translation>
+    </message>
+</context>
+<context>
+    <name>ComputerDialog</name>
+    <message>
+        <source>Computer plays:</source>
+        <translation>L’ordinateur joue :</translation>
+    </message>
+    <message>
+        <source>Blue/Red</source>
+        <translation>Bleu/rouge</translation>
+    </message>
+    <message>
+        <source>Purple</source>
+        <translation>Violet</translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation>Vert</translation>
+    </message>
+    <message>
+        <source>Blue</source>
+        <translation>Bleu</translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation>Jaune/vert</translation>
+    </message>
+    <message>
+        <source>Orange</source>
+        <translation>Orange</translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation>Jaune</translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation>Rouge</translation>
+    </message>
+    <message>
+        <source>Level %1</source>
+        <translation>Niveau %1</translation>
+    </message>
+</context>
+<context>
+    <name>ExportImageDialog</name>
+    <message>
+        <source>Image width:</source>
+        <translation>Largeur de l’image :</translation>
+    </message>
+</context>
+<context>
+    <name>FileDialog</name>
+    <message>
+        <source>Overwrite file?</source>
+        <translation>Remplacer le fichier ?</translation>
+    </message>
+    <message>
+        <source>Open</source>
+        <translation>Ouvrir</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>Enregistrer</translation>
+    </message>
+    <message>
+        <source>All files</source>
+        <translation>Tous les fichiers</translation>
+    </message>
+</context>
+<context>
+    <name>GameDisplayDesktop</name>
+    <message>
+        <source>Computer is thinking…</source>
+        <translation>L’ordinateur pense…</translation>
+    </message>
+    <message>
+        <source>Running game analysis…</source>
+        <translation>Exécution de l’analyse de la partie…</translation>
+    </message>
+    <message>
+        <source>Computer is thinking… (up to %1 seconds remaining)</source>
+        <translation>L’ordinateur pense… (jusqu’à %1 secondes restantes)</translation>
+    </message>
+    <message>
+        <source>Computer is thinking… (up to %1 minutes remaining)</source>
+        <translation>L’ordinateur pense… (jusqu’à %1 minutes restantes)</translation>
+    </message>
+</context>
+<context>
+    <name>GameInfoDialog</name>
+    <message>
+        <source>Player Blue/Red:</source>
+        <translation>Joueur bleu/rouge :</translation>
+    </message>
+    <message>
+        <source>Player Purple:</source>
+        <translation>Joueur violet :</translation>
+    </message>
+    <message>
+        <source>Player Green:</source>
+        <translation>Joueur vert :</translation>
+    </message>
+    <message>
+        <source>Player Blue:</source>
+        <translation>Joueur bleu :</translation>
+    </message>
+    <message>
+        <source>Player Yellow/Green:</source>
+        <translation>Joueur jaune/vert :</translation>
+    </message>
+    <message>
+        <source>Player Orange:</source>
+        <translation>Joueur orange :</translation>
+    </message>
+    <message>
+        <source>Player Yellow:</source>
+        <translation>Joueur jaune :</translation>
+    </message>
+    <message>
+        <source>Player Red:</source>
+        <translation>Joueur rouge :</translation>
+    </message>
+    <message>
+        <source>Date:</source>
+        <translation>Date :</translation>
+    </message>
+    <message>
+        <source>Time:</source>
+        <translation>Temps :</translation>
+    </message>
+    <message>
+        <source>Event:</source>
+        <translation>Événement :</translation>
+    </message>
+    <message>
+        <source>Round:</source>
+        <translation>Round :</translation>
+    </message>
+</context>
+<context>
+    <name>GameModel</name>
+    <message>
+        <source>Blue/Red</source>
+        <translation>Bleu/rouge</translation>
+    </message>
+    <message>
+        <source>Purple</source>
+        <translation>Violet</translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation>Vert</translation>
+    </message>
+    <message>
+        <source>Blue</source>
+        <translation>Bleu</translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation>Jaune/vert</translation>
+    </message>
+    <message>
+        <source>Orange</source>
+        <translation>Orange</translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation>Jaune</translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation>Rouge</translation>
+    </message>
+    <message>
+        <source>Purple wins with 1 point.</source>
+        <translation>Violet gagne avec 1 point.</translation>
+    </message>
+    <message>
+        <source>Purple wins with %L1 points.</source>
+        <translation>Violet gagne avec %L1 points.</translation>
+    </message>
+    <message>
+        <source>Orange wins with 1 point.</source>
+        <translation>Orange gagne avec 1 point.</translation>
+    </message>
+    <message>
+        <source>Orange wins with %L1 points.</source>
+        <translation>Orange gagne avec %L1 points.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie.</source>
+        <translation>La partie se termine par une égalité.</translation>
+    </message>
+    <message>
+        <source>Green wins with 1 point.</source>
+        <translation>Vert gagne avec 1 point.</translation>
+    </message>
+    <message>
+        <source>Green wins with %L1 points.</source>
+        <translation>Vert gagne avec %L1 points.</translation>
+    </message>
+    <message>
+        <source>Blue wins with 1 point.</source>
+        <translation>Bleu gagne avec 1 point.</translation>
+    </message>
+    <message>
+        <source>Blue wins with %L1 points.</source>
+        <translation>Bleu gagne avec %L1 points.</translation>
+    </message>
+    <message>
+        <source>Green wins (tie resolved).</source>
+        <translation>Vert gagne (égalité résolue).</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with 1 point.</source>
+        <translation>Bleu/rouge gagne avec 1 point.</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with %L1 points.</source>
+        <translation>Bleu/rouge gagne avec %L1 points.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with 1 point.</source>
+        <translation>Jaune/vert gagne avec 1 point.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with %L1 points.</source>
+        <translation>Jaune/vert gagne avec %L1 points.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins (tie resolved).</source>
+        <translation>Jaune/vert gagne (égalité résolue).</translation>
+    </message>
+    <message>
+        <source>Blue wins.</source>
+        <translation>Bleu gagne.</translation>
+    </message>
+    <message>
+        <source>Yellow wins.</source>
+        <translation>Jaune gagne.</translation>
+    </message>
+    <message>
+        <source>Red wins.</source>
+        <translation>Rouge gagne.</translation>
+    </message>
+    <message>
+        <source>Red wins (tie resolved).</source>
+        <translation>Rouge gagne (égalité résolue).</translation>
+    </message>
+    <message>
+        <source>Yellow wins (tie resolved).</source>
+        <translation>Jaune gagne (égalité résolue).</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Yellow.</source>
+        <translation>La partie se termine par une égalité entre bleu et jaune.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Red.</source>
+        <translation>La partie se termine par une égalité entre bleu et rouge.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow and Red.</source>
+        <translation>La partie se termine par une égalité entre jaune et rouge.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between all players.</source>
+        <translation>La partie se termine par une égalité entre tous les joueurs.</translation>
+    </message>
+    <message>
+        <source>Green wins.</source>
+        <translation>Vert gagne.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Red.</source>
+        <translation>La partie se termine par une égalité entre bleu, jaune et rouge.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Green.</source>
+        <translation>La partie se termine par une égalité entre bleu, jaune et vert.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Red and Green.</source>
+        <translation>La partie se termine par une égalité entre bleu, rouge et vert.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow, Red and Green.</source>
+        <translation>La partie se termine par une égalité entre jaune, rouge et vert.</translation>
+    </message>
+    <message>
+        <source>Invalid Blokus SGF file. (%1)</source>
+        <translation>Le fichier n’est pas un fichier Blokus SGF valable. (%1)</translation>
+    </message>
+    <message>
+        <source>Clipboard is empty.</source>
+        <translation>Le presse-papiers est vide.</translation>
+    </message>
+    <message>
+        <source>Untitled</source>
+        <translation>Sans nom</translation>
+    </message>
+    <message>
+        <source>Untitled %1</source>
+        <extracomment>The argument is a number, which will be increased if a file with the same name already exists</extracomment>
+        <translation>Sans nom %1</translation>
+    </message>
+    <message>
+        <source>New Folder</source>
+        <translation>Nouveau dossier</translation>
+    </message>
+    <message>
+        <source>New Folder %1</source>
+        <extracomment>The argument is a number, which will be increased if a folder with the same name already exists</extracomment>
+        <translation>Nouveau dossier %1</translation>
+    </message>
+    <message>
+        <source>(Setup)</source>
+        <translation>(Position)</translation>
+    </message>
+    <message>
+        <source>(No moves)</source>
+        <translation>(Pas de coups)</translation>
+    </message>
+    <message>
+        <source>Move %1</source>
+        <extracomment>The argument is the current move number.</extracomment>
+        <translation>Coup %1</translation>
+    </message>
+    <message>
+        <source>Unsupported character set</source>
+        <translation>Codage des caractères non supporté</translation>
+    </message>
+</context>
+<context>
+    <name>GameVariantDialog</name>
+    <message>
+        <source>Classic</source>
+        <translation>Classique</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>Duo</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>Junior</translation>
+    </message>
+    <message>
+        <source>Trigon</source>
+        <translation>Trigon</translation>
+    </message>
+    <message>
+        <source>Nexos</source>
+        <translation>Nexos</translation>
+    </message>
+    <message>
+        <source>GembloQ</source>
+        <translation>GembloQ</translation>
+    </message>
+    <message>
+        <source>Callisto</source>
+        <translation>Callisto</translation>
+    </message>
+    <message>
+        <source>Players:</source>
+        <translation>Joueurs :</translation>
+    </message>
+    <message>
+        <source>Colors:</source>
+        <translation>Couleurs :</translation>
+    </message>
+</context>
+<context>
+    <name>GotoMoveDialog</name>
+    <message>
+        <source>Move number:</source>
+        <translation>Numéro de coup :</translation>
+    </message>
+</context>
+<context>
+    <name>HelpWindow</name>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Aide de Pentobi</translation>
+    </message>
+</context>
+<context>
+    <name>ImageSaveDialog</name>
+    <message>
+        <source>Save Image</source>
+        <translation>Enregistrer l’image</translation>
+    </message>
+    <message>
+        <source>PNG image files</source>
+        <translation>Image PNG</translation>
+    </message>
+    <message>
+        <source>JPEG image files</source>
+        <translation>Image JPEG</translation>
+    </message>
+</context>
+<context>
+    <name>InitialRatingDialog</name>
+    <message>
+        <source>Initialize your rating for this game variant.</source>
+        <translation>Initialisez votre classement pour cette variante du jeu.</translation>
+    </message>
+    <message>
+        <source>Initial rating:</source>
+        <translation>Classement initial :</translation>
+    </message>
+    <message>
+        <source>Beginner</source>
+        <translation>Débutant</translation>
+    </message>
+    <message>
+        <source>Expert</source>
+        <translation>Expert</translation>
+    </message>
+</context>
+<context>
+    <name>Main</name>
+    <message>
+        <source>Pentobi</source>
+        <extracomment>Window title if no file is loaded.</extracomment>
+        <translation>Pentobi</translation>
+    </message>
+    <message>
+        <source>Game analysis is only possible in main variation.</source>
+        <translation>L’analyse de la partie n’est possible que dans la variation principale.</translation>
+    </message>
+    <message>
+        <source>Autosaved game was changed by another instance of Pentobi. Overwrite?</source>
+        <translation>Jeu sauvé automatiquement a été changé par une autre instance de Pentobi. Remplacer ?</translation>
+    </message>
+    <message>
+        <source>Your rating has increased from %1 to %2.</source>
+        <translation>Votre classement a augmenté de %1 à %2.</translation>
+    </message>
+    <message>
+        <source>Your rating has decreased from %1 to %2.</source>
+        <translation>Votre classement a diminué de %1 à %2.</translation>
+    </message>
+    <message>
+        <source>Your rating stays at %1.</source>
+        <translation>Votre classement reste à %1.</translation>
+    </message>
+    <message>
+        <source>No permission to access storage</source>
+        <translation>Aucune autorisation d’accès au stockage</translation>
+    </message>
+    <message>
+        <source>Delete all rating information for the current game variant?</source>
+        <translation>Supprimer toutes les informations de classement de l’actuelle variante du jeu ?</translation>
+    </message>
+    <message>
+        <source>Delete all variations?</source>
+        <translation>Détruire toutes les variations ?</translation>
+    </message>
+    <message>
+        <source>Save failed.</source>
+        <translation>Échec de l’enregistrement.</translation>
+    </message>
+    <message>
+        <source>End of tree was reached. Continue search from start of the tree?</source>
+        <translation>La fin de l’arbre est atteinte. Continuer la recherche depuis la racine ?</translation>
+    </message>
+    <message>
+        <source>No comment found</source>
+        <translation>Aucun commentaire trouvé</translation>
+    </message>
+    <message>
+        <source>%1 (modified)</source>
+        <translation>%1 (modifié)</translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Reload?</source>
+        <translation>Le fichier a été modifié par une autre application. Recharger ?</translation>
+    </message>
+    <message>
+        <source>Continue computer move?</source>
+        <translation>Coninuer le coup de l’ordinateur ?</translation>
+    </message>
+    <message>
+        <source>Keep only position?</source>
+        <translation>Garder seulement la position ?</translation>
+    </message>
+    <message>
+        <source>Keep only subtree?</source>
+        <translation>Garder seulement le sous-arbre ?</translation>
+    </message>
+    <message>
+        <source>Open failed.</source>
+        <translation>Échec de l’ouverture.</translation>
+    </message>
+    <message>
+        <source>Start rated game with Purple against Pentobi level %1?</source>
+        <translation>Commencer une partie classée avec violet contre Pentobi niveau %1 ?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Green against Pentobi level %1?</source>
+        <translation>Commencer une partie classée avec vert contre Pentobi niveau %1 ?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue/Red against Pentobi level %1?</source>
+        <translation>Commencer une partie classée avec bleu/rouge contre Pentobi niveau %1 ?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue against Pentobi level %1?</source>
+        <translation>Commencer une partie classée avec bleu contre Pentobi niveau %1 ?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Orange against Pentobi level %1?</source>
+        <translation>Commencer une partie classée avec orange contre Pentobi niveau %1 ?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow/Green against Pentobi level %1?</source>
+        <translation>Commencer une partie classée avec jaune/vert contre Pentobi niveau %1 ?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow against Pentobi level %1?</source>
+        <translation>Commencer une partie classée avec jaune contre Pentobi niveau %1 ?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Red against Pentobi level %1?</source>
+        <translation>Commencer une partie classée avec rouge contre Pentobi niveau %1 ?</translation>
+    </message>
+    <message>
+        <source>You have not yet played rated games in this game variant.</source>
+        <translation>Vous n’avez pas encore joué des parties classées dans cette variante du jeu.</translation>
+    </message>
+    <message>
+        <source>Truncate this subtree?</source>
+        <translation>Élaguer la branche actuelle ?</translation>
+    </message>
+    <message>
+        <source>Truncate children?</source>
+        <translation>Élaguer les branches filles ?</translation>
+    </message>
+    <message>
+        <source>Discard game?</source>
+        <translation>Abandonner la partie ?</translation>
+    </message>
+    <message>
+        <source>Pentobi %1 (level %2)</source>
+        <extracomment>Player name for game info in rated game. First argument is version of Pentobi, second argument is level.</extracomment>
+        <translation>Pentobi %1 (niveau %2)</translation>
+    </message>
+    <message>
+        <source>Human</source>
+        <extracomment>Player name for game info in rated game.</extracomment>
+        <translation>Personne</translation>
+    </message>
+    <message>
+        <source>Rated game</source>
+        <translation>Partie classée</translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Overwrite?</source>
+        <translation>Le fichier a été modifié par une autre application. Remplacer ?</translation>
+    </message>
+    <message>
+        <source>%1 - Pentobi</source>
+        <extracomment>Window title if file is loaded. The argument is the file name prepended with a star if the file has been modified.</extracomment>
+        <translation>%1 - Pentobi</translation>
+    </message>
+    <message>
+        <source>Not enough memory</source>
+        <translation>Mémoire insuffisante</translation>
+    </message>
+    <message>
+        <source>Game analysis aborted</source>
+        <translation>Analyse de la partie abandonnée</translation>
+    </message>
+    <message>
+        <source>Computer move aborted</source>
+        <translation>Coup de l&apos;ordinateur abandonné</translation>
+    </message>
+    <message>
+        <source>Rating information deleted</source>
+        <translation>Informations de classement supprimées</translation>
+    </message>
+    <message>
+        <source>Variations deleted</source>
+        <translation>Variations supprimées</translation>
+    </message>
+    <message>
+        <source>File saved</source>
+        <translation>Fichier enregistré</translation>
+    </message>
+    <message>
+        <source>Saving image failed or unsupported image format</source>
+        <translation>Impossible d’enregistrer l’image ou format de l’image non pris en charge</translation>
+    </message>
+    <message>
+        <source>Image saved</source>
+        <translation>Image enregistrée</translation>
+    </message>
+    <message>
+        <source>Creating image failed</source>
+        <translation>La création d’image a échoué</translation>
+    </message>
+    <message>
+        <source>Continuing rated game</source>
+        <translation>Partie classée est continuée</translation>
+    </message>
+    <message>
+        <source>Kept only position</source>
+        <translation>Gardé seulement la position</translation>
+    </message>
+    <message>
+        <source>Kept only subtree</source>
+        <translation>Gardé seulement le sous-arbre</translation>
+    </message>
+    <message>
+        <source>Variation is now %1</source>
+        <translation>La variation est maintenant %1</translation>
+    </message>
+    <message>
+        <source>Children truncated</source>
+        <translation>Nœuds fils coupé</translation>
+    </message>
+    <message>
+        <source>Setup</source>
+        <extracomment>Small-screen label for setup mode (short for &quot;Setup Mode&quot;).</extracomment>
+        <translation>Position</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Position</translation>
+    </message>
+    <message>
+        <source>Rated</source>
+        <extracomment>Label for ongoing rated game</extracomment>
+        <translation>Classée</translation>
+    </message>
+    <message>
+        <source>Rated %1</source>
+        <extracomment>Small-screen label for finished rated game (short for &quot;Rated Game&quot;). The argument is the game number.</extracomment>
+        <translation>Classée %1</translation>
+    </message>
+    <message>
+        <source>Rated Game %1</source>
+        <extracomment>Label for rated game. The argument is the game number.</extracomment>
+        <translation>Partie classée %1</translation>
+    </message>
+    <message>
+        <source>Main Variation</source>
+        <translation>Variation principale</translation>
+    </message>
+    <message>
+        <source>Beginning of Branch</source>
+        <translation>Au début de la branche</translation>
+    </message>
+    <message>
+        <source>Comment</source>
+        <translation>Commentaire</translation>
+    </message>
+    <message>
+        <source>Settings</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation>Configuration</translation>
+    </message>
+    <message>
+        <source>Find Move</source>
+        <translation>Trouver un coup</translation>
+    </message>
+    <message>
+        <source>Next Comment</source>
+        <translation>Commentaire suivant</translation>
+    </message>
+    <message>
+        <source>Fullscreen</source>
+        <translation>Plein écran</translation>
+    </message>
+    <message>
+        <source>Game Info</source>
+        <translation>Info sur la partie</translation>
+    </message>
+    <message>
+        <source>Move Number…</source>
+        <translation>Numéro de coup…</translation>
+    </message>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Aide de Pentobi</translation>
+    </message>
+    <message>
+        <source>New</source>
+        <translation>Nouveau</translation>
+    </message>
+    <message>
+        <source>Rated Game</source>
+        <translation>Partie classée</translation>
+    </message>
+    <message>
+        <source>Open…</source>
+        <translation>Ouvrir…</translation>
+    </message>
+    <message>
+        <source>Play</source>
+        <translation>Jouer</translation>
+    </message>
+    <message>
+        <source>Play Move</source>
+        <extracomment>Play a single move</extracomment>
+        <translation>Jouer un coup</translation>
+    </message>
+    <message>
+        <source>Quit</source>
+        <translation>Quitter</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>Enregistrer</translation>
+    </message>
+    <message>
+        <source>Save As…</source>
+        <translation>Enregistrer sous…</translation>
+    </message>
+    <message>
+        <source>Stop</source>
+        <translation>Arrêter</translation>
+    </message>
+    <message>
+        <source>Undo Move</source>
+        <translation>Annuler le coup</translation>
+    </message>
+</context>
+<context>
+    <name>MenuComputer</name>
+    <message>
+        <source>Computer</source>
+        <translation>Ordinateur</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu Computer. Leave empty for no mnemonic.</extracomment>
+        <translation>O</translation>
+    </message>
+    <message>
+        <source>S</source>
+        <extracomment>Mnemonic for menu item Computer Settings. Leave empty for no mnemonic.</extracomment>
+        <translation>C</translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu item Play. Leave empty for no mnemonic.</extracomment>
+        <translation>J</translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Play Move. Leave empty for no mnemonic.</extracomment>
+        <translation>P</translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu item Stop. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+</context>
+<context>
+    <name>MenuEdit</name>
+    <message>
+        <source>Edit</source>
+        <translation>Édition</translation>
+    </message>
+    <message>
+        <source>E</source>
+        <extracomment>Mnemonic for menu Edit. Leave empty for no mnemonic.</extracomment>
+        <translation>N</translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic.</extracomment>
+        <translation>P</translation>
+    </message>
+    <message>
+        <source>Make Main Variation</source>
+        <translation>Choisir comme variation principale</translation>
+    </message>
+    <message>
+        <source>Variation Up</source>
+        <extracomment>Short for Move Variation Up</extracomment>
+        <translation>Variation vers le haut</translation>
+    </message>
+    <message>
+        <source>U</source>
+        <extracomment>Mnemonic for menu item Variation Up. Leave empty for no mnemonic.</extracomment>
+        <translation>H</translation>
+    </message>
+    <message>
+        <source>Variation Down</source>
+        <extracomment>Short for Move Variation Down</extracomment>
+        <translation>Variation vers le bas</translation>
+    </message>
+    <message>
+        <source>W</source>
+        <extracomment>Mnemonic for menu item Variation Down. Leave empty for no mnemonic.</extracomment>
+        <translation>B</translation>
+    </message>
+    <message>
+        <source>Delete Variations</source>
+        <translation>Détruire les variations</translation>
+    </message>
+    <message>
+        <source>D</source>
+        <extracomment>Mnemonic for menu item Delete Variations. Leave empty for no mnemonic.</extracomment>
+        <translation>D</translation>
+    </message>
+    <message>
+        <source>Truncate</source>
+        <translation>Couper</translation>
+    </message>
+    <message>
+        <source>T</source>
+        <extracomment>Mnemonic for menu item Truncate. Leave empty for no mnemonic.</extracomment>
+        <translation>C</translation>
+    </message>
+    <message>
+        <source>Truncate Children</source>
+        <translation>Couper les branches filles</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Truncate Children. Leave empty for no mnemonic.</extracomment>
+        <translation>F</translation>
+    </message>
+    <message>
+        <source>Keep Position</source>
+        <translation>Garder la position</translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu item Keep Position. Leave empty for no mnemonic.</extracomment>
+        <translation>G</translation>
+    </message>
+    <message>
+        <source>Keep Subtree</source>
+        <translation>Garder le sous-arbre</translation>
+    </message>
+    <message>
+        <source>S</source>
+        <extracomment>Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic.</extracomment>
+        <translation>S</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Position</translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu item Setup Mode. Leave empty for no mnemonic.</extracomment>
+        <translation>O</translation>
+    </message>
+    <message>
+        <source>Next Color</source>
+        <translation>Couleur suivante</translation>
+    </message>
+    <message>
+        <source>N</source>
+        <extracomment>Mnemonic for menu item Next Color. Leave empty for no mnemonic.</extracomment>
+        <translation>N</translation>
+    </message>
+    <message>
+        <source>Annotation…</source>
+        <translation>Annotation…</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item Annotation. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>Made main variation</source>
+        <translation>Choisi comme variation principale</translation>
+    </message>
+</context>
+<context>
+    <name>MenuExport</name>
+    <message>
+        <source>Export</source>
+        <translation>Exporter</translation>
+    </message>
+    <message>
+        <source>E</source>
+        <extracomment>Mnemonic for menu Export. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Image. Leave empty for no mnemonic.</extracomment>
+        <translation>I</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item ASCII Art. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>Image…</source>
+        <translation>Image…</translation>
+    </message>
+    <message>
+        <source>ASCII Art…</source>
+        <translation>Art ASCII…</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGame</name>
+    <message>
+        <source>Game</source>
+        <translation>Partie</translation>
+    </message>
+    <message>
+        <source>G</source>
+        <extracomment>Mnemonic for menu Game. Leave empty for no mnemonic.</extracomment>
+        <translation>P</translation>
+    </message>
+    <message>
+        <source>N</source>
+        <extracomment>Mnemonic for menu item New. Leave empty for no mnemonic.</extracomment>
+        <translation>N</translation>
+    </message>
+    <message>
+        <source>R</source>
+        <extracomment>Mnemonic for menu item Rated Game. Leave empty for no mnemonic.</extracomment>
+        <translation>C</translation>
+    </message>
+    <message>
+        <source>Game Variant…</source>
+        <translation>Variante du jeu…</translation>
+    </message>
+    <message>
+        <source>V</source>
+        <extracomment>Mnemonic for menu item Game Variant. Leave empty for no mnemonic.</extracomment>
+        <translation>V</translation>
+    </message>
+    <message>
+        <source>I</source>
+        <extracomment>Mnemonic for menu item Game Info. Leave empty for no mnemonic.</extracomment>
+        <translation>I</translation>
+    </message>
+    <message>
+        <source>U</source>
+        <extracomment>Mnemonic for menu item Undo. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>F</source>
+        <extracomment>Mnemonic for menu item Find Move. Leave empty for no mnemonic.</extracomment>
+        <translation>T</translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu item Open. Leave empty for no mnemonic.</extracomment>
+        <translation>O</translation>
+    </message>
+    <message>
+        <source>Open Clipboard</source>
+        <translation>Ouvrir le presse-papiers</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic.</extracomment>
+        <translation>P</translation>
+    </message>
+    <message>
+        <source>S</source>
+        <extracomment>Mnemonic for menu item Save. Leave empty for no mnemonic.</extracomment>
+        <translation>R</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item Save As. Leave empty for no mnemonic.</extracomment>
+        <translation>S</translation>
+    </message>
+    <message>
+        <source>Q</source>
+        <extracomment>Mnemonic for menu item Quit. Leave empty for no mnemonic.</extracomment>
+        <translation>Q</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGo</name>
+    <message>
+        <source>Go</source>
+        <translation>Déplacement</translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu Go. Leave empty for no mnemonic.</extracomment>
+        <translation>D</translation>
+    </message>
+    <message>
+        <source>N</source>
+        <extracomment>Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic.</extracomment>
+        <translation>N</translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic.</extracomment>
+        <translation>P</translation>
+    </message>
+    <message>
+        <source>B</source>
+        <extracomment>Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic.</extracomment>
+        <translation>D</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Next Comment. Leave empty for no mnemonic.</extracomment>
+        <translation>C</translation>
+    </message>
+</context>
+<context>
+    <name>MenuHelp</name>
+    <message>
+        <source>Help</source>
+        <translation>Aide</translation>
+    </message>
+    <message>
+        <source>H</source>
+        <extracomment>Mnemonic for menu Help. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>About Pentobi</source>
+        <translation>À propos de Pentobi</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item About Pentobi. Leave empty for no mnemonic.</extracomment>
+        <translation>P</translation>
+    </message>
+    <message>
+        <source>Report Bug</source>
+        <translation>Rapportez une erreur</translation>
+    </message>
+    <message>
+        <source>B</source>
+        <extracomment>Mnemonic for menu item Report Bug. Leave empty for no mnemonic.</extracomment>
+        <translation>R</translation>
+    </message>
+</context>
+<context>
+    <name>MenuItem</name>
+    <message>
+        <source>Ctrl</source>
+        <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+        <translation>Ctrl</translation>
+    </message>
+    <message>
+        <source>Shift</source>
+        <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+        <translation>Maj</translation>
+    </message>
+</context>
+<context>
+    <name>MenuRecentFiles</name>
+    <message>
+        <source>Open Recent</source>
+        <translation>Fichiers récents</translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu Open Recent. Leave empty for no mnemonic.</extracomment>
+        <translation>F</translation>
+    </message>
+    <message>
+        <source>%1. %2</source>
+        <extracomment>Format in recent files menu. First argument is the file number, second argument the file name.</extracomment>
+        <translation>%1. %2</translation>
+    </message>
+    <message>
+        <source>Clear List</source>
+        <extracomment>Menu item for clearing the recent files list</extracomment>
+        <translation>Effacer la liste</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+</context>
+<context>
+    <name>MenuTools</name>
+    <message>
+        <source>Tools</source>
+        <translation>Outils</translation>
+    </message>
+    <message>
+        <source>T</source>
+        <extracomment>Mnemonic for menu Tools. Leave empty for no mnemonic.</extracomment>
+        <translation>U</translation>
+    </message>
+    <message>
+        <source>Rating</source>
+        <translation>Classement</translation>
+    </message>
+    <message>
+        <source>R</source>
+        <extracomment>Mnemonic for menu item Rating. Leave empty for no mnemonic.</extracomment>
+        <translation>C</translation>
+    </message>
+    <message>
+        <source>Clear Rating</source>
+        <translation>Effacer le classement</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Clear Rating. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+    <message>
+        <source>Analyze Game</source>
+        <translation>Analyser la partie</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item Analyze Game. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>Clear Analysis</source>
+        <translation>Effacer l’analyse</translation>
+    </message>
+    <message>
+        <source>E</source>
+        <extracomment>Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic.</extracomment>
+        <translation>N</translation>
+    </message>
+    <message>
+        <source>Analyze Game…</source>
+        <translation>Analyser la partie…</translation>
+    </message>
+</context>
+<context>
+    <name>MenuView</name>
+    <message>
+        <source>View</source>
+        <translation>Affichage</translation>
+    </message>
+    <message>
+        <source>V</source>
+        <extracomment>Mnemonic for menu View. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>Appearance…</source>
+        <translation type="vanished">Apparence…</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu Appearance. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+    <message>
+        <source>F</source>
+        <extracomment>Mnemonic for menu item Fullscreen. Leave empty for no mnemonic.</extracomment>
+        <translation>P</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item View/Comment. Leave empty for no mnemonic.</extracomment>
+        <translation>C</translation>
+    </message>
+    <message>
+        <source>Appearance</source>
+        <translation>Apparence</translation>
+    </message>
+    <message>
+        <source>Toolbar</source>
+        <translation>Barre d’outils</translation>
+    </message>
+    <message>
+        <source>T</source>
+        <extracomment>Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic.</extracomment>
+        <translation>B</translation>
+    </message>
+</context>
+<context>
+    <name>MoveAnnotationDialog</name>
+    <message>
+        <source>Move %1</source>
+        <translation>Coup %1</translation>
+    </message>
+    <message>
+        <source>Very good</source>
+        <translation>Très bon</translation>
+    </message>
+    <message>
+        <source>Good</source>
+        <translation>Bon</translation>
+    </message>
+    <message>
+        <source>Interesting</source>
+        <translation>Intéressant</translation>
+    </message>
+    <message>
+        <source>Doubtful</source>
+        <translation>Douteux</translation>
+    </message>
+    <message>
+        <source>Bad</source>
+        <translation>Mauvais</translation>
+    </message>
+    <message>
+        <source>Very Bad</source>
+        <translation>Très mauvais</translation>
+    </message>
+    <message>
+        <source>No annotation</source>
+        <translation>Aucune annotation</translation>
+    </message>
+</context>
+<context>
+    <name>NewFolderDialog</name>
+    <message>
+        <source>Folder name:</source>
+        <translation>Nom de dossier :</translation>
+    </message>
+</context>
+<context>
+    <name>OpenDialog</name>
+    <message>
+        <source>Open</source>
+        <translation>Ouvrir</translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation>Parties de Blokus</translation>
+    </message>
+</context>
+<context>
+    <name>RatingDialog</name>
+    <message>
+        <source>Your rating:</source>
+        <translation>Votre classement :</translation>
+    </message>
+    <message>
+        <source>Game variant:</source>
+        <translation>Variante du jeu :</translation>
+    </message>
+    <message>
+        <source>Classic (2)</source>
+        <extracomment>Short for Classic (2 players)</extracomment>
+        <translation>Classique (2)</translation>
+    </message>
+    <message>
+        <source>Classic (3)</source>
+        <extracomment>Short for Classic (3 players)</extracomment>
+        <translation>Classique (3)</translation>
+    </message>
+    <message>
+        <source>Classic (4)</source>
+        <extracomment>Short for Classic (4 players)</extracomment>
+        <translation>Classique (4)</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>Duo</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>Junior</translation>
+    </message>
+    <message>
+        <source>Trigon (2)</source>
+        <extracomment>Short for Trigon (2 players)</extracomment>
+        <translation>Trigon (2)</translation>
+    </message>
+    <message>
+        <source>Trigon (3)</source>
+        <extracomment>Short for Trigon (3 players)</extracomment>
+        <translation>Trigon (3)</translation>
+    </message>
+    <message>
+        <source>Trigon (4)</source>
+        <extracomment>Short for Trigon (4 players)</extracomment>
+        <translation>Trigon (4)</translation>
+    </message>
+    <message>
+        <source>Nexos (2)</source>
+        <extracomment>Short for Nexos (2 players)</extracomment>
+        <translation>Nexos (2)</translation>
+    </message>
+    <message>
+        <source>Nexos (4)</source>
+        <extracomment>Short for Nexos (4 players)</extracomment>
+        <translation>Nexos (4)</translation>
+    </message>
+    <message>
+        <source>Callisto (2)</source>
+        <extracomment>Short for Callisto (2 players, 2 colors)</extracomment>
+        <translation>Callisto (2)</translation>
+    </message>
+    <message>
+        <source>Callisto (2/4)</source>
+        <extracomment>Short for Callisto (2 players, 4 colors)</extracomment>
+        <translation>Callisto (2/4)</translation>
+    </message>
+    <message>
+        <source>Callisto (3)</source>
+        <extracomment>Short for Callisto (3 players)</extracomment>
+        <translation>Callisto (3)</translation>
+    </message>
+    <message>
+        <source>Callisto (4)</source>
+        <extracomment>Short for Callisto (4 players)</extracomment>
+        <translation>Callisto (4)</translation>
+    </message>
+    <message>
+        <source>GembloQ (4)</source>
+        <extracomment>Short for GembloQ (4 players)</extracomment>
+        <translation>GembloQ (4)</translation>
+    </message>
+    <message>
+        <source>GembloQ (2)</source>
+        <extracomment>Short for GembloQ (2 players, 2 colors)</extracomment>
+        <translation>GembloQ (2)</translation>
+    </message>
+    <message>
+        <source>GembloQ (2/4)</source>
+        <extracomment>Short for GembloQ (2 players, 4 colors)</extracomment>
+        <translation>GembloQ (2/4)</translation>
+    </message>
+    <message>
+        <source>GembloQ (3)</source>
+        <extracomment>Short for GembloQ (3 players)</extracomment>
+        <translation>GembloQ (3)</translation>
+    </message>
+    <message>
+        <source>Rated games:</source>
+        <translation>Parties classées :</translation>
+    </message>
+    <message>
+        <source>Best previous rating:</source>
+        <translation>Meilleur classement précédent :</translation>
+    </message>
+    <message>
+        <source>Recent development:</source>
+        <translation>Développement récent :</translation>
+    </message>
+    <message>
+        <source>Game</source>
+        <translation>Partie</translation>
+    </message>
+    <message>
+        <source>Result</source>
+        <translation>Résultat</translation>
+    </message>
+    <message>
+        <source>Win</source>
+        <extracomment>Result of rated game is a win</extracomment>
+        <translation>Victoire</translation>
+    </message>
+    <message>
+        <source>Loss</source>
+        <extracomment>Result of rated game is a loss</extracomment>
+        <translation>Perte</translation>
+    </message>
+    <message>
+        <source>Tie</source>
+        <extracomment>Result of rated game is a tie. Abbreviate long translations to ensure that all columns of rated games list are visible on mobile devices with small screens.</extracomment>
+        <translation>Egalité</translation>
+    </message>
+    <message>
+        <source>Level</source>
+        <translation>Niveau</translation>
+    </message>
+    <message>
+        <source>Your Color</source>
+        <translation>Votre couleur</translation>
+    </message>
+    <message>
+        <source>Date</source>
+        <translation>Date</translation>
+    </message>
+    <message>
+        <source>Open Game %1</source>
+        <translation>Ouvrir la partie %1</translation>
+    </message>
+</context>
+<context>
+    <name>SaveDialog</name>
+    <message>
+        <source>Save</source>
+        <translation>Enregistrer</translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation>Parties de Blokus</translation>
+    </message>
+</context>
+<context>
+    <name>ToolBar</name>
+    <message>
+        <source>Start a new game</source>
+        <translation>Commencer une nouvelle partie</translation>
+    </message>
+    <message>
+        <source>Start a rated game</source>
+        <translation>Commence une partie classée</translation>
+    </message>
+    <message>
+        <source>Set the colors played by the computer</source>
+        <translation>Définir les couleurs joué par l’ordinateur</translation>
+    </message>
+    <message>
+        <source>Make the computer continue to play the current color</source>
+        <translation>Faire que l’ordinateur continue à jouer la couleur actuelle</translation>
+    </message>
+    <message>
+        <source>Make the computer play the current color</source>
+        <translation>Faire que l’ordinateur joue la couleur actuelle</translation>
+    </message>
+    <message>
+        <source>Go to beginning of game</source>
+        <translation>Aller en début de partie</translation>
+    </message>
+    <message>
+        <source>Go ten moves backward</source>
+        <translation>Revenir dix coups en arrière</translation>
+    </message>
+    <message>
+        <source>Go one move backward</source>
+        <translation>Revenir un coup en arrière</translation>
+    </message>
+    <message>
+        <source>Go one move forward</source>
+        <translation>Avancer d’un coup</translation>
+    </message>
+    <message>
+        <source>Go ten moves forward</source>
+        <translation>Avancer de dix coups</translation>
+    </message>
+    <message>
+        <source>Go to end of moves</source>
+        <translation>Aller à la fin de coups</translation>
+    </message>
+    <message>
+        <source>Go to previous variation</source>
+        <translation>Aller à la variation précédente</translation>
+    </message>
+    <message>
+        <source>Go to next variation</source>
+        <translation>Aller à la variation suivante</translation>
+    </message>
+    <message>
+        <source>Abort game analysis</source>
+        <translation>Abandonner l&apos;analyse</translation>
+    </message>
+    <message>
+        <source>Abort computer move</source>
+        <translation>Abandonner le coup de l&apos;ordinateur</translation>
+    </message>
+    <message>
+        <source>Undo move</source>
+        <extracomment>Tooltip for Undo button</extracomment>
+        <translation>Annuler le coup</translation>
+    </message>
+</context>
+</TS>
diff --git a/src/pentobi/qml/i18n/qml_nb_NO.ts b/src/pentobi/qml/i18n/qml_nb_NO.ts
new file mode 100644 (file)
index 0000000..85e59ba
--- /dev/null
@@ -0,0 +1,1744 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="nb_NO">
+<context>
+    <name>AboutDialog</name>
+    <message>
+        <source>Copyright © 2011–%1 Markus Enzenberger</source>
+        <translation>Copyright © 2011–%1 Markus Enzenberger</translation>
+    </message>
+    <message>
+        <source>Computer opponent for the board game Blokus</source>
+        <translation>Datamaskinmotstander for brettspillet Blokus</translation>
+    </message>
+    <message>
+        <source>Pentobi %1</source>
+        <extracomment>The argument is the application version.</extracomment>
+        <translation>Pentobi %1</translation>
+    </message>
+</context>
+<context>
+    <name>Actions</name>
+    <message>
+        <source>Main Variation</source>
+        <translation type="vanished">Hovedvariasjon</translation>
+    </message>
+    <message>
+        <source>Beginning of Branch</source>
+        <translation type="vanished">Begynnelse av forgreining</translation>
+    </message>
+    <message>
+        <source>Settings…</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation type="vanished">Innstillinger</translation>
+    </message>
+    <message>
+        <source>Find Move</source>
+        <translation type="vanished">Finn trekk</translation>
+    </message>
+    <message>
+        <source>Next Comment</source>
+        <translation type="vanished">Neste kommentar</translation>
+    </message>
+    <message>
+        <source>Fullscreen</source>
+        <translation type="vanished">Fullskjermsvisning</translation>
+    </message>
+    <message>
+        <source>Move Number…</source>
+        <translation type="vanished">Trekknummer…</translation>
+    </message>
+    <message>
+        <source>Pentobi Help</source>
+        <translation type="vanished">Pentobi-hjelp</translation>
+    </message>
+    <message>
+        <source>New</source>
+        <translation type="vanished">Ny</translation>
+    </message>
+    <message>
+        <source>Rated Game</source>
+        <translation type="vanished">Vurdert spill</translation>
+    </message>
+    <message>
+        <source>Open…</source>
+        <translation type="vanished">Åpne…</translation>
+    </message>
+    <message>
+        <source>Play</source>
+        <translation type="vanished">Spill</translation>
+    </message>
+    <message>
+        <source>Play Move</source>
+        <extracomment>Play a single move</extracomment>
+        <translation type="vanished">Spill enkelt trekk</translation>
+    </message>
+    <message>
+        <source>Quit</source>
+        <translation type="vanished">Avslutt</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation type="vanished">Lagre</translation>
+    </message>
+    <message>
+        <source>Save As…</source>
+        <translation type="vanished">Lagre som…</translation>
+    </message>
+    <message>
+        <source>Stop</source>
+        <translation type="vanished">Stopp</translation>
+    </message>
+    <message>
+        <source>Undo Move</source>
+        <translation type="vanished">Angre trekk</translation>
+    </message>
+    <message>
+        <source>Game Info</source>
+        <translation type="vanished">Spillinfo</translation>
+    </message>
+    <message>
+        <source>Comment</source>
+        <translation type="vanished">Kommentar</translation>
+    </message>
+    <message>
+        <source>Settings</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation type="vanished">Innstillinger</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeDialog</name>
+    <message>
+        <source>Analysis speed:</source>
+        <translation>Analysehastighet:</translation>
+    </message>
+    <message>
+        <source>Fast</source>
+        <translation>Rask</translation>
+    </message>
+    <message>
+        <source>Normal</source>
+        <translation>Normal</translation>
+    </message>
+    <message>
+        <source>Slow</source>
+        <translation>Treg</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeGame</name>
+    <message>
+        <source>(No analysis)</source>
+        <translation>(Ingen analyse)</translation>
+    </message>
+</context>
+<context>
+    <name>AppearanceDialog</name>
+    <message>
+        <source>Coordinates</source>
+        <translation>Koordinater</translation>
+    </message>
+    <message>
+        <source>Show variations</source>
+        <translation>Vis variasjoner</translation>
+    </message>
+    <message>
+        <source>Light</source>
+        <translation>Lys</translation>
+    </message>
+    <message>
+        <source>Dark</source>
+        <translation>Mørk</translation>
+    </message>
+    <message>
+        <source>Colorblind light</source>
+        <translation>Fargeblind lys</translation>
+    </message>
+    <message>
+        <source>Colorblind dark</source>
+        <translation>Fargeblind mørk</translation>
+    </message>
+    <message>
+        <source>System</source>
+        <extracomment>Name of theme using default system colors</extracomment>
+        <translation>System</translation>
+    </message>
+    <message>
+        <source>Move marking:</source>
+        <translation>Flytt markering:</translation>
+    </message>
+    <message>
+        <source>Last with dot</source>
+        <translation>Siste med punkt</translation>
+    </message>
+    <message>
+        <source>Last with number</source>
+        <translation>Siste med nummer</translation>
+    </message>
+    <message>
+        <source>All with number</source>
+        <translation>Alle med nummer</translation>
+    </message>
+    <message>
+        <source>None</source>
+        <extracomment>Move marking/None</extracomment>
+        <translation>Ingen</translation>
+    </message>
+    <message>
+        <source>Animations</source>
+        <translation>Animasjoner</translation>
+    </message>
+    <message>
+        <source>Show comment:</source>
+        <translation>Vis kommentar:</translation>
+    </message>
+    <message>
+        <source>Always</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Alltid</translation>
+    </message>
+    <message>
+        <source>As needed</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Ved behov</translation>
+    </message>
+    <message>
+        <source>Never</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Aldri</translation>
+    </message>
+    <message>
+        <source>Color theme:</source>
+        <translation>Fargepalett:</translation>
+    </message>
+    <message>
+        <source>Move number</source>
+        <extracomment>Check box in appearance dialog whether to show the move number in the status bar.</extracomment>
+        <translation>Trekknummer</translation>
+    </message>
+</context>
+<context>
+    <name>AsciiArtSaveDialog</name>
+    <message>
+        <source>Export ASCII Art</source>
+        <translation>Eksporter ASCII-kunst</translation>
+    </message>
+    <message>
+        <source>Text files</source>
+        <translation>Tekstfiler</translation>
+    </message>
+</context>
+<context>
+    <name>BoardContextMenu</name>
+    <message>
+        <source>Go to Move %1</source>
+        <translation>Gå til trekk %1</translation>
+    </message>
+    <message>
+        <source>Move Annotation</source>
+        <translation>Flytt anmerkning</translation>
+    </message>
+    <message>
+        <source>Move Annotation (%1)</source>
+        <extracomment>The argument is the annotation symbol for the current move</extracomment>
+        <translation>Flytt anmerkning (%1)</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonApply</name>
+    <message>
+        <source>Apply</source>
+        <translation>Bruk</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonCancel</name>
+    <message>
+        <source>Cancel</source>
+        <translation>Avbryt</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonClose</name>
+    <message>
+        <source>Close</source>
+        <translation>Lukk</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonOk</name>
+    <message>
+        <source>OK</source>
+        <translation>OK</translation>
+    </message>
+</context>
+<context>
+    <name>ComputerDialog</name>
+    <message>
+        <source>Computer plays:</source>
+        <translation>Datamaskinen spiller:</translation>
+    </message>
+    <message>
+        <source>Blue/Red</source>
+        <translation>Blå/rød</translation>
+    </message>
+    <message>
+        <source>Purple</source>
+        <translation>Lilla</translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation>Grønn</translation>
+    </message>
+    <message>
+        <source>Blue</source>
+        <translation>Blå</translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation>Gul/grønn</translation>
+    </message>
+    <message>
+        <source>Orange</source>
+        <translation>Oransje</translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation>Gul</translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation>Rød</translation>
+    </message>
+    <message>
+        <source>Level %1</source>
+        <translation>Nivå %1</translation>
+    </message>
+</context>
+<context>
+    <name>ExportImageDialog</name>
+    <message>
+        <source>Image width:</source>
+        <translation>Bildebredde:</translation>
+    </message>
+</context>
+<context>
+    <name>FileDialog</name>
+    <message>
+        <source>Overwrite file?</source>
+        <translation>Overskriv fil?</translation>
+    </message>
+    <message>
+        <source>Open</source>
+        <translation>Åpne</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>Lagre</translation>
+    </message>
+    <message>
+        <source>All files</source>
+        <translation>Alle filer</translation>
+    </message>
+</context>
+<context>
+    <name>GameDisplayDesktop</name>
+    <message>
+        <source>Computer is thinking…</source>
+        <translation>Datamaskinen tenker…</translation>
+    </message>
+    <message>
+        <source>Running game analysis…</source>
+        <translation>Kjører spillanalyse…</translation>
+    </message>
+    <message>
+        <source>Computer is thinking… (up to %1 seconds remaining)</source>
+        <translation>Datamaskinen tenker… (opptil %1 sekunder gjenstår)</translation>
+    </message>
+    <message>
+        <source>Computer is thinking… (up to %1 minutes remaining)</source>
+        <translation>Datamaskinen tenker… (opptil %1 minutter gjenstår)</translation>
+    </message>
+</context>
+<context>
+    <name>GameInfoDialog</name>
+    <message>
+        <source>Player Blue/Red:</source>
+        <translation>Spiller blå/rød:</translation>
+    </message>
+    <message>
+        <source>Player Purple:</source>
+        <translation>Spiller lilla:</translation>
+    </message>
+    <message>
+        <source>Player Green:</source>
+        <translation>Spiller grønn:</translation>
+    </message>
+    <message>
+        <source>Player Blue:</source>
+        <translation>Spiller blå:</translation>
+    </message>
+    <message>
+        <source>Player Yellow/Green:</source>
+        <translation>Spiller gul/grønn:</translation>
+    </message>
+    <message>
+        <source>Player Orange:</source>
+        <translation>Spiller oransje:</translation>
+    </message>
+    <message>
+        <source>Player Yellow:</source>
+        <translation>Spiller gul:</translation>
+    </message>
+    <message>
+        <source>Player Red:</source>
+        <translation>Spiller rød:</translation>
+    </message>
+    <message>
+        <source>Date:</source>
+        <translation>Dato:</translation>
+    </message>
+    <message>
+        <source>Time:</source>
+        <translation>Tid:</translation>
+    </message>
+    <message>
+        <source>Event:</source>
+        <translation>Hendelse:</translation>
+    </message>
+    <message>
+        <source>Round:</source>
+        <translation>Runde:</translation>
+    </message>
+</context>
+<context>
+    <name>GameModel</name>
+    <message>
+        <source>Blue/Red</source>
+        <translation>Blå/rød</translation>
+    </message>
+    <message>
+        <source>Purple</source>
+        <translation>Lilla</translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation>Grønn</translation>
+    </message>
+    <message>
+        <source>Blue</source>
+        <translation>Blå</translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation>Gul/grønn</translation>
+    </message>
+    <message>
+        <source>Orange</source>
+        <translation>Oransje</translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation>Gul</translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation>Rød</translation>
+    </message>
+    <message>
+        <source>Purple wins with 1 point.</source>
+        <translation>Lilla vinner med ett poeng.</translation>
+    </message>
+    <message>
+        <source>Purple wins with %L1 points.</source>
+        <translation>Lilla vinner med %L1 poeng.</translation>
+    </message>
+    <message>
+        <source>Orange wins with 1 point.</source>
+        <translation>Oransje vinner med ett poeng.</translation>
+    </message>
+    <message>
+        <source>Orange wins with %L1 points.</source>
+        <translation>Oransje vinner med %L1 poeng.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie.</source>
+        <translation>Spillet slutter uavgjort.</translation>
+    </message>
+    <message>
+        <source>Green wins with 1 point.</source>
+        <translation>Grønn vinner med ett poeng.</translation>
+    </message>
+    <message>
+        <source>Green wins with %L1 points.</source>
+        <translation>Grønn vinner med %L1 poeng.</translation>
+    </message>
+    <message>
+        <source>Blue wins with 1 point.</source>
+        <translation>Blå vinner med ett poeng.</translation>
+    </message>
+    <message>
+        <source>Blue wins with %L1 points.</source>
+        <translation>Blå vinner med %L1 poeng.</translation>
+    </message>
+    <message>
+        <source>Green wins (tie resolved).</source>
+        <translation>Grønn vinner (uavgjort tilstand løst).</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with 1 point.</source>
+        <translation>Blå/rød vinner med ett poeng.</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with %L1 points.</source>
+        <translation>Blå/rød vinner med %L1 poeng.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with 1 point.</source>
+        <translation>Gul/grønn vinnner med ett poeng.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with %L1 points.</source>
+        <translation>Gul/grønn vinner med %L1 poeng.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins (tie resolved).</source>
+        <translation>Gul/grønn vinner (uavgjort tilstand løst).</translation>
+    </message>
+    <message>
+        <source>Blue wins.</source>
+        <translation>Blå vinner.</translation>
+    </message>
+    <message>
+        <source>Yellow wins.</source>
+        <translation>Gul vinner.</translation>
+    </message>
+    <message>
+        <source>Red wins.</source>
+        <translation>Rød vinner.</translation>
+    </message>
+    <message>
+        <source>Red wins (tie resolved).</source>
+        <translation>Rød vinner (uavgjort tilstand løst).</translation>
+    </message>
+    <message>
+        <source>Yellow wins (tie resolved).</source>
+        <translation>Gul vinner (uavgjirt tilstand løst).</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Yellow.</source>
+        <translation>Spillet slutter uavgjort mellom blå og gul.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Red.</source>
+        <translation>Spillet slutter uavgjort mellom blå og rød.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow and Red.</source>
+        <translation>Spillet slutter uavgjort mellom gul og rød.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between all players.</source>
+        <translation>Spillet slutter uavgjort mellom alle spillere.</translation>
+    </message>
+    <message>
+        <source>Green wins.</source>
+        <translation>Grønn vinner.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Red.</source>
+        <translation>Spillet slutter uavgjort mellom blå, gul og rød.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Green.</source>
+        <translation>Spillet slutter uavgjort mellom blå, gul og grønn.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Red and Green.</source>
+        <translation>Spillet slutter uavgjort mellom blå, rød og grønn.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow, Red and Green.</source>
+        <translation>Spillet slutter uavgjort mellom gul, rød og grønn.</translation>
+    </message>
+    <message>
+        <source>Invalid Blokus SGF file. (%1)</source>
+        <translation>Ugyldig Blokus SGF-fil. (%1)</translation>
+    </message>
+    <message>
+        <source>Clipboard is empty.</source>
+        <translation>Utklippstavlen er tom.</translation>
+    </message>
+    <message>
+        <source>Untitled</source>
+        <translation>Uten tittel</translation>
+    </message>
+    <message>
+        <source>Untitled %1</source>
+        <extracomment>The argument is a number, which will be increased if a file with the same name already exists</extracomment>
+        <translation>Uten tittel %1</translation>
+    </message>
+    <message>
+        <source>New Folder</source>
+        <translation>Ny mappe</translation>
+    </message>
+    <message>
+        <source>New Folder %1</source>
+        <extracomment>The argument is a number, which will be increased if a folder with the same name already exists</extracomment>
+        <translation>Ny mappe %1</translation>
+    </message>
+    <message>
+        <source>(Setup)</source>
+        <translation>(Oppsett)</translation>
+    </message>
+    <message>
+        <source>(No moves)</source>
+        <translation>(Ingen trekk)</translation>
+    </message>
+    <message>
+        <source>Move %1</source>
+        <extracomment>The argument is the current move number.</extracomment>
+        <translation>Trekk %1</translation>
+    </message>
+    <message>
+        <source>Unsupported character set</source>
+        <translation>Ustøttet tegnsett</translation>
+    </message>
+</context>
+<context>
+    <name>GameVariantDialog</name>
+    <message>
+        <source>Classic</source>
+        <translation>Klassisk</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>Duo</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>Junior</translation>
+    </message>
+    <message>
+        <source>Trigon</source>
+        <translation>Trigon</translation>
+    </message>
+    <message>
+        <source>Nexos</source>
+        <translation>Nexos</translation>
+    </message>
+    <message>
+        <source>GembloQ</source>
+        <translation>GembloQ</translation>
+    </message>
+    <message>
+        <source>Callisto</source>
+        <translation>Callisto</translation>
+    </message>
+    <message>
+        <source>Players:</source>
+        <translation>Spillere:</translation>
+    </message>
+    <message>
+        <source>Colors:</source>
+        <translation>Farger:</translation>
+    </message>
+</context>
+<context>
+    <name>GotoMoveDialog</name>
+    <message>
+        <source>Move number:</source>
+        <translation>Trekknummer:</translation>
+    </message>
+</context>
+<context>
+    <name>HelpWindow</name>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Pentobi-hjelp</translation>
+    </message>
+</context>
+<context>
+    <name>ImageSaveDialog</name>
+    <message>
+        <source>Save Image</source>
+        <translation>Lagre bilde</translation>
+    </message>
+    <message>
+        <source>PNG image files</source>
+        <translation>PNG-bildefiler</translation>
+    </message>
+    <message>
+        <source>JPEG image files</source>
+        <translation>JPEG-bildefiler</translation>
+    </message>
+</context>
+<context>
+    <name>InitialRatingDialog</name>
+    <message>
+        <source>Initialize your rating for this game variant.</source>
+        <translation>Hent din vurdering for denne spillvarianten.</translation>
+    </message>
+    <message>
+        <source>Initial rating:</source>
+        <translation>Startsvurdering:</translation>
+    </message>
+    <message>
+        <source>Beginner</source>
+        <translation>Begynner</translation>
+    </message>
+    <message>
+        <source>Expert</source>
+        <translation>Ekspert</translation>
+    </message>
+</context>
+<context>
+    <name>Main</name>
+    <message>
+        <source>Pentobi</source>
+        <extracomment>Window title if no file is loaded.</extracomment>
+        <translation>Pentobi</translation>
+    </message>
+    <message>
+        <source>Game analysis is only possible in main variation.</source>
+        <translation>Spillanalyse er kun tilgjengelig i hovedvariasjonen.</translation>
+    </message>
+    <message>
+        <source>Autosaved game was changed by another instance of Pentobi. Overwrite?</source>
+        <translation>Automatisk lagret spill ble endret av en annen kjørende utgave av Pentobi. Overskriv?</translation>
+    </message>
+    <message>
+        <source>Your rating has increased from %1 to %2.</source>
+        <translation>Din vurdering har økt fra %1 til %2.</translation>
+    </message>
+    <message>
+        <source>Your rating has decreased from %1 to %2.</source>
+        <translation>Din vurdering har sunket fra %1 til %2.</translation>
+    </message>
+    <message>
+        <source>Your rating stays at %1.</source>
+        <translation>Din vurdering forblir %1.</translation>
+    </message>
+    <message>
+        <source>No permission to access storage</source>
+        <translation>Mangler lagringstilgang</translation>
+    </message>
+    <message>
+        <source>Delete all rating information for the current game variant?</source>
+        <translation>Slett all vurderingsinformasjon fra nåværende spillvariant?</translation>
+    </message>
+    <message>
+        <source>Delete all variations?</source>
+        <translation>Slett alle variasjoner?</translation>
+    </message>
+    <message>
+        <source>Save failed.</source>
+        <translation>Lagring mislyktes.</translation>
+    </message>
+    <message>
+        <source>End of tree was reached. Continue search from start of the tree?</source>
+        <translation>Nådde slutten av treet. Fortsett søket fra starten av treet?</translation>
+    </message>
+    <message>
+        <source>No comment found</source>
+        <translation>Ingen kommentar funnet</translation>
+    </message>
+    <message>
+        <source>%1 (modified)</source>
+        <translation>%1 (endret)</translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Reload?</source>
+        <translation>Filen har blitt endret av et annet program. Last inn på nytt?</translation>
+    </message>
+    <message>
+        <source>Continue computer move?</source>
+        <translation>Fortsett datamaskintrekk?</translation>
+    </message>
+    <message>
+        <source>Keep only position?</source>
+        <translation>Kun behold posisjonen?</translation>
+    </message>
+    <message>
+        <source>Keep only subtree?</source>
+        <translation>Kun behold undertreet?</translation>
+    </message>
+    <message>
+        <source>Open failed.</source>
+        <translation>Åpning mislyktes.</translation>
+    </message>
+    <message>
+        <source>Start rated game with Purple against Pentobi level %1?</source>
+        <translation>Start vurdert spill med lilla mot Pentobi nivå %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Green against Pentobi level %1?</source>
+        <translation>Start vurdert spill med grønn mot Pentobi nivå %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue/Red against Pentobi level %1?</source>
+        <translation>Start vurdert spill med blå/rød mot Pentobi nivå %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue against Pentobi level %1?</source>
+        <translation>Start vurdert spill med blå mot Pentobi nivå %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Orange against Pentobi level %1?</source>
+        <translation>Start vurdert spill med oransje mot Pentobi nivå %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow/Green against Pentobi level %1?</source>
+        <translation>Start vurdert spill med gul/grønn mot Pentobi nivå %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow against Pentobi level %1?</source>
+        <translation>Start vurdert spill med gul mot Pentobi nivå %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Red against Pentobi level %1?</source>
+        <translation>Start vurdert spill med rød mot Pentobi nivå %1?</translation>
+    </message>
+    <message>
+        <source>You have not yet played rated games in this game variant.</source>
+        <translation>Du har ikke spillt noen vurderte spill i denne varianten enda.</translation>
+    </message>
+    <message>
+        <source>Truncate this subtree?</source>
+        <translation>Forkort dette undertreet?</translation>
+    </message>
+    <message>
+        <source>Truncate children?</source>
+        <translation>Forkort underprosess?</translation>
+    </message>
+    <message>
+        <source>Discard game?</source>
+        <translation>Forkast spill?</translation>
+    </message>
+    <message>
+        <source>Pentobi %1 (level %2)</source>
+        <extracomment>Player name for game info in rated game. First argument is version of Pentobi, second argument is level.</extracomment>
+        <translation>Pentobi %1 (nivå %2)</translation>
+    </message>
+    <message>
+        <source>Human</source>
+        <extracomment>Player name for game info in rated game.</extracomment>
+        <translation>Menneske</translation>
+    </message>
+    <message>
+        <source>Rated game</source>
+        <translation>Vurdert spill</translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Overwrite?</source>
+        <translation>Filen har blitt endret av et annet program. Overskriv?</translation>
+    </message>
+    <message>
+        <source>%1 - Pentobi</source>
+        <extracomment>Window title if file is loaded. The argument is the file name prepended with a star if the file has been modified.</extracomment>
+        <translation>%1 - Pentobi</translation>
+    </message>
+    <message>
+        <source>Not enough memory</source>
+        <translation>Ikke nok minne</translation>
+    </message>
+    <message>
+        <source>Game analysis aborted</source>
+        <translation>Spillanalyse avbrutt</translation>
+    </message>
+    <message>
+        <source>Computer move aborted</source>
+        <translation>Datamaskintrekk avbrutt</translation>
+    </message>
+    <message>
+        <source>Rating information deleted</source>
+        <translation>Vurderingsinformasjon slettet</translation>
+    </message>
+    <message>
+        <source>Variations deleted</source>
+        <translation>Variasjoner slettet</translation>
+    </message>
+    <message>
+        <source>File saved</source>
+        <translation>Fil lagret</translation>
+    </message>
+    <message>
+        <source>Saving image failed or unsupported image format</source>
+        <translation>Lagring av bilde mislyktes, eller ustøttet bildeformat</translation>
+    </message>
+    <message>
+        <source>Image saved</source>
+        <translation>Bilde lagret</translation>
+    </message>
+    <message>
+        <source>Creating image failed</source>
+        <translation>Oppretting av bilde mislyktes</translation>
+    </message>
+    <message>
+        <source>Continuing rated game</source>
+        <translation>Fortsetter vurdert spill</translation>
+    </message>
+    <message>
+        <source>Kept only position</source>
+        <translation>Beholdt kun posisjonen</translation>
+    </message>
+    <message>
+        <source>Kept only subtree</source>
+        <translation>Beholdte kun undertre</translation>
+    </message>
+    <message>
+        <source>Variation is now %1</source>
+        <translation>Varianten er nå %1</translation>
+    </message>
+    <message>
+        <source>Children truncated</source>
+        <translation>Underprosess forkortet</translation>
+    </message>
+    <message>
+        <source>Setup</source>
+        <extracomment>Small-screen label for setup mode (short for &quot;Setup Mode&quot;).</extracomment>
+        <translation>Oppsett</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Oppsettsmodus</translation>
+    </message>
+    <message>
+        <source>Rated</source>
+        <extracomment>Label for ongoing rated game</extracomment>
+        <translation>Vurdert</translation>
+    </message>
+    <message>
+        <source>Rated %1</source>
+        <extracomment>Small-screen label for finished rated game (short for &quot;Rated Game&quot;). The argument is the game number.</extracomment>
+        <translation>Vurdert %1</translation>
+    </message>
+    <message>
+        <source>Rated Game %1</source>
+        <extracomment>Label for rated game. The argument is the game number.</extracomment>
+        <translation>Vurdert spill %1</translation>
+    </message>
+    <message>
+        <source>Main Variation</source>
+        <translation>Hovedvariasjon</translation>
+    </message>
+    <message>
+        <source>Beginning of Branch</source>
+        <translation>Begynnelse av forgreining</translation>
+    </message>
+    <message>
+        <source>Comment</source>
+        <translation>Kommentar</translation>
+    </message>
+    <message>
+        <source>Settings</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation>Innstillinger</translation>
+    </message>
+    <message>
+        <source>Find Move</source>
+        <translation>Finn trekk</translation>
+    </message>
+    <message>
+        <source>Next Comment</source>
+        <translation>Neste kommentar</translation>
+    </message>
+    <message>
+        <source>Fullscreen</source>
+        <translation>Fullskjermsvisning</translation>
+    </message>
+    <message>
+        <source>Game Info</source>
+        <translation>Spillinfo</translation>
+    </message>
+    <message>
+        <source>Move Number…</source>
+        <translation>Trekknummer…</translation>
+    </message>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Pentobi-hjelp</translation>
+    </message>
+    <message>
+        <source>New</source>
+        <translation>Ny</translation>
+    </message>
+    <message>
+        <source>Rated Game</source>
+        <translation>Vurdert spill</translation>
+    </message>
+    <message>
+        <source>Open…</source>
+        <translation>Åpne…</translation>
+    </message>
+    <message>
+        <source>Play</source>
+        <translation>Spill</translation>
+    </message>
+    <message>
+        <source>Play Move</source>
+        <extracomment>Play a single move</extracomment>
+        <translation>Spill enkelt trekk</translation>
+    </message>
+    <message>
+        <source>Quit</source>
+        <translation>Avslutt</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>Lagre</translation>
+    </message>
+    <message>
+        <source>Save As…</source>
+        <translation>Lagre som…</translation>
+    </message>
+    <message>
+        <source>Stop</source>
+        <translation>Stopp</translation>
+    </message>
+    <message>
+        <source>Undo Move</source>
+        <translation>Angre trekk</translation>
+    </message>
+</context>
+<context>
+    <name>MenuComputer</name>
+    <message>
+        <source>Computer</source>
+        <translation>Datamaskin</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu Computer. Leave empty for no mnemonic.</extracomment>
+        <translation>D</translation>
+    </message>
+    <message>
+        <source>S</source>
+        <extracomment>Mnemonic for menu item Computer Settings. Leave empty for no mnemonic.</extracomment>
+        <translation>L</translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu item Play. Leave empty for no mnemonic.</extracomment>
+        <translation>S</translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Play Move. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu item Stop. Leave empty for no mnemonic.</extracomment>
+        <translation>O</translation>
+    </message>
+</context>
+<context>
+    <name>MenuEdit</name>
+    <message>
+        <source>Edit</source>
+        <translation>Rediger</translation>
+    </message>
+    <message>
+        <source>E</source>
+        <extracomment>Mnemonic for menu Edit. Leave empty for no mnemonic.</extracomment>
+        <translation>R</translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic.</extracomment>
+        <translation>H</translation>
+    </message>
+    <message>
+        <source>Make Main Variation</source>
+        <translation>Gjør til hovedvariasjon</translation>
+    </message>
+    <message>
+        <source>Variation Up</source>
+        <extracomment>Short for Move Variation Up</extracomment>
+        <translation>Variasjon oppover</translation>
+    </message>
+    <message>
+        <source>U</source>
+        <extracomment>Mnemonic for menu item Variation Up. Leave empty for no mnemonic.</extracomment>
+        <translation>O</translation>
+    </message>
+    <message>
+        <source>Variation Down</source>
+        <extracomment>Short for Move Variation Down</extracomment>
+        <translation>Variasjon nedover</translation>
+    </message>
+    <message>
+        <source>W</source>
+        <extracomment>Mnemonic for menu item Variation Down. Leave empty for no mnemonic.</extracomment>
+        <translation>N</translation>
+    </message>
+    <message>
+        <source>Delete Variations</source>
+        <translation>Slett variasjoner</translation>
+    </message>
+    <message>
+        <source>D</source>
+        <extracomment>Mnemonic for menu item Delete Variations. Leave empty for no mnemonic.</extracomment>
+        <translation>S</translation>
+    </message>
+    <message>
+        <source>Truncate</source>
+        <translation>Forkort</translation>
+    </message>
+    <message>
+        <source>T</source>
+        <extracomment>Mnemonic for menu item Truncate. Leave empty for no mnemonic.</extracomment>
+        <translation>F</translation>
+    </message>
+    <message>
+        <source>Truncate Children</source>
+        <translation>Forkort underprosess</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Truncate Children. Leave empty for no mnemonic.</extracomment>
+        <translation>U</translation>
+    </message>
+    <message>
+        <source>Keep Position</source>
+        <translation>Behold posisjon</translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu item Keep Position. Leave empty for no mnemonic.</extracomment>
+        <translation>B</translation>
+    </message>
+    <message>
+        <source>Keep Subtree</source>
+        <translation>Behold undertreet</translation>
+    </message>
+    <message>
+        <source>S</source>
+        <extracomment>Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic.</extracomment>
+        <translation>T</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Oppsettsmodus</translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu item Setup Mode. Leave empty for no mnemonic.</extracomment>
+        <translation>M</translation>
+    </message>
+    <message>
+        <source>Next Color</source>
+        <translation>Neste farge</translation>
+    </message>
+    <message>
+        <source>N</source>
+        <extracomment>Mnemonic for menu item Next Color. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+    <message>
+        <source>Annotation…</source>
+        <translation>Anmerkning…</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item Annotation. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>Made main variation</source>
+        <translation>Gjort til hovedvariasjon</translation>
+    </message>
+</context>
+<context>
+    <name>MenuExport</name>
+    <message>
+        <source>Export</source>
+        <translation>Eksporter</translation>
+    </message>
+    <message>
+        <source>E</source>
+        <extracomment>Mnemonic for menu Export. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Image. Leave empty for no mnemonic.</extracomment>
+        <translation>B</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item ASCII Art. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>Image…</source>
+        <translation>Bilde…</translation>
+    </message>
+    <message>
+        <source>ASCII Art…</source>
+        <translation>ASCII-kunst…</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGame</name>
+    <message>
+        <source>Game</source>
+        <translation>Spil</translation>
+    </message>
+    <message>
+        <source>G</source>
+        <extracomment>Mnemonic for menu Game. Leave empty for no mnemonic.</extracomment>
+        <translation>S</translation>
+    </message>
+    <message>
+        <source>N</source>
+        <extracomment>Mnemonic for menu item New. Leave empty for no mnemonic.</extracomment>
+        <translation>N</translation>
+    </message>
+    <message>
+        <source>R</source>
+        <extracomment>Mnemonic for menu item Rated Game. Leave empty for no mnemonic.</extracomment>
+        <translation>U</translation>
+    </message>
+    <message>
+        <source>Game Variant…</source>
+        <translation>Spillvariant…</translation>
+    </message>
+    <message>
+        <source>V</source>
+        <extracomment>Mnemonic for menu item Game Variant. Leave empty for no mnemonic.</extracomment>
+        <translation>V</translation>
+    </message>
+    <message>
+        <source>I</source>
+        <extracomment>Mnemonic for menu item Game Info. Leave empty for no mnemonic.</extracomment>
+        <translation>O</translation>
+    </message>
+    <message>
+        <source>U</source>
+        <extracomment>Mnemonic for menu item Undo. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>F</source>
+        <extracomment>Mnemonic for menu item Find Move. Leave empty for no mnemonic.</extracomment>
+        <translation>F</translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu item Open. Leave empty for no mnemonic.</extracomment>
+        <translation>P</translation>
+    </message>
+    <message>
+        <source>Open Clipboard</source>
+        <translation>Åpne utklippstavle</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic.</extracomment>
+        <translation>K</translation>
+    </message>
+    <message>
+        <source>S</source>
+        <extracomment>Mnemonic for menu item Save. Leave empty for no mnemonic.</extracomment>
+        <translation>L</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item Save As. Leave empty for no mnemonic.</extracomment>
+        <translation>S</translation>
+    </message>
+    <message>
+        <source>Q</source>
+        <extracomment>Mnemonic for menu item Quit. Leave empty for no mnemonic.</extracomment>
+        <translation>T</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGo</name>
+    <message>
+        <source>Go</source>
+        <translation>Gå</translation>
+    </message>
+    <message>
+        <source>O</source>
+        <extracomment>Mnemonic for menu Go. Leave empty for no mnemonic.</extracomment>
+        <translation>G</translation>
+    </message>
+    <message>
+        <source>N</source>
+        <extracomment>Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic.</extracomment>
+        <translation>N</translation>
+    </message>
+    <message>
+        <source>M</source>
+        <extracomment>Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic.</extracomment>
+        <translation>H</translation>
+    </message>
+    <message>
+        <source>B</source>
+        <extracomment>Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic.</extracomment>
+        <translation>F</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Next Comment. Leave empty for no mnemonic.</extracomment>
+        <translation>K</translation>
+    </message>
+</context>
+<context>
+    <name>MenuHelp</name>
+    <message>
+        <source>Help</source>
+        <translation>Hjelp</translation>
+    </message>
+    <message>
+        <source>H</source>
+        <extracomment>Mnemonic for menu Help. Leave empty for no mnemonic.</extracomment>
+        <translation>H</translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic.</extracomment>
+        <translation>P</translation>
+    </message>
+    <message>
+        <source>About Pentobi</source>
+        <translation>Om Pentobi</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item About Pentobi. Leave empty for no mnemonic.</extracomment>
+        <translation>O</translation>
+    </message>
+    <message>
+        <source>Report Bug</source>
+        <translation>Innrapporter feil</translation>
+    </message>
+    <message>
+        <source>B</source>
+        <extracomment>Mnemonic for menu item Report Bug. Leave empty for no mnemonic.</extracomment>
+        <translation>R</translation>
+    </message>
+</context>
+<context>
+    <name>MenuItem</name>
+    <message>
+        <source>Ctrl</source>
+        <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+        <translation>Ctrl</translation>
+    </message>
+    <message>
+        <source>Shift</source>
+        <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+        <translation>Shift</translation>
+    </message>
+</context>
+<context>
+    <name>MenuRecentFiles</name>
+    <message>
+        <source>Open Recent</source>
+        <translation>Åpne nylige</translation>
+    </message>
+    <message>
+        <source>P</source>
+        <extracomment>Mnemonic for menu Open Recent. Leave empty for no mnemonic.</extracomment>
+        <translation>Y</translation>
+    </message>
+    <message>
+        <source>%1. %2</source>
+        <extracomment>Format in recent files menu. First argument is the file number, second argument the file name.</extracomment>
+        <translation>%1. %2</translation>
+    </message>
+    <message>
+        <source>Clear List</source>
+        <extracomment>Menu item for clearing the recent files list</extracomment>
+        <translation>Tøm liste</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic.</extracomment>
+        <translation>T</translation>
+    </message>
+</context>
+<context>
+    <name>MenuTools</name>
+    <message>
+        <source>Tools</source>
+        <translation>Verktøy</translation>
+    </message>
+    <message>
+        <source>T</source>
+        <extracomment>Mnemonic for menu Tools. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+    <message>
+        <source>Rating</source>
+        <translation>Vurdering</translation>
+    </message>
+    <message>
+        <source>R</source>
+        <extracomment>Mnemonic for menu item Rating. Leave empty for no mnemonic.</extracomment>
+        <translation>U</translation>
+    </message>
+    <message>
+        <source>Clear Rating</source>
+        <translation>Fjern vurdering</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item Clear Rating. Leave empty for no mnemonic.</extracomment>
+        <translation>F</translation>
+    </message>
+    <message>
+        <source>Analyze Game</source>
+        <translation>Analyser spill</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu item Analyze Game. Leave empty for no mnemonic.</extracomment>
+        <translation>A</translation>
+    </message>
+    <message>
+        <source>Clear Analysis</source>
+        <translation>Tøm analyse</translation>
+    </message>
+    <message>
+        <source>E</source>
+        <extracomment>Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic.</extracomment>
+        <translation>N</translation>
+    </message>
+    <message>
+        <source>Analyze Game…</source>
+        <translation>Analyser spill…</translation>
+    </message>
+</context>
+<context>
+    <name>MenuView</name>
+    <message>
+        <source>View</source>
+        <translation>Vis</translation>
+    </message>
+    <message>
+        <source>V</source>
+        <extracomment>Mnemonic for menu View. Leave empty for no mnemonic.</extracomment>
+        <translation>V</translation>
+    </message>
+    <message>
+        <source>Appearance…</source>
+        <translation type="vanished">Utseende…</translation>
+    </message>
+    <message>
+        <source>A</source>
+        <extracomment>Mnemonic for menu Appearance. Leave empty for no mnemonic.</extracomment>
+        <translation>U</translation>
+    </message>
+    <message>
+        <source>F</source>
+        <extracomment>Mnemonic for menu item Fullscreen. Leave empty for no mnemonic.</extracomment>
+        <translation>F</translation>
+    </message>
+    <message>
+        <source>C</source>
+        <extracomment>Mnemonic for menu item View/Comment. Leave empty for no mnemonic.</extracomment>
+        <translation>K</translation>
+    </message>
+    <message>
+        <source>Appearance</source>
+        <translation>Utseende</translation>
+    </message>
+    <message>
+        <source>Toolbar</source>
+        <translation>Verktøyslinje</translation>
+    </message>
+    <message>
+        <source>T</source>
+        <extracomment>Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic.</extracomment>
+        <translation>E</translation>
+    </message>
+</context>
+<context>
+    <name>MoveAnnotationDialog</name>
+    <message>
+        <source>Move %1</source>
+        <translation>Trekk %1</translation>
+    </message>
+    <message>
+        <source>Very good</source>
+        <translation>Veldig god</translation>
+    </message>
+    <message>
+        <source>Good</source>
+        <translation>God</translation>
+    </message>
+    <message>
+        <source>Interesting</source>
+        <translation>Interessant</translation>
+    </message>
+    <message>
+        <source>Doubtful</source>
+        <translation>Tvilsom</translation>
+    </message>
+    <message>
+        <source>Bad</source>
+        <translation>Dårlig</translation>
+    </message>
+    <message>
+        <source>Very Bad</source>
+        <translation>Veldig dårlig</translation>
+    </message>
+    <message>
+        <source>No annotation</source>
+        <translation>Ingen tilknytning</translation>
+    </message>
+</context>
+<context>
+    <name>NewFolderDialog</name>
+    <message>
+        <source>Folder name:</source>
+        <translation>Mappenavn:</translation>
+    </message>
+</context>
+<context>
+    <name>OpenDialog</name>
+    <message>
+        <source>Open</source>
+        <translation>Åpne</translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation>Blokus-spill</translation>
+    </message>
+</context>
+<context>
+    <name>RatingDialog</name>
+    <message>
+        <source>Your rating:</source>
+        <translation>Din vurdering:</translation>
+    </message>
+    <message>
+        <source>Game variant:</source>
+        <translation>Spillvariant:</translation>
+    </message>
+    <message>
+        <source>Classic (2)</source>
+        <extracomment>Short for Classic (2 players)</extracomment>
+        <translation>Klassisk (2)</translation>
+    </message>
+    <message>
+        <source>Classic (3)</source>
+        <extracomment>Short for Classic (3 players)</extracomment>
+        <translation>Klassisk (3)</translation>
+    </message>
+    <message>
+        <source>Classic (4)</source>
+        <extracomment>Short for Classic (4 players)</extracomment>
+        <translation>Klassisk (4)</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>Duo</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>Junior</translation>
+    </message>
+    <message>
+        <source>Trigon (2)</source>
+        <extracomment>Short for Trigon (2 players)</extracomment>
+        <translation>Trigon (2)</translation>
+    </message>
+    <message>
+        <source>Trigon (3)</source>
+        <extracomment>Short for Trigon (3 players)</extracomment>
+        <translation>Trigon (3)</translation>
+    </message>
+    <message>
+        <source>Trigon (4)</source>
+        <extracomment>Short for Trigon (4 players)</extracomment>
+        <translation>Trigon (4)</translation>
+    </message>
+    <message>
+        <source>Nexos (2)</source>
+        <extracomment>Short for Nexos (2 players)</extracomment>
+        <translation>Nexos (2)</translation>
+    </message>
+    <message>
+        <source>Nexos (4)</source>
+        <extracomment>Short for Nexos (4 players)</extracomment>
+        <translation>Nexos (4)</translation>
+    </message>
+    <message>
+        <source>Callisto (2)</source>
+        <extracomment>Short for Callisto (2 players, 2 colors)</extracomment>
+        <translation>Callisto (2)</translation>
+    </message>
+    <message>
+        <source>Callisto (2/4)</source>
+        <extracomment>Short for Callisto (2 players, 4 colors)</extracomment>
+        <translation>Callisto (2/4)</translation>
+    </message>
+    <message>
+        <source>Callisto (3)</source>
+        <extracomment>Short for Callisto (3 players)</extracomment>
+        <translation>Callisto (3)</translation>
+    </message>
+    <message>
+        <source>Callisto (4)</source>
+        <extracomment>Short for Callisto (4 players)</extracomment>
+        <translation>Callisto (4)</translation>
+    </message>
+    <message>
+        <source>GembloQ (4)</source>
+        <extracomment>Short for GembloQ (4 players)</extracomment>
+        <translation>GembloQ (4)</translation>
+    </message>
+    <message>
+        <source>GembloQ (2)</source>
+        <extracomment>Short for GembloQ (2 players, 2 colors)</extracomment>
+        <translation>GembloQ (2)</translation>
+    </message>
+    <message>
+        <source>GembloQ (2/4)</source>
+        <extracomment>Short for GembloQ (2 players, 4 colors)</extracomment>
+        <translation>GembloQ (2/4)</translation>
+    </message>
+    <message>
+        <source>GembloQ (3)</source>
+        <extracomment>Short for GembloQ (3 players)</extracomment>
+        <translation>GembloQ (3)</translation>
+    </message>
+    <message>
+        <source>Rated games:</source>
+        <translation>Vurderte spill:</translation>
+    </message>
+    <message>
+        <source>Best previous rating:</source>
+        <translation>Beste tidligere vurdering:</translation>
+    </message>
+    <message>
+        <source>Recent development:</source>
+        <translation>Nylig utvikling:</translation>
+    </message>
+    <message>
+        <source>Game</source>
+        <translation>Spil</translation>
+    </message>
+    <message>
+        <source>Result</source>
+        <translation>Resultat</translation>
+    </message>
+    <message>
+        <source>Win</source>
+        <extracomment>Result of rated game is a win</extracomment>
+        <translation>Vunnet</translation>
+    </message>
+    <message>
+        <source>Loss</source>
+        <extracomment>Result of rated game is a loss</extracomment>
+        <translation>Tapt</translation>
+    </message>
+    <message>
+        <source>Tie</source>
+        <extracomment>Result of rated game is a tie. Abbreviate long translations to ensure that all columns of rated games list are visible on mobile devices with small screens.</extracomment>
+        <translation>Uavgjort</translation>
+    </message>
+    <message>
+        <source>Level</source>
+        <translation>Nivå</translation>
+    </message>
+    <message>
+        <source>Your Color</source>
+        <translation>Din farge</translation>
+    </message>
+    <message>
+        <source>Date</source>
+        <translation>Dato</translation>
+    </message>
+    <message>
+        <source>Open Game %1</source>
+        <translation>Åpne spill %1</translation>
+    </message>
+</context>
+<context>
+    <name>SaveDialog</name>
+    <message>
+        <source>Save</source>
+        <translation>Lagre</translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation>Blokus-spill</translation>
+    </message>
+</context>
+<context>
+    <name>ToolBar</name>
+    <message>
+        <source>Start a new game</source>
+        <translation>Start nytt spill</translation>
+    </message>
+    <message>
+        <source>Start a rated game</source>
+        <translation>Start vurdert spill</translation>
+    </message>
+    <message>
+        <source>Set the colors played by the computer</source>
+        <translation>Sett fargene spilt av datamaskinen</translation>
+    </message>
+    <message>
+        <source>Make the computer continue to play the current color</source>
+        <translation>Få datamaskinen til å fortsette å spille gjeldende farge</translation>
+    </message>
+    <message>
+        <source>Make the computer play the current color</source>
+        <translation>Få datamaskinen til å spille gjeldende farge</translation>
+    </message>
+    <message>
+        <source>Go to beginning of game</source>
+        <translation>Gå til begynnelsen av spillet</translation>
+    </message>
+    <message>
+        <source>Go ten moves backward</source>
+        <translation>Gå ti trekk tilbake</translation>
+    </message>
+    <message>
+        <source>Go one move backward</source>
+        <translation>Gå ett steg bakover</translation>
+    </message>
+    <message>
+        <source>Go one move forward</source>
+        <translation>Gå ett steg forover</translation>
+    </message>
+    <message>
+        <source>Go ten moves forward</source>
+        <translation>Gå ti trekk forover</translation>
+    </message>
+    <message>
+        <source>Go to end of moves</source>
+        <translation>Gå til trekkslutt</translation>
+    </message>
+    <message>
+        <source>Go to previous variation</source>
+        <translation>Gå til forrige variasjon</translation>
+    </message>
+    <message>
+        <source>Go to next variation</source>
+        <translation>Gå til neste variasjon</translation>
+    </message>
+    <message>
+        <source>Abort game analysis</source>
+        <translation>Avbryt spillanalyse</translation>
+    </message>
+    <message>
+        <source>Abort computer move</source>
+        <translation>Avbryt datamaskintrekk</translation>
+    </message>
+    <message>
+        <source>Undo move</source>
+        <extracomment>Tooltip for Undo button</extracomment>
+        <translation>Angre trekk</translation>
+    </message>
+</context>
+</TS>
diff --git a/src/pentobi/qml/i18n/translations.qrc b/src/pentobi/qml/i18n/translations.qrc
new file mode 100644 (file)
index 0000000..89f05df
--- /dev/null
@@ -0,0 +1,7 @@
+<RCC>
+    <qresource prefix="/qml/i18n">
+        <file>qml_de.qm</file>
+        <file>qml_fr.qm</file>
+        <file>qml_nb_NO.qm</file>
+    </qresource>
+</RCC>
diff --git a/src/pentobi/qml/icons/filedialog-folder.svg b/src/pentobi/qml/icons/filedialog-folder.svg
new file mode 100644 (file)
index 0000000..c60410a
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="SVGRoot" width="16px" height="16px" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path d="m1.5 4.5h13v-2h-9v-1h-4z" fill="#fff" stroke="#404040" stroke-linejoin="round" stroke-width="1px"/>
+ <path d="m0.5 4.5h15v10h-15z" fill="#bfbfbf" stroke="#404040" stroke-linejoin="round" stroke-width="1px"/>
+</svg>
diff --git a/src/pentobi/qml/icons/filedialog-newfolder.svg b/src/pentobi/qml/icons/filedialog-newfolder.svg
new file mode 100644 (file)
index 0000000..53239a8
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="SVGRoot" width="16px" height="16px" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <rect transform="rotate(90)" x="7" y="-14" width="2" height="12" ry="0" style="paint-order:fill markers stroke"/>
+ <rect transform="scale(-1)" x="-9" y="-14" width="2" height="12" ry="0" style="paint-order:fill markers stroke"/>
+</svg>
diff --git a/src/pentobi/qml/icons/filedialog-parent.svg b/src/pentobi/qml/icons/filedialog-parent.svg
new file mode 100644 (file)
index 0000000..ae20c44
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="SVGRoot" width="16px" height="16px" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path d="m10.944 12.942v-9.8981l-6.9007 4.9491 6.9007 4.9491" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.0871"/>
+</svg>
diff --git a/src/pentobi/qml/themes/colorblind-dark/Theme.qml b/src/pentobi/qml/themes/colorblind-dark/Theme.qml
new file mode 100644 (file)
index 0000000..961bc6a
--- /dev/null
@@ -0,0 +1,18 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/themes/colorblind-dark/Theme.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import "../colorblind-light" as ColorblindLight
+import "../dark" as Dark
+
+Dark.Theme {
+    property var colorBlue: [ "#008f9d", "#006069", "#00bcce", "#ffffff" ]
+    property var colorGreen: [ "#72a074", "#4e7450", "#9cbc9e", "#ffffff" ]
+    property var colorOrange: colorRed
+    property var colorPurple: colorBlue
+    property var colorRed: [ "#984326", "#692e19", "#ca5a30", "#ffffff" ]
+    property var colorYellow: [ "#bb7031", "#8c5525", "#d28b4f", "#ffffff" ]
+}
diff --git a/src/pentobi/qml/themes/colorblind-light/Theme.qml b/src/pentobi/qml/themes/colorblind-light/Theme.qml
new file mode 100644 (file)
index 0000000..668e8c2
--- /dev/null
@@ -0,0 +1,16 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/themes/colorblind-light/Theme.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "../light" as Light
+
+Light.Theme {
+    property var colorBlue: [ "#008f9d", "#006069", "#00bcce", "#ffffff" ]
+    property var colorGreen: [ "#72a074", "#4e7450", "#9cbc9e", "#ffffff" ]
+    property var colorOrange: colorRed
+    property var colorPurple: colorBlue
+    property var colorRed: [ "#984326", "#692e19", "#ca5a30", "#ffffff" ]
+    property var colorYellow: [ "#bb7031", "#8c5525", "#d28b4f", "#ffffff" ]
+}
diff --git a/src/pentobi/qml/themes/dark/Theme.qml b/src/pentobi/qml/themes/dark/Theme.qml
new file mode 100644 (file)
index 0000000..57c0c75
--- /dev/null
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/themes/dark/Theme.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import "../light" as Light
+
+// See themes/light/Theme.qml for comments
+Light.Theme {
+    property var colorBoard: [ "#494347", "#3b3639", "#6d686b",
+                               "#696267", "#5a5458", "#797276" ]
+
+    property color colorBackground: "#131313"
+    property color colorButtonPressed: Qt.lighter(colorBackground, 4)
+    property color colorButtonHovered: Qt.lighter(colorBackground, 2)
+    property color colorButtonBorder: Qt.lighter(colorBackground, 5)
+    property color colorCommentBase: "#1e2028"
+    property color colorCommentBorder: "#5a5756"
+    property color colorCommentFocus: "#4799cc"
+    property color colorCommentText: "#C8C1BE"
+    property color colorMessageText: "#C8C1BE"
+    property color colorMessageBase: "#333333"
+    property color colorSelectedText: colorBackground
+    property color colorSelection: "#4799cc"
+    property color colorStartingPoint: "#82777E"
+    property color colorText: "#e6d5e1"
+
+    function getImage(name) {
+        if (name === "pentobi-rated-game"
+                || name.startsWith("piece-manipulator"))
+            return "themes/dark/" + name + ".svg"
+        return light.getImage(name)
+    }
+
+    Light.Theme { id: light }
+}
diff --git a/src/pentobi/qml/themes/dark/pentobi-rated-game.svg b/src/pentobi/qml/themes/dark/pentobi-rated-game.svg
new file mode 100644 (file)
index 0000000..fca1d78
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m15.5 8a7.4998 7.4999 0 0 1-7.4972 7.4999 7.4998 7.4999 0 0 1-7.5025-7.4945 7.4998 7.4999 0 0 1 7.4917-7.5052 7.4998 7.4999 0 0 1 7.5079 7.4892" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.0003" style="paint-order:fill markers stroke"/>
+ <path d="m14 8a6 6 0 0 1-5.9979 6 6 6 0 0 1-6.0021-5.9957 6 6 0 0 1 5.9936-6.0043 6 6 0 0 1 6.0064 5.9915" style="paint-order:fill markers stroke"/>
+</svg>
diff --git a/src/pentobi/qml/themes/dark/piece-manipulator-desktop-legal.svg b/src/pentobi/qml/themes/dark/piece-manipulator-desktop-legal.svg
new file mode 100644 (file)
index 0000000..bed72ba
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-0.75976 25.988a74 74 0 0 1 71.84 73.969 74 74 0 0 1-148 0 74 74 0 0 1 76.16-73.969z" fill="#777" fill-opacity=".59"/>
+ <g id="b" transform="matrix(-.65 0 0 .65 113 -52)">
+  <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+  <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="#666"/>
+ </g>
+ <g id="a" transform="matrix(0 .65 -.65 0 304 35)">
+  <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+  <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#666"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,113,87)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="rotate(90 100 100)" width="100%" height="100%" xlink:href="#a"/>
+ <path d="m174 100a74 74 0 0 1-73.974 74 74 74 0 0 1-74.026-73.947 74 74 0 0 1 73.921-74.053 74 74 0 0 1 74.079 73.894" fill="#bbb" fill-opacity=".75"/>
+</svg>
diff --git a/src/pentobi/qml/themes/dark/piece-manipulator-desktop.svg b/src/pentobi/qml/themes/dark/piece-manipulator-desktop.svg
new file mode 100644 (file)
index 0000000..9b7e6fa
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-0.75976 25.988a74 74 0 0 1 71.84 73.969 74 74 0 0 1-148 0 74 74 0 0 1 76.16-73.969z" fill="#777" fill-opacity=".75"/>
+ <g id="b" transform="matrix(-.65 0 0 .65 113 -52)">
+  <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+  <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="#666"/>
+ </g>
+ <g id="a" transform="matrix(0 .65 -.65 0 304 35)">
+  <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+  <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#666"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,113,87)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="rotate(90 100 100)" width="100%" height="100%" xlink:href="#a"/>
+ <path d="m174 100a74 74 0 0 1-73.974 74 74 74 0 0 1-74.026-73.947 74 74 0 0 1 73.921-74.053 74 74 0 0 1 74.079 73.894" fill="#555" fill-opacity=".75"/>
+</svg>
diff --git a/src/pentobi/qml/themes/dark/piece-manipulator-legal.svg b/src/pentobi/qml/themes/dark/piece-manipulator-legal.svg
new file mode 100644 (file)
index 0000000..c72b7c3
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-1.0508 35.984a64 64 0 0 1 62.131 63.973 64 64 0 0 1-128 0 64 64 0 0 1 65.869-63.973z" fill="#777" fill-opacity=".59"/>
+ <g id="c" transform="matrix(.9 0 0 .9 0 10)">
+  <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+  <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="#666"/>
+ </g>
+ <g id="d" transform="matrix(.9 0 0 .9 10 20)">
+  <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+  <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#666"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,118,-82)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="matrix(0,1,-1,0,364,0)" width="100%" height="100%" xlink:href="#d"/>
+ <path d="m164 100a64 64 0 0 1-63.977 64 64 64 0 0 1-64.023-63.954 64 64 0 0 1 63.931-64.046 64 64 0 0 1 64.069 63.908" fill="#bbb" fill-opacity=".75"/>
+</svg>
diff --git a/src/pentobi/qml/themes/dark/piece-manipulator.svg b/src/pentobi/qml/themes/dark/piece-manipulator.svg
new file mode 100644 (file)
index 0000000..d142c9b
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-1.0508 35.984a64 64 0 0 1 62.131 63.973 64 64 0 0 1-128 0 64 64 0 0 1 65.869-63.973z" fill="#777" fill-opacity=".75"/>
+ <g id="c" transform="matrix(.9 0 0 .9 0 10)">
+  <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+  <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="#666"/>
+ </g>
+ <g id="d" transform="matrix(.9 0 0 .9 10 20)">
+  <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+  <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#666"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,118,-82)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="matrix(0,1,-1,0,364,0)" width="100%" height="100%" xlink:href="#d"/>
+ <path d="m164 100a64 64 0 0 1-63.977 64 64 64 0 0 1-64.023-63.954 64 64 0 0 1 63.931-64.046 64 64 0 0 1 64.069 63.908" fill="#555" fill-opacity=".75"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/Theme.qml b/src/pentobi/qml/themes/light/Theme.qml
new file mode 100644 (file)
index 0000000..07d61e7
--- /dev/null
@@ -0,0 +1,44 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/themes/light/Theme.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+
+/** Theme using light colors. */
+Item {
+    /** @name Colors for board and piece elements.
+        Each color has several versions to paint raised or sunken borders. The
+        first color is the base color, the second a darker version, the third
+        a lighter version. The board has a second set of three colors for
+        painting the center section in Callisto, the pieces have a fourth color
+        for painting markup. */
+    /// @{
+    property var colorBlue: [ "#0073cf", "#004881", "#1499ff", "#ffffff" ]
+    property var colorBoard: [ "#aea7ac", "#868084", "#c7bfc5",
+                               "#918b8f", "#7c777b", "#a09a9f"]
+    property var colorGreen: [ "#00c000", "#007800", "#00fa00", "#333333" ]
+    property var colorOrange: [ "#f09217", "#9d5e0b", "#ffbb67", "#333333" ]
+    property var colorPurple: [ "#a12ccf", "#6d2787", "#be70dc", "#ffffff" ]
+    property var colorRed: [ "#e63e2c", "#90261b", "#ff655a", "#ffffff" ]
+    property var colorYellow: [ "#f5c320", "#aa8516", "#ffdb58", "#333333" ]
+    /// @}
+
+    property color colorBackground: "#e8e8e8"
+    property color colorButtonPressed: Qt.darker(colorBackground, 1.1)
+    property color colorButtonHovered: Qt.lighter(colorBackground, 3)
+    property color colorButtonBorder: Qt.darker(colorBackground, 2)
+    property color colorCommentBase: "#ffffff"
+    property color colorCommentBorder: "#b4b3b3"
+    property color colorCommentFocus: "#4799cc"
+    property color colorCommentText: colorText
+    property color colorMessageText: "black"
+    property color colorMessageBase: "#cac9c9"
+    property color colorSelectedText: colorBackground
+    property color colorSelection: "#4799cc"
+    property color colorStartingPoint: "#767074"
+    property color colorText: "#111111"
+
+    function getImage(name) { return "themes/light/" + name + ".svg" }
+}
diff --git a/src/pentobi/qml/themes/light/menu.svg b/src/pentobi/qml/themes/light/menu.svg
new file mode 100644 (file)
index 0000000..93afa1b
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path id="a" d="M 10,2 A 2,2 0 0 1 8.0007,4 2,2 0 0 1 6,2.0014 2,2 0 0 1 7.9979,0 2,2 0 0 1 10,1.9971" style="paint-order:fill markers stroke"/>
+ <use transform="translate(0,6.0029)" width="100%" height="100%" xlink:href="#a"/>
+ <use transform="translate(0 12.003)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-backward.svg b/src/pentobi/qml/themes/light/pentobi-backward.svg
new file mode 100644 (file)
index 0000000..900de05
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m10 1v4h6v6h-6v4l-10-6.9965z" stroke-width=".94336"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-backward10.svg b/src/pentobi/qml/themes/light/pentobi-backward10.svg
new file mode 100644 (file)
index 0000000..baaa638
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path id="a" d="m10 1v14l-10-6.9965z" stroke-width=".94336"/>
+ <use transform="translate(6)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-beginning.svg b/src/pentobi/qml/themes/light/pentobi-beginning.svg
new file mode 100644 (file)
index 0000000..e2cabdc
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <rect x="2" y="2" width="3" height="12" stroke-width=".96077" style="paint-order:fill markers stroke"/>
+ <path id="a" d="m14 1v14l-10-6.9965z" stroke-width=".94336"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-computer-colors.svg b/src/pentobi/qml/themes/light/pentobi-computer-colors.svg
new file mode 100644 (file)
index 0000000..8d1042f
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect id="a" x="2" y="2" width="6" height="6" style="paint-order:fill markers stroke"/>
+ <rect x=".50013" y=".50013" width="15" height="15" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.0003" style="paint-order:fill markers stroke"/>
+ <use transform="translate(6,6)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-end.svg b/src/pentobi/qml/themes/light/pentobi-end.svg
new file mode 100644 (file)
index 0000000..29c61f4
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <rect transform="scale(-1,1)" x="-14" y="2" width="3" height="12" stroke-width=".96077" style="paint-order:fill markers stroke"/>
+ <path id="a" d="m2 1v14l10-6.9965z" stroke-width=".94336"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-forward.svg b/src/pentobi/qml/themes/light/pentobi-forward.svg
new file mode 100644 (file)
index 0000000..9f319cd
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m6 1v4h-6v6h6v4l10-6.9965z" stroke-width=".94336"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-forward10.svg b/src/pentobi/qml/themes/light/pentobi-forward10.svg
new file mode 100644 (file)
index 0000000..7088c92
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path id="a" d="m6 1v14l10-6.9965z" stroke-width=".94336"/>
+ <use transform="translate(-6)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-newgame.svg b/src/pentobi/qml/themes/light/pentobi-newgame.svg
new file mode 100644 (file)
index 0000000..e18e8c9
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m12.523 12.908-4.6934-4.5523 0.21582 6.3642-0.66919-6.5042-3.9255 5.014 3.6682-5.4126-6.23 1.3177 6.2892-1.7885-5.6194-2.9952 5.9674 2.6726-2.3795-5.9065 2.8534 5.883 1.9739-6.0542-1.5957 6.3408 5.4036-3.369-5.2982 3.8316 6.3049 0.89259-6.5216-0.47042z" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.7305" style="paint-order:fill markers stroke"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-next-variation.svg b/src/pentobi/qml/themes/light/pentobi-next-variation.svg
new file mode 100644 (file)
index 0000000..a1b4279
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m15 6h-4v-5h-6v5h-4l6.9965 10z" stroke-width=".94336"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-play.svg b/src/pentobi/qml/themes/light/pentobi-play.svg
new file mode 100644 (file)
index 0000000..5ec4380
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m2.5 0.5v15l12.75-7.5z" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width="1px"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-previous-variation.svg b/src/pentobi/qml/themes/light/pentobi-previous-variation.svg
new file mode 100644 (file)
index 0000000..dd264ab
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m15 10h-4v5h-6v-5h-4l6.9965-10z" stroke-width=".94336"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-rated-game.svg b/src/pentobi/qml/themes/light/pentobi-rated-game.svg
new file mode 100644 (file)
index 0000000..1a126a0
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m15 8a7.0001 7.0001 0 0 1-6.9976 7.0001 7.0001 7.0001 0 0 1-7.0026-6.9951 7.0001 7.0001 0 0 1 6.9925-7.0051 7.0001 7.0001 0 0 1 7.0076 6.9901" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.9998" style="paint-order:fill markers stroke"/>
+ <path d="m12.5 8a4.5001 4.5001 0 0 1-4.4985 4.5001 4.5001 4.5001 0 0 1-4.5017-4.4969 4.5001 4.5001 0 0 1 4.4953-4.5033 4.5001 4.5001 0 0 1 4.5049 4.4937" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width=".99978" style="paint-order:fill markers stroke"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-stop.svg b/src/pentobi/qml/themes/light/pentobi-stop.svg
new file mode 100644 (file)
index 0000000..f4025c3
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m3 3 10 10" fill="none" stroke="#000" stroke-linecap="round" stroke-width="3.4904"/>
+ <path d="m13 3-10 10" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.461"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/pentobi-undo.svg b/src/pentobi/qml/themes/light/pentobi-undo.svg
new file mode 100644 (file)
index 0000000..faef7ae
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m6 12.544a5.2183 5.1494 0 0 1-3.6178-5.3032 5.2183 5.1494 0 0 1 4.401-4.6913 5.2183 5.1494 0 0 1 5.6368 3.151" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" style="paint-order:fill markers stroke"/>
+ <path d="m7.2557 7.9583-4.6751 7.6593 8.9512 0.08219z" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width=".60041px"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/piece-manipulator-desktop-legal.svg b/src/pentobi/qml/themes/light/piece-manipulator-desktop-legal.svg
new file mode 100644 (file)
index 0000000..f64faa0
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-0.75976 25.988a74 74 0 0 1 71.84 73.969 74 74 0 0 1-148 0 74 74 0 0 1 76.16-73.969z" fill="#666" fill-opacity=".59"/>
+ <g id="b" transform="matrix(-.65 0 0 .65 113 -52)">
+  <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+  <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="#686868"/>
+ </g>
+ <g id="a" transform="matrix(0 .65 -.65 0 304 35)">
+  <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+  <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#686868"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,113,87)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="rotate(90 100 100)" width="100%" height="100%" xlink:href="#a"/>
+ <path d="m174 100a74 74 0 0 1-73.974 74 74 74 0 0 1-74.026-73.947 74 74 0 0 1 73.921-74.053 74 74 0 0 1 74.079 73.894" fill="#fff" fill-opacity=".75"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/piece-manipulator-desktop.svg b/src/pentobi/qml/themes/light/piece-manipulator-desktop.svg
new file mode 100644 (file)
index 0000000..f471947
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-0.75976 25.988a74 74 0 0 1 71.84 73.969 74 74 0 0 1-148 0 74 74 0 0 1 76.16-73.969z" fill="#666" fill-opacity=".75"/>
+ <g id="b" transform="matrix(-.65 0 0 .65 113 -52)">
+  <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+  <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="#686868"/>
+ </g>
+ <g id="a" transform="matrix(0 .65 -.65 0 304 35)">
+  <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+  <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#686868"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,113,87)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="rotate(90 100 100)" width="100%" height="100%" xlink:href="#a"/>
+ <path d="m174 100a74 74 0 0 1-73.974 74 74 74 0 0 1-74.026-73.947 74 74 0 0 1 73.921-74.053 74 74 0 0 1 74.079 73.894" fill="#aaa" fill-opacity=".75"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/piece-manipulator-legal.svg b/src/pentobi/qml/themes/light/piece-manipulator-legal.svg
new file mode 100644 (file)
index 0000000..efcdc77
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-1.0508 35.984a64 64 0 0 1 62.131 63.973 64 64 0 0 1-128 0 64 64 0 0 1 65.869-63.973z" fill="#666" fill-opacity=".59"/>
+ <g id="c" transform="matrix(.9 0 0 .9 0 10)">
+  <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+  <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="#686868"/>
+ </g>
+ <g id="d" transform="matrix(.9 0 0 .9 10 20)">
+  <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+  <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#686868"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,118,-82)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="matrix(0,1,-1,0,364,0)" width="100%" height="100%" xlink:href="#d"/>
+ <path d="m164 100a64 64 0 0 1-63.977 64 64 64 0 0 1-64.023-63.954 64 64 0 0 1 63.931-64.046 64 64 0 0 1 64.069 63.908" fill="#fff" fill-opacity=".75"/>
+</svg>
diff --git a/src/pentobi/qml/themes/light/piece-manipulator.svg b/src/pentobi/qml/themes/light/piece-manipulator.svg
new file mode 100644 (file)
index 0000000..dd42b88
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-1.0508 35.984a64 64 0 0 1 62.131 63.973 64 64 0 0 1-128 0 64 64 0 0 1 65.869-63.973z" fill="#666" fill-opacity=".75"/>
+ <g id="c" transform="matrix(.9 0 0 .9 0 10)">
+  <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+  <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="#686868"/>
+ </g>
+ <g id="d" transform="matrix(.9 0 0 .9 10 20)">
+  <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+  <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#686868"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,118,-82)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="matrix(0,1,-1,0,364,0)" width="100%" height="100%" xlink:href="#d"/>
+ <path d="m164 100a64 64 0 0 1-63.977 64 64 64 0 0 1-64.023-63.954 64 64 0 0 1 63.931-64.046 64 64 0 0 1 64.069 63.908" fill="#aaa" fill-opacity=".75"/>
+</svg>
diff --git a/src/pentobi/qml/themes/system/Theme.qml b/src/pentobi/qml/themes/system/Theme.qml
new file mode 100644 (file)
index 0000000..280ed68
--- /dev/null
@@ -0,0 +1,61 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/themes/system/Theme.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import "../dark" as Dark
+import "../light" as Light
+
+// See themes/light/Theme.qml for comments
+Item {
+    property color colorBackground: {
+        // If the contrast to yellow is too bad, we use a slightly modified
+        // system background color
+        var c = _contrast(palette.window, colorYellow)
+        if (c > 0 && c < 0.12)
+            return Qt.lighter(palette.window, 1.1)
+        if (c < 0 && c > -0.12)
+            return Qt.darker(palette.window, 1.5)
+        return palette.window
+    }
+
+    property var colorBlue: _isDark ? dark.colorBlue : light.colorBlue
+    property var colorBoard: _isDark ? dark.colorBoard : light.colorBoard
+    property var colorGreen: _isDark ? dark.colorGreen : light.colorGreen
+    property var colorOrange: _isDark ? dark.colorOrange : light.colorOrange
+    property var colorPurple: _isDark ? dark.colorPurple : light.colorPurple
+    property var colorRed: _isDark ? dark.colorRed : light.colorRed
+    property var colorYellow: _isDark ? dark.colorYellow : light.colorYellow
+
+    property color colorButtonPressed: palette.mid
+    property color colorButtonHovered: palette.window
+    property color colorButtonBorder: palette.dark
+    property color colorCommentBase: palette.base
+    property color colorCommentBorder: palette.mid
+    property color colorCommentFocus: palette.highlight
+    property color colorCommentText: colorText
+    property color colorMessageText: colorText
+    property color colorMessageBase: palette.base
+    property color colorSelectedText: palette.highlightedText
+    property color colorSelection: palette.highlight
+    property color colorStartingPoint:
+        _isDark ? dark.colorStartingPoint : light.colorStartingPoint
+    property color colorText: palette.text
+
+    property bool _isDark: palette.window.hslLightness < 0.5
+
+    function getImage(name) {
+        return _isDark ? dark.getImage(name) : light.getImage(name)
+    }
+
+    function _contrast(color1, color2) {
+        return 0.30 * (color1.r - color2.r) + 0.59 * (color1.g - color2.g)
+                + 0.11 * (color1.b - color2.b)
+    }
+
+    SystemPalette { id: palette }
+    Dark.Theme { id: dark }
+    Light.Theme { id: light }
+}
diff --git a/src/pentobi/qml/themes/themes.qrc b/src/pentobi/qml/themes/themes.qrc
new file mode 100644 (file)
index 0000000..d8056b5
--- /dev/null
@@ -0,0 +1,33 @@
+<RCC>
+<qresource prefix="/qml/themes">
+<file>colorblind-dark/Theme.qml</file>
+<file>colorblind-light/Theme.qml</file>
+<file>dark/pentobi-rated-game.svg</file>
+<file>dark/piece-manipulator-desktop.svg</file>
+<file>dark/piece-manipulator-desktop-legal.svg</file>
+<file>dark/piece-manipulator.svg</file>
+<file>dark/piece-manipulator-legal.svg</file>
+<file>dark/Theme.qml</file>
+<file>light/menu.svg</file>
+<file>light/pentobi-backward.svg</file>
+<file>light/pentobi-backward10.svg</file>
+<file>light/pentobi-beginning.svg</file>
+<file>light/pentobi-computer-colors.svg</file>
+<file>light/pentobi-end.svg</file>
+<file>light/pentobi-forward.svg</file>
+<file>light/pentobi-forward10.svg</file>
+<file>light/pentobi-newgame.svg</file>
+<file>light/pentobi-next-variation.svg</file>
+<file>light/pentobi-play.svg</file>
+<file>light/pentobi-previous-variation.svg</file>
+<file>light/pentobi-rated-game.svg</file>
+<file>light/pentobi-stop.svg</file>
+<file>light/pentobi-undo.svg</file>
+<file>light/piece-manipulator-desktop.svg</file>
+<file>light/piece-manipulator-desktop-legal.svg</file>
+<file>light/piece-manipulator.svg</file>
+<file>light/piece-manipulator-legal.svg</file>
+<file>light/Theme.qml</file>
+<file>system/Theme.qml</file>
+</qresource>
+</RCC>
diff --git a/src/pentobi/resources.qrc b/src/pentobi/resources.qrc
new file mode 100644 (file)
index 0000000..e6d284c
--- /dev/null
@@ -0,0 +1,81 @@
+<RCC>
+    <qresource prefix="/">
+        <file>qml/icons/filedialog-folder.svg</file>
+        <file>qml/icons/filedialog-newfolder.svg</file>
+        <file>qml/icons/filedialog-parent.svg</file>
+        <file>qml/AboutDialog.qml</file>
+        <file>qml/AnalyzeGame.qml</file>
+        <file>qml/AppearanceDialog.qml</file>
+        <file>qml/AsciiArtSaveDialog.qml</file>
+        <file>qml/Board.qml</file>
+        <file>qml/BoardContextMenu.qml</file>
+        <file>qml/BusyIndicator.qml</file>
+        <file>qml/Button.qml</file>
+        <file>qml/ButtonApply.qml</file>
+        <file>qml/ButtonCancel.qml</file>
+        <file>qml/ButtonClose.qml</file>
+        <file>qml/ButtonOk.qml</file>
+        <file>qml/ButtonToolTip.qml</file>
+        <file>qml/ComputerDialog.qml</file>
+        <file>qml/Comment.qml</file>
+        <file>qml/Controls.js</file>
+        <file>qml/Dialog.qml</file>
+        <file>qml/DialogButtonBox.qml</file>
+        <file>qml/DialogButtonBoxOkCancel.qml</file>
+        <file>qml/DialogLoader.qml</file>
+        <file>qml/ExportImageDialog.qml</file>
+        <file>qml/FatalMessage.qml</file>
+        <file>qml/FileDialog.qml</file>
+        <file>qml/GameDisplayMobile.qml</file>
+        <file>qml/GameInfoDialog.qml</file>
+        <file>qml/GameVariantDialog.qml</file>
+        <file>qml/GotoMoveDialog.qml</file>
+        <file>qml/HelpWindow.qml</file>
+        <file>qml/ImageSaveDialog.qml</file>
+        <file>qml/InitialRatingDialog.qml</file>
+        <file>qml/LineSegment.qml</file>
+        <file>qml/Main.qml</file>
+        <file>qml/MessageDialog.qml</file>
+        <file>qml/Menu.qml</file>
+        <file>qml/MenuComputer.qml</file>
+        <file>qml/MenuEdit.qml</file>
+        <file>qml/MenuExport.qml</file>
+        <file>qml/MenuGame.qml</file>
+        <file>qml/MenuGo.qml</file>
+        <file>qml/MenuHelp.qml</file>
+        <file>qml/MenuItem.qml</file>
+        <file>qml/MenuRecentFiles.qml</file>
+        <file>qml/MenuSeparator.qml</file>
+        <file>qml/MenuTools.qml</file>
+        <file>qml/MenuView.qml</file>
+        <file>qml/MoveAnnotationDialog.qml</file>
+        <file>qml/NavigationButtons.qml</file>
+        <file>qml/NavigationPanel.qml</file>
+        <file>qml/NewFolderDialog.qml</file>
+        <file>qml/OpenDialog.qml</file>
+        <file>qml/PieceCallisto.qml</file>
+        <file>qml/PieceClassic.qml</file>
+        <file>qml/PieceFlipAnimation.qml</file>
+        <file>qml/PieceGembloQ.qml</file>
+        <file>qml/PieceList.qml</file>
+        <file>qml/PieceManipulator.qml</file>
+        <file>qml/PieceNexos.qml</file>
+        <file>qml/PieceRotationAnimation.qml</file>
+        <file>qml/PieceSelectorMobile.qml</file>
+        <file>qml/PieceSwitchedFlipAnimation.qml</file>
+        <file>qml/PieceTrigon.qml</file>
+        <file>qml/QuarterSquare.qml</file>
+        <file>qml/QuestionDialog.qml</file>
+        <file>qml/RatingDialog.qml</file>
+        <file>qml/RatingGraph.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/resources_desktop.qrc b/src/pentobi/resources_desktop.qrc
new file mode 100644 (file)
index 0000000..4aef039
--- /dev/null
@@ -0,0 +1,7 @@
+<RCC>
+    <qresource prefix="/">
+        <file>qml/AnalyzeDialog.qml</file>
+        <file>qml/GameDisplayDesktop.qml</file>
+        <file>qml/PieceSelectorDesktop.qml</file>
+    </qresource>
+</RCC>
diff --git a/src/pentobi_gtp/CMakeLists.txt b/src/pentobi_gtp/CMakeLists.txt
new file mode 100644 (file)
index 0000000..9589f0d
--- /dev/null
@@ -0,0 +1,10 @@
+add_executable(pentobi-gtp
+  Engine.h
+  Engine.cpp
+  Main.cpp
+)
+
+target_compile_definitions(pentobi-gtp PRIVATE VERSION="${PENTOBI_VERSION}")
+
+target_link_libraries(pentobi-gtp pentobi_mcts)
+
diff --git a/src/pentobi_gtp/Engine.cpp b/src/pentobi_gtp/Engine.cpp
new file mode 100644 (file)
index 0000000..d863d27
--- /dev/null
@@ -0,0 +1,207 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi_gtp/Engine.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Engine.h"
+
+#include <fstream>
+#include "libboardgame_sgf/Writer.h"
+#include "libpentobi_mcts/Util.h"
+
+namespace pentobi_gtp {
+
+using libboardgame_gtp::Failure;
+using libboardgame_sgf::Writer;
+using libpentobi_base::Board;
+using libpentobi_base::get_color_id;
+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("selfplay", &Engine::cmd_selfplay);
+    add("version", &Engine::cmd_version);
+}
+
+Engine::~Engine() = default; // Non-inline to avoid GCC -Winline warning
+
+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;
+    children.reserve(tree.get_root().get_nu_children());
+    for (auto& i : tree.get_root_children())
+        children.push_back(&i);
+    sort(children.begin(), children.end(), libpentobi_mcts::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(Arguments args)
+{
+    auto& search = get_search();
+    if (! search.get_last_history().is_valid())
+        throw Failure("no search tree");
+    ofstream out(args.get());
+    libpentobi_mcts::dump_tree(out, search);
+}
+
+/** Let the engine play a number of games against itself.
+    This is more efficient than using twogtp if selfplay games are needed
+    because it has lower memory requirements (only one engine needed), process
+    switches between the engines are avoided and parts of the search tree can
+    be reused between moves of different players. */
+void Engine::cmd_selfplay(Arguments args)
+{
+    args.check_size(2);
+    auto nu_games = args.parse<int>(0);
+    ofstream out(args.get(1));
+    auto variant = get_board().get_variant();
+    auto variant_str = to_string(variant);
+    Board bd(variant);
+    auto& player = get_mcts_player();
+    ostringstream s;
+    for (int i = 0; i < nu_games; ++i)
+    {
+        s.str("");
+        Writer writer(s);
+        writer.set_indent(-1);
+        bd.init();
+        writer.begin_tree();
+        writer.begin_node();
+        writer.write_property("GM", variant_str);
+        writer.end_node();
+        while (! bd.is_game_over())
+        {
+            auto c = bd.get_effective_to_play();
+            auto mv = player.genmove(bd, c);
+            bd.play(c, mv);
+            writer.begin_node();
+            writer.write_property(get_color_id(variant, c),
+                                  bd.to_string(mv, false));
+            writer.end_node();
+        }
+        writer.end_tree();
+        out << s.str() << '\n';
+    }
+}
+
+void Engine::cmd_param(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'
+            << "exploration_constant " << s.get_exploration_constant() << '\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 == "exploration_constant")
+            s.set_exploration_constant(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;
+#else
+    version = "unknown";
+#endif
+#ifdef 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 = make_unique<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..1e4c66c
--- /dev/null
@@ -0,0 +1,61 @@
+//-----------------------------------------------------------------------------
+/** @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:
+    explicit Engine(Variant variant, unsigned level = 5,
+                    bool use_book = true, const string& books_dir = "",
+                    unsigned nu_threads = 0);
+
+    ~Engine() override;
+
+    void cmd_param(Arguments args, Response& response);
+    void cmd_get_value(Response& response);
+    void cmd_move_values(Response& response);
+    void cmd_name(Response& response);
+    void cmd_selfplay(Arguments args);
+    void cmd_save_tree(Arguments args);
+    void cmd_version(Response& 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..3b937b9
--- /dev/null
@@ -0,0 +1,167 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi_gtp/Main.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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 == nullptr || argv[0] == nullptr)
+        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)
+{
+    libboardgame_util::LogInitializer log_initializer;
+    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 unknown version";
+#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"));
+        const 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..b07d1e4
--- /dev/null
@@ -0,0 +1,23 @@
+find_package(ECM REQUIRED NO_MODULE)
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH})
+
+include(KDEInstallDirs)
+include(KDECompilerSettings)
+include(KDECMakeSettings)
+
+find_package(KF5 REQUIRED COMPONENTS KIO)
+
+add_library(pentobi-thumbnail MODULE
+  PentobiThumbCreator.h
+  PentobiThumbCreator.cpp
+)
+
+target_include_directories(pentobi-thumbnail PRIVATE "${CMAKE_SOURCE_DIR}/src")
+
+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..f4f98de
--- /dev/null
@@ -0,0 +1,32 @@
+//-----------------------------------------------------------------------------
+/** @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; }
+
+}
+
+//-----------------------------------------------------------------------------
+
+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..a0eb268
--- /dev/null
@@ -0,0 +1,24 @@
+//-----------------------------------------------------------------------------
+/** @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 <kio/thumbcreator.h>
+
+//-----------------------------------------------------------------------------
+
+class PentobiThumbCreator
+    : public ThumbCreator
+{
+public:
+    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..10cf91f
--- /dev/null
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Type=Service
+Name=Blokus games
+ServiceTypes=ThumbCreator
+MimeType=application/x-blokus-sgf;
+X-KDE-Library=pentobi-thumbnail
+
+
+# Translations
+Name[de]=Blokus-Partien
+Name[fr]=Parties de Blokus
diff --git a/src/pentobi_thumbnailer/CMakeLists.txt b/src/pentobi_thumbnailer/CMakeLists.txt
new file mode 100644 (file)
index 0000000..099f3cd
--- /dev/null
@@ -0,0 +1,5 @@
+add_executable(pentobi-thumbnailer Main.cpp)
+
+target_link_libraries(pentobi-thumbnailer pentobi_thumbnail)
+
+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..47fff58
--- /dev/null
@@ -0,0 +1,65 @@
+//-----------------------------------------------------------------------------
+/** @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 "libboardgame_util/Log.h"
+#include "libpentobi_thumbnail/CreateThumbnail.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char* argv[])
+{
+    libboardgame_util::LogInitializer log_initializer;
+    QCoreApplication app(argc, argv);
+    try
+    {
+        QCommandLineParser parser;
+        QCommandLineOption optionSize(
+                    QStringList() << QStringLiteral("s")
+                    << QStringLiteral("size"),
+                    QStringLiteral(
+                        "Generate image with height and width <size>."),
+                    QStringLiteral("size"), QStringLiteral("128"));
+        parser.addOption(optionSize);
+        parser.addHelpOption();
+        parser.addPositionalArgument(QStringLiteral("input.blksgf"),
+                                     QStringLiteral("Blokus SGF input file"));
+        parser.addPositionalArgument(QStringLiteral("output.png"),
+                                     QStringLiteral("PNG image output file"));
+        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 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("Not a valid Blokus SGF file");
+        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..4b4af3d
--- /dev/null
@@ -0,0 +1,134 @@
+//-----------------------------------------------------------------------------
+/** @file twogtp/Analyze.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Analyze.h"
+
+#include <fstream>
+#include <map>
+#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)
+{
+    FmtSaver saver(cout);
+    ifstream in(file);
+    Statistics<> stat_result;
+    map<unsigned, Statistics<>> stat_result_player;
+    map<double, 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;
+        double result;
+        unsigned length;
+        unsigned player;
+        double cpu_b;
+        double 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';
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/twogtp/Analyze.h b/src/twogtp/Analyze.h
new file mode 100644 (file)
index 0000000..2c70069
--- /dev/null
@@ -0,0 +1,20 @@
+//-----------------------------------------------------------------------------
+/** @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);
+
+//-----------------------------------------------------------------------------
+
+#endif // TWOGTP_ANALYZE_H
diff --git a/src/twogtp/CMakeLists.txt b/src/twogtp/CMakeLists.txt
new file mode 100644 (file)
index 0000000..ee744c7
--- /dev/null
@@ -0,0 +1,23 @@
+find_package(Threads)
+
+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
+    Threads::Threads
+    )
+
diff --git a/src/twogtp/FdStream.cpp b/src/twogtp/FdStream.cpp
new file mode 100644 (file)
index 0000000..93928d0
--- /dev/null
@@ -0,0 +1,89 @@
+//-----------------------------------------------------------------------------
+/** @file twogtp/FdStream.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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.begin()) + m_buf.size();
+    setg(end, end, end);
+}
+
+FdInBuf::~FdInBuf() = default; // Non-inline to avoid GCC -Winline warning
+
+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; // Non-inline to avoid GCC -Winline warning
+
+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..631bf5b
--- /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:
+    explicit FdInBuf(int fd, size_t buf_size = 1024);
+
+    ~FdInBuf() override;
+
+protected:
+    int_type underflow() override;
+
+private:
+    int m_fd;
+
+    vector<char_type> m_buf;
+};
+
+//-----------------------------------------------------------------------------
+
+/** Input stream from a file descriptor. */
+class FdInStream final
+    : 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() override;
+
+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 final
+    : 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..d42675b
--- /dev/null
@@ -0,0 +1,166 @@
+//-----------------------------------------------------------------------------
+/** @file twogtp/GtpConnection.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "GtpConnection.h"
+
+#include <cstring>
+#include <vector>
+#include <unistd.h>
+#include "FdStream.h"
+#include "libboardgame_util/Log.h"
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+[[noreturn]] void terminate_child(const string& message)
+{
+    LIBBOARDGAME_LOG(message);
+    exit(1);
+}
+
+vector<string> split_args(const 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) != 0) && ! 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)
+{
+    auto args = split_args(command);
+    if (args.empty())
+        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");
+    if (pid > 0) // Parent
+    {
+        close(fd1[0]);
+        close(fd2[1]);
+        m_in  = make_unique<FdInStream>(fd2[0]);
+        m_out = make_unique<FdOutStream>(fd1[1]);
+        return;
+    }
+    // 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");
+        }
+    vector<char*> argv;
+    argv.reserve(args.size() + 1);
+    for (auto& a : args)
+        argv.push_back(const_cast<char*>(a.c_str()));
+    argv.push_back(nullptr);
+    execvp(args[0].c_str(), &(*argv.begin()));
+    terminate_child("Could not execute '" + command + "': " + strerror(errno));
+}
+
+GtpConnection::~GtpConnection() = default; // Non-inline to avoid GCC -Winline warning
+
+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..a9bcf9b
--- /dev/null
@@ -0,0 +1,101 @@
+//-----------------------------------------------------------------------------
+/** @file twogtp/Main.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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::Options;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char** argv)
+{
+    libboardgame_util::LogInitializer log_initializer;
+    atomic<int> result(0);
+    try
+    {
+        vector<string> specs = {
+            "analyze:",
+            "black|b:",
+            "fastopen",
+            "file|f:",
+            "game|g:",
+            "nugames|n:",
+            "quiet",
+            "saveinterval:",
+            "threads:",
+            "tree",
+            "white|w:",
+        };
+        Options opt(argc, argv, specs);
+        if (opt.contains("analyze"))
+        {
+            analyze(opt.get("analyze"));
+            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");
+        auto save_interval = opt.get<double>("saveinterval", 60);
+        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>> twogtps;
+        twogtps.reserve(nu_threads);
+        for (unsigned i = 0; i < nu_threads; ++i)
+        {
+            string log_prefix;
+            if (nu_threads > 1)
+                log_prefix = to_string(i + 1);
+            auto twogtp = make_shared<TwoGtp>(black, white, variant,
+                                              nu_games, output, quiet,
+                                              log_prefix, fast_open);
+            twogtp->set_save_interval(save_interval);
+            twogtps.push_back(twogtp);
+        }
+        vector<thread> threads;
+        threads.reserve(nu_threads);
+        for (auto& i : twogtps)
+            threads.emplace_back([&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..66f95b7
--- /dev/null
@@ -0,0 +1,139 @@
+//-----------------------------------------------------------------------------
+/** @file twogtp/Output.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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");
+    m_timer.reset(m_time_source);
+    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()
+{
+    save();
+    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_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()));
+        m_sgf_buffer << sgf;
+        if (m_create_tree)
+            m_output_tree.add_game(bd, player_black, result, is_real_move);
+    }
+    if (m_timer() > m_save_interval)
+    {
+        save();
+        m_timer.reset();
+    }
+}
+
+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;
+}
+
+void Output::save()
+{
+    lock_guard<mutex> lock(m_mutex);
+    {
+        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 << m_sgf_buffer.str();
+        m_sgf_buffer.str("");
+    }
+    if (m_create_tree)
+        m_output_tree.save(m_prefix + "-tree.blksgf");
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/twogtp/Output.h b/src/twogtp/Output.h
new file mode 100644 (file)
index 0000000..c5fe7c6
--- /dev/null
@@ -0,0 +1,72 @@
+//-----------------------------------------------------------------------------
+/** @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"
+#include "libboardgame_util/Timer.h"
+#include "libboardgame_util/WallTimeSource.h"
+
+using libboardgame_util::Timer;
+using libboardgame_util::WallTimeSource;
+
+//-----------------------------------------------------------------------------
+
+/** Handles the output files of TwoGtp and their concurrent access. */
+class Output
+{
+public:
+    Output(Variant variant, const string& prefix, bool create_tree);
+
+    ~Output();
+
+    void set_save_interval(double seconds) { m_save_interval = seconds; }
+
+    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_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;
+
+    ostringstream m_sgf_buffer;
+
+    WallTimeSource m_time_source;
+
+    Timer m_timer;
+
+    double m_save_interval = 60;
+
+    void save();
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // TWOGTP_OUTPUT_H
diff --git a/src/twogtp/OutputTree.cpp b/src/twogtp/OutputTree.cpp
new file mode 100644 (file)
index 0000000..b20dd8d
--- /dev/null
@@ -0,0 +1,237 @@
+//-----------------------------------------------------------------------------
+/** @file twogtp/OutputTree.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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 libboardgame_util::ArrayList;
+using libpentobi_base::get_transforms;
+using libpentobi_base::ColorMove;
+using libpentobi_base::MovePoints;
+using libpentobi_base::get_transformed;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void add(PentobiTree& tree, const SgfNode& node, bool is_player_black,
+         bool is_real_move, double 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_moves>& s1,
+                      ArrayList<ColorMove, Board::max_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;
+        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() = default; // Non-inline to avoid GCC -Winline warning
+
+void OutputTree::add_game(const Board& bd, unsigned player_black,
+                          double result, const array<bool,
+                          Board::max_moves>& is_real_move)
+{
+    if (bd.has_setup())
+        throw runtime_error("OutputTree: setup not supported");
+
+    // Find the canonical representation
+    ArrayList<ColorMove, Board::max_moves> sequence;
+    for (auto& transform : m_transforms)
+    {
+        ArrayList<ColorMove, Board::max_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 == nullptr)
+        {
+            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 == nullptr)
+            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;
+    }
+    auto 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..c2736c4
--- /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 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 probability,
+    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, double result,
+                  const array<bool, Board::max_moves>& is_real_move);
+
+private:
+    using PointTransform = libboardgame_base::PointTransform<Point>;
+
+
+    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..f8cc7b7
--- /dev/null
@@ -0,0 +1,190 @@
+//-----------------------------------------------------------------------------
+/** @file twogtp/TwoGtp.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "TwoGtp.h"
+
+#include "libboardgame_sgf/Writer.h"
+#include "libboardgame_util/Log.h"
+#include "libpentobi_base/ScoreUtil.h"
+
+using libboardgame_sgf::Writer;
+using libpentobi_base::get_multiplayer_result;
+using libpentobi_base::Move;
+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;
+        get_multiplayer_result(nu_players, points, player_result,
+                               m_bd.get_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(-1);
+    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_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;
+            }
+            if (! m_bd.from_string(mv, response))
+                throw runtime_error("invalid move");
+        }
+        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();
+    sgf_string << '\n';
+    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..a233633
--- /dev/null
@@ -0,0 +1,63 @@
+//-----------------------------------------------------------------------------
+/** @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();
+
+    void set_save_interval(double seconds) { m_output.set_save_interval(seconds); }
+
+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..6514b1c
--- /dev/null
@@ -0,0 +1,14 @@
+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
+    )
+
+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..e580461
--- /dev/null
@@ -0,0 +1,57 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_base/MarkerTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/Marker.h"
+#include "libboardgame_base/Point.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+using Point = libboardgame_base::Point<19 * 19, 19, 19, unsigned short>;
+using Marker = libboardgame_base::Marker<Point>;
+
+//-----------------------------------------------------------------------------
+
+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..3ff2497
--- /dev/null
@@ -0,0 +1,42 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_base/PointTransformTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/Point.h"
+#include "libboardgame_base/PointTransform.h"
+#include "libboardgame_base/RectGeometry.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+using Point = libboardgame_base::Point<19 * 19, 19, 19, unsigned short>;
+using RectGeometry = libboardgame_base::RectGeometry<Point>;
+
+//-----------------------------------------------------------------------------
+
+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..54bad28
--- /dev/null
@@ -0,0 +1,66 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_base/RatingTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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, 1);
+    LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2585, 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, 1);
+    LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2575, 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, 1);
+    LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2580, 1);
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/unittest/libboardgame_base/RectGeometryTest.cpp b/src/unittest/libboardgame_base/RectGeometryTest.cpp
new file mode 100644 (file)
index 0000000..5a12200
--- /dev/null
@@ -0,0 +1,97 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_base/RectGeometryTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/Point.h"
+#include "libboardgame_base/RectGeometry.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+using Point = libboardgame_base::Point<19 * 19, 19, 19, unsigned short>;
+using Geometry = libboardgame_base::Geometry<Point>;
+using RectGeometry = libboardgame_base::RectGeometry<Point>;
+using PointList = libboardgame_util::ArrayList<Point, Point::range_onboard>;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+bool from_string(const string& s, const Geometry& geo, Point& p)
+{
+    return geo.from_string(s.begin(), s.end(), p);
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+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(from_string("a1", geo, p));
+    LIBBOARDGAME_CHECK(p == geo.get_point(0, 18));
+
+    LIBBOARDGAME_CHECK(from_string("a19", geo, p));
+    LIBBOARDGAME_CHECK(p == geo.get_point(0, 0));
+
+    LIBBOARDGAME_CHECK(from_string("A1", geo, p));
+    LIBBOARDGAME_CHECK(p == geo.get_point(0, 18));
+
+    LIBBOARDGAME_CHECK(! from_string("foobar", geo, p));
+    LIBBOARDGAME_CHECK(! from_string("a123", geo, p));
+    LIBBOARDGAME_CHECK(! from_string("a56", geo, p));
+    LIBBOARDGAME_CHECK(! from_string("aa1", geo, p));
+    LIBBOARDGAME_CHECK(! from_string("c3#", geo, 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..5d43e0f
--- /dev/null
@@ -0,0 +1,80 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_base/StringRepTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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)
+{
+    return string_rep.read(s.begin(), s.end(), 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..641227c
--- /dev/null
@@ -0,0 +1,153 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_gtp/ArgumentsTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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(R"(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-4f);
+}
+
+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(R"(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..2ccd7aa
--- /dev/null
@@ -0,0 +1,13 @@
+add_executable(unittest_libboardgame_gtp
+  ArgumentsTest.cpp
+  CmdLineTest.cpp
+  EngineTest.cpp
+  ResponseTest.cpp
+)
+
+target_link_libraries(unittest_libboardgame_gtp
+    boardgame_test_main
+    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..5a5e92f
--- /dev/null
@@ -0,0 +1,66 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_gtp/CmdLineTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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));
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+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..771cfe3
--- /dev/null
@@ -0,0 +1,118 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_gtp/EngineTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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 libboardgame_gtp::Engine::exec_main_loop). */
+class InvalidResponseEngine
+    : public Engine
+{
+public:
+    InvalidResponseEngine();
+
+    void invalid_response(Response& r);
+
+    void invalid_response_2(Response& r);
+};
+
+InvalidResponseEngine::InvalidResponseEngine()
+{
+    add("invalid_response", &InvalidResponseEngine::invalid_response);
+    add("invalid_response_2", &InvalidResponseEngine::invalid_response_2);
+}
+
+void InvalidResponseEngine::invalid_response(Response& r)
+{
+    r << "This response is invalid\n"
+      << "\n"
+      << "because it contains an empty line";
+}
+
+void InvalidResponseEngine::invalid_response_2(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("known_command known_command\n");
+    ostringstream out;
+    Engine engine;
+    engine.exec_main_loop(in, out);
+    LIBBOARDGAME_CHECK_EQUAL(string("= true\n\n"), out.str());
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_engine_command_with_id)
+{
+    istringstream in("10 known_command known_command\n");
+    ostringstream out;
+    Engine engine;
+    engine.exec_main_loop(in, out);
+    LIBBOARDGAME_CHECK_EQUAL(string("=10 true\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..59d37ab
--- /dev/null
@@ -0,0 +1,24 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_gtp/ResponseTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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..9ac9b61
--- /dev/null
@@ -0,0 +1,10 @@
+add_executable(unittest_libboardgame_mcts
+  NodeTest.cpp
+)
+
+target_link_libraries(unittest_libboardgame_mcts
+    boardgame_test_main
+    boardgame_mcts
+    )
+
+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..e994f8c
--- /dev/null
@@ -0,0 +1,37 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_mcts/NodeTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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, 1);
+    node.add_value(5);
+    LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 5.f, 1e-4f);
+    node.add_value(2);
+    LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 3.5f, 1e-4f);
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_mcts_node_add_value_remove_loss)
+{
+    libboardgame_mcts::Node<int, float, true> node;
+    node.init(0, 0.5, 0, 1);
+    node.add_value(5);
+    LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 5.f, 1e-4f);
+    node.add_value(0);
+    LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 2.5f, 1e-4f);
+    node.add_value_remove_loss(2);
+    LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 3.5f, 1e-4f);
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/unittest/libboardgame_sgf/CMakeLists.txt b/src/unittest/libboardgame_sgf/CMakeLists.txt
new file mode 100644 (file)
index 0000000..7f0ce5e
--- /dev/null
@@ -0,0 +1,13 @@
+add_executable(unittest_libboardgame_sgf
+  SgfNodeTest.cpp
+  SgfTreeTest.cpp
+  SgfUtilTest.cpp
+  TreeReaderTest.cpp
+)
+
+target_link_libraries(unittest_libboardgame_sgf
+    boardgame_test_main
+    boardgame_sgf
+    )
+
+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..e9cb37b
--- /dev/null
@@ -0,0 +1,37 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_sgf/SgfNodeTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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)
+{
+    auto parent = make_unique<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";
+    auto node = make_unique<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/SgfTreeTest.cpp b/src/unittest/libboardgame_sgf/SgfTreeTest.cpp
new file mode 100644 (file)
index 0000000..5ce447c
--- /dev/null
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_sgf/SgfTreeTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_sgf/SgfTree.h"
+#include "libboardgame_test/Test.h"
+
+using namespace libboardgame_sgf;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(sgf_tree_delete_all_variations)
+{
+    // root - node1 - node2 - node3
+    //              \ node4
+    SgfTree tree;
+    auto& root = tree.get_root();
+    auto& node1 = tree.create_new_child(root);
+    auto& node2 = tree.create_new_child(node1);
+    auto& node3 = tree.create_new_child(node2);
+    auto& node4 = tree.create_new_child(node1);
+    LIBBOARDGAME_CHECK_EQUAL(root.get_nu_children(), 1u);
+    LIBBOARDGAME_CHECK_EQUAL(node1.get_nu_children(), 2u);
+    LIBBOARDGAME_CHECK_EQUAL(node2.get_nu_children(), 1u);
+    LIBBOARDGAME_CHECK_EQUAL(node3.get_nu_children(), 0u);
+    LIBBOARDGAME_CHECK_EQUAL(node4.get_nu_children(), 0u);
+    tree.clear_modified();
+    LIBBOARDGAME_CHECK(! tree.is_modified());
+    tree.delete_all_variations();
+    LIBBOARDGAME_CHECK(tree.is_modified());
+    LIBBOARDGAME_CHECK_EQUAL(root.get_nu_children(), 1u);
+    LIBBOARDGAME_CHECK_EQUAL(node1.get_nu_children(), 1u);
+    LIBBOARDGAME_CHECK_EQUAL(node2.get_nu_children(), 1u);
+    LIBBOARDGAME_CHECK_EQUAL(node3.get_nu_children(), 0u);
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/unittest/libboardgame_sgf/SgfUtilTest.cpp b/src/unittest/libboardgame_sgf/SgfUtilTest.cpp
new file mode 100644 (file)
index 0000000..e40815d
--- /dev/null
@@ -0,0 +1,27 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_sgf/SgfUtilTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_sgf/SgfUtil.h"
+
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_sgf;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(sgf_util_get_path_from_root)
+{
+    auto root = make_unique<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..3569a85
--- /dev/null
@@ -0,0 +1,133 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_sgf/TreeReaderTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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");
+}
+
+LIBBOARDGAME_TEST_CASE(sgf_tree_reader_multiprop_with_whitespace)
+{
+    istringstream in("(;A [1]\n[2] [3]\t[4]\r\n[5])");
+    TreeReader reader;
+    reader.read(in);
+    auto& root = reader.get_tree();
+    auto values = root.get_multi_property("A");
+    LIBBOARDGAME_CHECK_EQUAL(values.size(), 5u);
+    LIBBOARDGAME_CHECK_EQUAL(values[0], "1");
+    LIBBOARDGAME_CHECK_EQUAL(values[1], "2");
+    LIBBOARDGAME_CHECK_EQUAL(values[2], "3");
+    LIBBOARDGAME_CHECK_EQUAL(values[3], "4");
+    LIBBOARDGAME_CHECK_EQUAL(values[4], "5");
+}
+
+/** 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 = "ü";
+    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..fee8b80
--- /dev/null
@@ -0,0 +1,63 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_util/ArrayListTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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_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..fcff0db
--- /dev/null
@@ -0,0 +1,13 @@
+add_executable(unittest_libboardgame_util
+  ArrayListTest.cpp
+  OptionsTest.cpp
+  StatisticsTest.cpp
+  StringUtilTest.cpp
+)
+
+target_link_libraries(unittest_libboardgame_util
+    boardgame_test_main
+    boardgame_util
+    )
+
+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..339b07a
--- /dev/null
@@ -0,0 +1,87 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_util/OptionsTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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" };
+    auto 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" };
+    auto 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" };
+    auto 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" };
+    auto 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" };
+    auto 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" };
+    auto 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..32fcaeb
--- /dev/null
@@ -0,0 +1,29 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_util/StatisticsTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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..0ee52e9
--- /dev/null
@@ -0,0 +1,93 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_util/StringUtilTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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..07960f5
--- /dev/null
@@ -0,0 +1,50 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/BoardConstTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libpentobi_base/BoardConst.h"
+
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libpentobi_base;
+
+//-----------------------------------------------------------------------------
+
+/** Test that from_string() handles null moves.
+    Used for example in pentobi/AnalyzeGameMode.cpp */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_const_from_string_null)
+{
+    auto& bc = BoardConst::get(Variant::duo);
+    Move mv;
+    LIBBOARDGAME_CHECK(bc.from_string(mv, "null"));
+    LIBBOARDGAME_CHECK(mv.is_null());
+}
+
+/** 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_to_string_ordered)
+{
+    auto& bc = BoardConst::get(Variant::duo);
+    Move mv;
+    LIBBOARDGAME_CHECK(bc.from_string(mv, "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);
+    Move mv;
+    LIBBOARDGAME_CHECK(bc.from_string(mv, "q9,q10,r10,q11,r11,s11"));
+    auto& info_ext_2 = bc.get_move_info_ext_2(mv);
+    LIBBOARDGAME_CHECK(! info_ext_2.breaks_symmetry);
+    LIBBOARDGAME_CHECK(bc.from_string(mv, "q8,r8,s8,r9,s9,s10"));
+    LIBBOARDGAME_CHECK_EQUAL(info_ext_2.symmetric_move.to_int(), mv.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..8852d84
--- /dev/null
@@ -0,0 +1,165 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/BoardTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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)
+{
+    Move mv;
+    auto ok = bd.from_string(mv, s);
+    LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(ok);
+    LIBBOARDGAME_ASSERT(ok);
+    bd.play(c, mv);
+}
+
+} // 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]
+      )
+    */
+    auto bd = make_unique<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)
+{
+    auto bd = make_unique<Board>(Variant::classic);
+    auto moves = make_unique<MoveList>();
+    auto marker = make_unique<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)
+{
+    auto bd = make_unique<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..04b6286
--- /dev/null
@@ -0,0 +1,142 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/BoardUpdaterTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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::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);
+    auto bd = make_unique<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);
+    auto bd = make_unique<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);
+    auto bd = make_unique<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 of Color(0) 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);
+    auto bd = make_unique<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));
+}
+
+/** Test removing a piece of Color(1) with the AE property. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup_empty_1)
+{
+    istringstream in("(;GM[Blokus Duo]"
+                     " ;W[e8,e9,f9,d10,e10]"
+                     " ;B[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);
+    auto bd = make_unique<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(5));
+    LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(0));
+}
+
+/** Test removing a piece in a game variant with multiple instances per
+    piece. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup_empty_multi_instance)
+{
+    istringstream in("(;GM[Blokus Junior];B[e10];W[j5];B[f9];AE[f9][e10])");
+    TreeReader reader;
+    reader.read(in);
+    unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+    PentobiTree tree(root);
+    auto bd = make_unique<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(1));
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/unittest/libpentobi_base/CMakeLists.txt b/src/unittest/libpentobi_base/CMakeLists.txt
new file mode 100644 (file)
index 0000000..c384d4c
--- /dev/null
@@ -0,0 +1,15 @@
+add_executable(unittest_libpentobi_base
+  BoardConstTest.cpp
+  BoardTest.cpp
+  BoardUpdaterTest.cpp
+  GameTest.cpp
+  PentobiTreeTest.cpp
+  PentobiSgfUtilTest.cpp
+)
+
+target_link_libraries(unittest_libpentobi_base
+    boardgame_test_main
+    pentobi_base
+    )
+
+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..4bad9c7
--- /dev/null
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/GameTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libpentobi_base/Game.h"
+
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libpentobi_base;
+using libboardgame_sgf::TreeReader;
+
+//-----------------------------------------------------------------------------
+
+/** 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/PentobiSgfUtilTest.cpp b/src/unittest/libpentobi_base/PentobiSgfUtilTest.cpp
new file mode 100644 (file)
index 0000000..1ac8b04
--- /dev/null
@@ -0,0 +1,112 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/PentobiSgfUtilTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libpentobi_base/PentobiSgfUtil.h"
+
+#include <cstring>
+#include "libboardgame_test/Test.h"
+
+using namespace libpentobi_base;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(pentobi_base_get_color_id)
+{
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic, Color(0)), "1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic, Color(1)), "2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic, Color(2)), "3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic, Color(3)), "4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_2, Color(0)), "1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_2, Color(1)), "2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_2, Color(2)), "3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_2, Color(3)), "4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_3, Color(0)), "1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_3, Color(1)), "2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_3, Color(2)), "3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_3, Color(3)), "4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::duo, Color(0)), "B") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::duo, Color(1)), "W") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::junior, Color(0)), "B") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::junior, Color(1)), "W") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon, Color(0)), "1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon, Color(1)), "2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon, Color(2)), "3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon, Color(3)), "4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_2, Color(0)), "1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_2, Color(1)), "2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_2, Color(2)), "3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_2, Color(3)), "4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_3, Color(0)), "1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_3, Color(1)), "2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_3, Color(2)), "3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos, Color(0)), "1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos, Color(1)), "2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos, Color(2)), "3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos, Color(3)), "4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos_2, Color(0)), "1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos_2, Color(1)), "2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos_2, Color(2)), "3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos_2, Color(3)), "4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto, Color(0)), "1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto, Color(1)), "2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto, Color(2)), "3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto, Color(3)), "4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto_2, Color(0)), "B") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto_2, Color(1)), "W") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto_3, Color(0)), "1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto_3, Color(1)), "2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto_3, Color(2)), "3") == 0);
+}
+
+LIBBOARDGAME_TEST_CASE(pentobi_base_get_setup_id)
+{
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic, Color(0)), "A1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic, Color(1)), "A2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic, Color(2)), "A3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic, Color(3)), "A4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_2, Color(0)), "A1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_2, Color(1)), "A2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_2, Color(2)), "A3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_2, Color(3)), "A4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_3, Color(0)), "A1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_3, Color(1)), "A2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_3, Color(2)), "A3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_3, Color(3)), "A4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::duo, Color(0)), "AB") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::duo, Color(1)), "AW") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::junior, Color(0)), "AB") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::junior, Color(1)), "AW") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon, Color(0)), "A1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon, Color(1)), "A2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon, Color(2)), "A3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon, Color(3)), "A4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_2, Color(0)), "A1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_2, Color(1)), "A2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_2, Color(2)), "A3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_2, Color(3)), "A4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_3, Color(0)), "A1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_3, Color(1)), "A2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_3, Color(2)), "A3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos, Color(0)), "A1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos, Color(1)), "A2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos, Color(2)), "A3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos, Color(3)), "A4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos_2, Color(0)), "A1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos_2, Color(1)), "A2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos_2, Color(2)), "A3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos_2, Color(3)), "A4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto, Color(0)), "A1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto, Color(1)), "A2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto, Color(2)), "A3") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto, Color(3)), "A4") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto_2, Color(0)), "AB") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto_2, Color(1)), "AW") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto_3, Color(0)), "A1") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto_3, Color(1)), "A2") == 0);
+    LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto_3, Color(2)), "A3") == 0);
+}
+
+//-----------------------------------------------------------------------------
diff --git a/src/unittest/libpentobi_base/PentobiTreeTest.cpp b/src/unittest/libpentobi_base/PentobiTreeTest.cpp
new file mode 100644 (file)
index 0000000..6d8e6fc
--- /dev/null
@@ -0,0 +1,218 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/PentobiTreeTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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::InvalidProperty;
+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.to_int(), 0u);
+        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.to_int(), 1u);
+        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.to_int(), 2u);
+        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.to_int(), 3u);
+        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 InvalidProperty 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), InvalidProperty);
+}
+
+/** Check that keep_only_subtree() works in Callisto.
+    Regression test for a bug in Pentobi 12.0 */
+LIBBOARDGAME_TEST_CASE(pentobi_base_tree_keep_only_subtree_callisto)
+{
+
+    istringstream in("(;GM[Callisto]"
+                     ";1[h12]"
+                     ";2[m9]"
+                     ";3[l8]"
+                     ";4[i13]"
+                     ";1[i8]"
+                     ";2[p14])");
+    TreeReader reader;
+    reader.read(in);
+    unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+    PentobiTree tree(root);
+    auto node = &tree.get_root();
+    node = &node->get_first_child();
+    node = &node->get_first_child();
+    node = &node->get_first_child();
+    node = &node->get_first_child();
+    node = &node->get_first_child();
+    LIBBOARDGAME_CHECK(! tree.is_modified());
+    tree.keep_only_subtree(*node);
+    LIBBOARDGAME_CHECK(tree.is_modified());
+
+    node = &tree.get_root();
+    LIBBOARDGAME_CHECK(node->has_single_child());
+    {
+        auto& values = node->get_multi_property("A1");
+        LIBBOARDGAME_CHECK_EQUAL(values.size(), 2u);
+        auto begin = values.begin();
+        auto end = values.end();
+        LIBBOARDGAME_CHECK(find(begin, end, "h12") != end);
+        LIBBOARDGAME_CHECK(find(begin, end, "i8") != end);
+    }
+    {
+        auto& values = node->get_multi_property("A2");
+        LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+        LIBBOARDGAME_CHECK_EQUAL(values[0], "m9");
+    }
+    {
+        auto& values = node->get_multi_property("A3");
+        LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+        LIBBOARDGAME_CHECK_EQUAL(values[0], "l8");
+    }
+    {
+        auto& values = node->get_multi_property("A4");
+        LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+        LIBBOARDGAME_CHECK_EQUAL(values[0], "i13");
+    }
+    {
+        auto& value = node->get_property("PL");
+        LIBBOARDGAME_CHECK_EQUAL(value, "2");
+
+        node = &node->get_first_child();
+        LIBBOARDGAME_CHECK(! node->has_children());
+    }
+    {
+        auto& value = node->get_property("2");
+        LIBBOARDGAME_CHECK_EQUAL(value, "p14");
+    }
+}
+
+/** Check that keep_only_subtree() works in Nexos.
+    Regression test for a bug in Pentobi 12.0 */
+LIBBOARDGAME_TEST_CASE(pentobi_base_tree_keep_only_subtree_nexos)
+{
+
+    istringstream in("(;GM[Nexos Two-Player]"
+                     ";1[g16,g18,f19,e20]"
+                     ";2[r17,s18,t19,u20]"
+                     ";3[t5,s6,s8,r9]"
+                     ";4[e6,e8,f9,h9]"
+                     ";1[m14,h15,j15,l15])");
+    TreeReader reader;
+    reader.read(in);
+    unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+    PentobiTree tree(root);
+    auto node = &tree.get_root();
+    node = &node->get_first_child();
+    node = &node->get_first_child();
+    node = &node->get_first_child();
+    node = &node->get_first_child();
+    LIBBOARDGAME_CHECK(! tree.is_modified());
+    tree.keep_only_subtree(*node);
+    LIBBOARDGAME_CHECK(tree.is_modified());
+
+    node = &tree.get_root();
+    LIBBOARDGAME_CHECK(node->has_single_child());
+    auto values = node->get_multi_property("A1");
+    LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+    LIBBOARDGAME_CHECK_EQUAL(values[0], "g16,g18,f19,e20");
+    values = node->get_multi_property("A2");
+    LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+    LIBBOARDGAME_CHECK_EQUAL(values[0], "r17,s18,t19,u20");
+    values = node->get_multi_property("A3");
+    LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+    LIBBOARDGAME_CHECK_EQUAL(values[0], "t5,s6,s8,r9");
+    values = node->get_multi_property("A4");
+    LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+    LIBBOARDGAME_CHECK_EQUAL(values[0], "e6,e8,f9,h9");
+    auto value = node->get_property("PL");
+    LIBBOARDGAME_CHECK_EQUAL(value, "1");
+
+    node = &node->get_first_child();
+    LIBBOARDGAME_CHECK(! node->has_children());
+    value = node->get_property("1");
+    LIBBOARDGAME_CHECK_EQUAL(value, "m14,h15,j15,l15");
+}
+
+/** 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..e560cbf
--- /dev/null
@@ -0,0 +1,10 @@
+add_executable(unittest_libpentobi_mcts
+  SearchTest.cpp
+)
+
+target_link_libraries(unittest_libpentobi_mcts
+    boardgame_test_main
+    pentobi_mcts
+    )
+
+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..24f42de
--- /dev/null
@@ -0,0 +1,109 @@
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_mcts/SearchTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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::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);
+    auto bd = make_unique<Board>(tree.get_variant());
+    BoardUpdater updater;
+    updater.update(*bd, tree, get_last_node(tree.get_root()));
+    unsigned nu_threads = 1;
+    size_t memory = 10000;
+    auto search = make_unique<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);
+    auto bd = make_unique<Board>(tree.get_variant());
+    BoardUpdater updater;
+    updater.update(*bd, tree, get_last_node(tree.get_root()));
+    unsigned nu_threads = 1;
+    size_t memory = 10000;
+    auto search = make_unique<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());
+}
+
+//-----------------------------------------------------------------------------