Import pentobi_18.3.orig.tar.xz
authorJuhani Numminen <juhaninumminen0@gmail.com>
Fri, 20 Nov 2020 15:04:16 +0000 (15:04 +0000)
committerJuhani Numminen <juhaninumminen0@gmail.com>
Fri, 20 Nov 2020 15:04:16 +0000 (15:04 +0000)
[dgit import orig pentobi_18.3.orig.tar.xz]

496 files changed:
AUTHORS.md [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
HACKING.md [new file with mode: 0644]
INSTALL.md [new file with mode: 0644]
LICENSE.md [new file with mode: 0644]
NEWS.md [new file with mode: 0644]
README.md [new file with mode: 0644]
cmake/FindDocBookXSL.cmake [new file with mode: 0644]
learn_tool/CMakeLists.txt [new file with mode: 0644]
learn_tool/Main.cpp [new file with mode: 0644]
libboardgame_base/ArrayList.h [new file with mode: 0644]
libboardgame_base/Assert.cpp [new file with mode: 0644]
libboardgame_base/Assert.h [new file with mode: 0644]
libboardgame_base/Barrier.cpp [new file with mode: 0644]
libboardgame_base/Barrier.h [new file with mode: 0644]
libboardgame_base/CMakeLists.txt [new file with mode: 0644]
libboardgame_base/Compiler.h [new file with mode: 0644]
libboardgame_base/CoordPoint.cpp [new file with mode: 0644]
libboardgame_base/CoordPoint.h [new file with mode: 0644]
libboardgame_base/CpuTime.cpp [new file with mode: 0644]
libboardgame_base/CpuTime.h [new file with mode: 0644]
libboardgame_base/CpuTimeSource.cpp [new file with mode: 0644]
libboardgame_base/CpuTimeSource.h [new file with mode: 0644]
libboardgame_base/FmtSaver.h [new file with mode: 0644]
libboardgame_base/Geometry.h [new file with mode: 0644]
libboardgame_base/GeometryUtil.h [new file with mode: 0644]
libboardgame_base/Grid.h [new file with mode: 0644]
libboardgame_base/IntervalChecker.cpp [new file with mode: 0644]
libboardgame_base/IntervalChecker.h [new file with mode: 0644]
libboardgame_base/Log.cpp [new file with mode: 0644]
libboardgame_base/Log.h [new file with mode: 0644]
libboardgame_base/Marker.h [new file with mode: 0644]
libboardgame_base/MathUtil.h [new file with mode: 0644]
libboardgame_base/Memory.cpp [new file with mode: 0644]
libboardgame_base/Memory.h [new file with mode: 0644]
libboardgame_base/Options.cpp [new file with mode: 0644]
libboardgame_base/Options.h [new file with mode: 0644]
libboardgame_base/Point.h [new file with mode: 0644]
libboardgame_base/PointTransform.h [new file with mode: 0644]
libboardgame_base/RandomGenerator.cpp [new file with mode: 0644]
libboardgame_base/RandomGenerator.h [new file with mode: 0644]
libboardgame_base/Range.h [new file with mode: 0644]
libboardgame_base/Rating.cpp [new file with mode: 0644]
libboardgame_base/Rating.h [new file with mode: 0644]
libboardgame_base/Reader.cpp [new file with mode: 0644]
libboardgame_base/Reader.h [new file with mode: 0644]
libboardgame_base/RectGeometry.h [new file with mode: 0644]
libboardgame_base/RectTransform.cpp [new file with mode: 0644]
libboardgame_base/RectTransform.h [new file with mode: 0644]
libboardgame_base/SgfError.cpp [new file with mode: 0644]
libboardgame_base/SgfError.h [new file with mode: 0644]
libboardgame_base/SgfNode.cpp [new file with mode: 0644]
libboardgame_base/SgfNode.h [new file with mode: 0644]
libboardgame_base/SgfTree.cpp [new file with mode: 0644]
libboardgame_base/SgfTree.h [new file with mode: 0644]
libboardgame_base/SgfUtil.cpp [new file with mode: 0644]
libboardgame_base/SgfUtil.h [new file with mode: 0644]
libboardgame_base/Statistics.h [new file with mode: 0644]
libboardgame_base/StringRep.cpp [new file with mode: 0644]
libboardgame_base/StringRep.h [new file with mode: 0644]
libboardgame_base/StringUtil.cpp [new file with mode: 0644]
libboardgame_base/StringUtil.h [new file with mode: 0644]
libboardgame_base/TimeIntervalChecker.cpp [new file with mode: 0644]
libboardgame_base/TimeIntervalChecker.h [new file with mode: 0644]
libboardgame_base/TimeSource.cpp [new file with mode: 0644]
libboardgame_base/TimeSource.h [new file with mode: 0644]
libboardgame_base/Timer.cpp [new file with mode: 0644]
libboardgame_base/Timer.h [new file with mode: 0644]
libboardgame_base/Transform.cpp [new file with mode: 0644]
libboardgame_base/Transform.h [new file with mode: 0644]
libboardgame_base/TreeReader.cpp [new file with mode: 0644]
libboardgame_base/TreeReader.h [new file with mode: 0644]
libboardgame_base/TreeWriter.cpp [new file with mode: 0644]
libboardgame_base/TreeWriter.h [new file with mode: 0644]
libboardgame_base/WallTimeSource.cpp [new file with mode: 0644]
libboardgame_base/WallTimeSource.h [new file with mode: 0644]
libboardgame_base/Writer.cpp [new file with mode: 0644]
libboardgame_base/Writer.h [new file with mode: 0644]
libboardgame_base/tests/ArrayListTest.cpp [new file with mode: 0644]
libboardgame_base/tests/CMakeLists.txt [new file with mode: 0644]
libboardgame_base/tests/MarkerTest.cpp [new file with mode: 0644]
libboardgame_base/tests/OptionsTest.cpp [new file with mode: 0644]
libboardgame_base/tests/PointTransformTest.cpp [new file with mode: 0644]
libboardgame_base/tests/RatingTest.cpp [new file with mode: 0644]
libboardgame_base/tests/RectGeometryTest.cpp [new file with mode: 0644]
libboardgame_base/tests/SgfNodeTest.cpp [new file with mode: 0644]
libboardgame_base/tests/SgfTreeTest.cpp [new file with mode: 0644]
libboardgame_base/tests/SgfUtilTest.cpp [new file with mode: 0644]
libboardgame_base/tests/StatisticsTest.cpp [new file with mode: 0644]
libboardgame_base/tests/StringRepTest.cpp [new file with mode: 0644]
libboardgame_base/tests/StringUtilTest.cpp [new file with mode: 0644]
libboardgame_base/tests/TreeReaderTest.cpp [new file with mode: 0644]
libboardgame_gtp/Arguments.cpp [new file with mode: 0644]
libboardgame_gtp/Arguments.h [new file with mode: 0644]
libboardgame_gtp/CMakeLists.txt [new file with mode: 0644]
libboardgame_gtp/CmdLine.cpp [new file with mode: 0644]
libboardgame_gtp/CmdLine.h [new file with mode: 0644]
libboardgame_gtp/Failure.h [new file with mode: 0644]
libboardgame_gtp/GtpEngine.cpp [new file with mode: 0644]
libboardgame_gtp/GtpEngine.h [new file with mode: 0644]
libboardgame_gtp/Response.cpp [new file with mode: 0644]
libboardgame_gtp/Response.h [new file with mode: 0644]
libboardgame_gtp/tests/ArgumentsTest.cpp [new file with mode: 0644]
libboardgame_gtp/tests/CMakeLists.txt [new file with mode: 0644]
libboardgame_gtp/tests/CmdLineTest.cpp [new file with mode: 0644]
libboardgame_gtp/tests/GtpEngineTest.cpp [new file with mode: 0644]
libboardgame_gtp/tests/ResponseTest.cpp [new file with mode: 0644]
libboardgame_mcts/Atomic.h [new file with mode: 0644]
libboardgame_mcts/CMakeLists.txt [new file with mode: 0644]
libboardgame_mcts/LastGoodReply.h [new file with mode: 0644]
libboardgame_mcts/Node.h [new file with mode: 0644]
libboardgame_mcts/PlayerMove.h [new file with mode: 0644]
libboardgame_mcts/SearchBase.h [new file with mode: 0644]
libboardgame_mcts/Tree.h [new file with mode: 0644]
libboardgame_mcts/TreeUtil.h [new file with mode: 0644]
libboardgame_mcts/tests/CMakeLists.txt [new file with mode: 0644]
libboardgame_mcts/tests/NodeTest.cpp [new file with mode: 0644]
libboardgame_test/CMakeLists.txt [new file with mode: 0644]
libboardgame_test/Main.cpp [new file with mode: 0644]
libboardgame_test/Test.cpp [new file with mode: 0644]
libboardgame_test/Test.h [new file with mode: 0644]
libpentobi_base/Board.cpp [new file with mode: 0644]
libpentobi_base/Board.h [new file with mode: 0644]
libpentobi_base/BoardConst.cpp [new file with mode: 0644]
libpentobi_base/BoardConst.h [new file with mode: 0644]
libpentobi_base/BoardUpdater.cpp [new file with mode: 0644]
libpentobi_base/BoardUpdater.h [new file with mode: 0644]
libpentobi_base/BoardUtil.cpp [new file with mode: 0644]
libpentobi_base/BoardUtil.h [new file with mode: 0644]
libpentobi_base/Book.cpp [new file with mode: 0644]
libpentobi_base/Book.h [new file with mode: 0644]
libpentobi_base/CMakeLists.txt [new file with mode: 0644]
libpentobi_base/CallistoGeometry.cpp [new file with mode: 0644]
libpentobi_base/CallistoGeometry.h [new file with mode: 0644]
libpentobi_base/Color.h [new file with mode: 0644]
libpentobi_base/ColorMap.h [new file with mode: 0644]
libpentobi_base/ColorMove.h [new file with mode: 0644]
libpentobi_base/Game.cpp [new file with mode: 0644]
libpentobi_base/Game.h [new file with mode: 0644]
libpentobi_base/GembloQGeometry.cpp [new file with mode: 0644]
libpentobi_base/GembloQGeometry.h [new file with mode: 0644]
libpentobi_base/GembloQTransform.cpp [new file with mode: 0644]
libpentobi_base/GembloQTransform.h [new file with mode: 0644]
libpentobi_base/Geometry.h [new file with mode: 0644]
libpentobi_base/Grid.h [new file with mode: 0644]
libpentobi_base/Marker.h [new file with mode: 0644]
libpentobi_base/Move.h [new file with mode: 0644]
libpentobi_base/MoveInfo.h [new file with mode: 0644]
libpentobi_base/MoveList.h [new file with mode: 0644]
libpentobi_base/MoveMarker.h [new file with mode: 0644]
libpentobi_base/MovePoints.h [new file with mode: 0644]
libpentobi_base/NexosGeometry.cpp [new file with mode: 0644]
libpentobi_base/NexosGeometry.h [new file with mode: 0644]
libpentobi_base/NodeUtil.cpp [new file with mode: 0644]
libpentobi_base/NodeUtil.h [new file with mode: 0644]
libpentobi_base/Pentobi-SGF.md [new file with mode: 0644]
libpentobi_base/PentobiSgfUtil.cpp [new file with mode: 0644]
libpentobi_base/PentobiSgfUtil.h [new file with mode: 0644]
libpentobi_base/PentobiTree.cpp [new file with mode: 0644]
libpentobi_base/PentobiTree.h [new file with mode: 0644]
libpentobi_base/PentobiTreeWriter.cpp [new file with mode: 0644]
libpentobi_base/PentobiTreeWriter.h [new file with mode: 0644]
libpentobi_base/Piece.h [new file with mode: 0644]
libpentobi_base/PieceInfo.cpp [new file with mode: 0644]
libpentobi_base/PieceInfo.h [new file with mode: 0644]
libpentobi_base/PieceMap.h [new file with mode: 0644]
libpentobi_base/PieceTransforms.cpp [new file with mode: 0644]
libpentobi_base/PieceTransforms.h [new file with mode: 0644]
libpentobi_base/PieceTransformsClassic.cpp [new file with mode: 0644]
libpentobi_base/PieceTransformsClassic.h [new file with mode: 0644]
libpentobi_base/PieceTransformsGembloQ.cpp [new file with mode: 0644]
libpentobi_base/PieceTransformsGembloQ.h [new file with mode: 0644]
libpentobi_base/PieceTransformsTrigon.cpp [new file with mode: 0644]
libpentobi_base/PieceTransformsTrigon.h [new file with mode: 0644]
libpentobi_base/PlayerBase.cpp [new file with mode: 0644]
libpentobi_base/PlayerBase.h [new file with mode: 0644]
libpentobi_base/Point.h [new file with mode: 0644]
libpentobi_base/PointList.h [new file with mode: 0644]
libpentobi_base/PointState.h [new file with mode: 0644]
libpentobi_base/PrecompMoves.h [new file with mode: 0644]
libpentobi_base/ScoreUtil.h [new file with mode: 0644]
libpentobi_base/Setup.h [new file with mode: 0644]
libpentobi_base/StartingPoints.cpp [new file with mode: 0644]
libpentobi_base/StartingPoints.h [new file with mode: 0644]
libpentobi_base/SymmetricPoints.cpp [new file with mode: 0644]
libpentobi_base/SymmetricPoints.h [new file with mode: 0644]
libpentobi_base/TreeUtil.cpp [new file with mode: 0644]
libpentobi_base/TreeUtil.h [new file with mode: 0644]
libpentobi_base/TrigonGeometry.cpp [new file with mode: 0644]
libpentobi_base/TrigonGeometry.h [new file with mode: 0644]
libpentobi_base/TrigonTransform.cpp [new file with mode: 0644]
libpentobi_base/TrigonTransform.h [new file with mode: 0644]
libpentobi_base/Variant.cpp [new file with mode: 0644]
libpentobi_base/Variant.h [new file with mode: 0644]
libpentobi_base/tests/BoardConstTest.cpp [new file with mode: 0644]
libpentobi_base/tests/BoardTest.cpp [new file with mode: 0644]
libpentobi_base/tests/BoardUpdaterTest.cpp [new file with mode: 0644]
libpentobi_base/tests/CMakeLists.txt [new file with mode: 0644]
libpentobi_base/tests/GameTest.cpp [new file with mode: 0644]
libpentobi_base/tests/PentobiSgfUtilTest.cpp [new file with mode: 0644]
libpentobi_base/tests/PentobiTreeTest.cpp [new file with mode: 0644]
libpentobi_gtp/CMakeLists.txt [new file with mode: 0644]
libpentobi_gtp/GtpEngine.cpp [new file with mode: 0644]
libpentobi_gtp/GtpEngine.h [new file with mode: 0644]
libpentobi_kde_thumbnailer/CMakeLists.txt [new file with mode: 0644]
libpentobi_mcts/AnalyzeGame.cpp [new file with mode: 0644]
libpentobi_mcts/AnalyzeGame.h [new file with mode: 0644]
libpentobi_mcts/CMakeLists.txt [new file with mode: 0644]
libpentobi_mcts/Float.h [new file with mode: 0644]
libpentobi_mcts/History.cpp [new file with mode: 0644]
libpentobi_mcts/History.h [new file with mode: 0644]
libpentobi_mcts/LocalPoints.cpp [new file with mode: 0644]
libpentobi_mcts/LocalPoints.h [new file with mode: 0644]
libpentobi_mcts/Player.cpp [new file with mode: 0644]
libpentobi_mcts/Player.h [new file with mode: 0644]
libpentobi_mcts/PlayoutFeatures.h [new file with mode: 0644]
libpentobi_mcts/PriorKnowledge.cpp [new file with mode: 0644]
libpentobi_mcts/PriorKnowledge.h [new file with mode: 0644]
libpentobi_mcts/Search.cpp [new file with mode: 0644]
libpentobi_mcts/Search.h [new file with mode: 0644]
libpentobi_mcts/SearchParamConst.h [new file with mode: 0644]
libpentobi_mcts/SharedConst.cpp [new file with mode: 0644]
libpentobi_mcts/SharedConst.h [new file with mode: 0644]
libpentobi_mcts/State.cpp [new file with mode: 0644]
libpentobi_mcts/State.h [new file with mode: 0644]
libpentobi_mcts/StateUtil.cpp [new file with mode: 0644]
libpentobi_mcts/StateUtil.h [new file with mode: 0644]
libpentobi_mcts/Util.cpp [new file with mode: 0644]
libpentobi_mcts/Util.h [new file with mode: 0644]
libpentobi_mcts/tests/CMakeLists.txt [new file with mode: 0644]
libpentobi_mcts/tests/SearchTest.cpp [new file with mode: 0644]
libpentobi_paint/CMakeLists.txt [new file with mode: 0644]
libpentobi_paint/Paint.cpp [new file with mode: 0644]
libpentobi_paint/Paint.h [new file with mode: 0644]
libpentobi_thumbnail/CMakeLists.txt [new file with mode: 0644]
libpentobi_thumbnail/CreateThumbnail.cpp [new file with mode: 0644]
libpentobi_thumbnail/CreateThumbnail.h [new file with mode: 0644]
opening_books/book_callisto.blksgf [new file with mode: 0644]
opening_books/book_callisto_2.blksgf [new file with mode: 0644]
opening_books/book_callisto_2_4.blksgf [new file with mode: 0644]
opening_books/book_callisto_3.blksgf [new file with mode: 0644]
opening_books/book_classic.blksgf [new file with mode: 0644]
opening_books/book_classic_2.blksgf [new file with mode: 0644]
opening_books/book_classic_3.blksgf [new file with mode: 0644]
opening_books/book_duo.blksgf [new file with mode: 0644]
opening_books/book_gembloq.blksgf [new file with mode: 0644]
opening_books/book_gembloq_2.blksgf [new file with mode: 0644]
opening_books/book_gembloq_2_4.blksgf [new file with mode: 0644]
opening_books/book_gembloq_3.blksgf [new file with mode: 0644]
opening_books/book_junior.blksgf [new file with mode: 0644]
opening_books/book_nexos.blksgf [new file with mode: 0644]
opening_books/book_nexos_2.blksgf [new file with mode: 0644]
opening_books/book_trigon.blksgf [new file with mode: 0644]
opening_books/book_trigon_2.blksgf [new file with mode: 0644]
opening_books/book_trigon_3.blksgf [new file with mode: 0644]
opening_books/pentobi_books.qrc [new file with mode: 0644]
pentobi/AnalyzeGameModel.cpp [new file with mode: 0644]
pentobi/AnalyzeGameModel.h [new file with mode: 0644]
pentobi/AndroidUtils.cpp [new file with mode: 0644]
pentobi/AndroidUtils.h [new file with mode: 0644]
pentobi/CMakeLists.txt [new file with mode: 0644]
pentobi/GameModel.cpp [new file with mode: 0644]
pentobi/GameModel.h [new file with mode: 0644]
pentobi/ImageProvider.cpp [new file with mode: 0644]
pentobi/ImageProvider.h [new file with mode: 0644]
pentobi/Main.cpp [new file with mode: 0644]
pentobi/Pentobi.pro [new file with mode: 0644]
pentobi/PieceModel.cpp [new file with mode: 0644]
pentobi/PieceModel.h [new file with mode: 0644]
pentobi/PlayerModel.cpp [new file with mode: 0644]
pentobi/PlayerModel.h [new file with mode: 0644]
pentobi/RatingModel.cpp [new file with mode: 0644]
pentobi/RatingModel.h [new file with mode: 0644]
pentobi/SyncSettings.h [new file with mode: 0644]
pentobi/android/AndroidManifest.xml [new file with mode: 0644]
pentobi/android/res/drawable-hdpi/icon.png [new file with mode: 0644]
pentobi/android/res/drawable-mdpi/icon.png [new file with mode: 0644]
pentobi/android/res/drawable-xhdpi/icon.png [new file with mode: 0644]
pentobi/android/res/drawable-xxhdpi/icon.png [new file with mode: 0644]
pentobi/android/res/drawable-xxxhdpi/icon.png [new file with mode: 0644]
pentobi/android/res/drawable/splash.xml [new file with mode: 0644]
pentobi/android/res/values/colors.xml [new file with mode: 0644]
pentobi/android/res/values/styles.xml [new file with mode: 0644]
pentobi/android/src/net/sf/pentobi/Activity.java [new file with mode: 0644]
pentobi/android_icons_svg/icon48.svg [new file with mode: 0644]
pentobi/android_icons_svg/icon72.svg [new file with mode: 0644]
pentobi/docbook/CMakeLists.txt [new file with mode: 0644]
pentobi/docbook/create-html [new file with mode: 0755]
pentobi/docbook/create-pot [new file with mode: 0755]
pentobi/docbook/custom.xsl [new file with mode: 0644]
pentobi/docbook/docbook.its [new file with mode: 0644]
pentobi/docbook/figures/analysis.jpg [new file with mode: 0644]
pentobi/docbook/figures/board_callisto.png [new file with mode: 0644]
pentobi/docbook/figures/board_classic.png [new file with mode: 0644]
pentobi/docbook/figures/board_duo.png [new file with mode: 0644]
pentobi/docbook/figures/board_gembloq.png [new file with mode: 0644]
pentobi/docbook/figures/board_nexos.png [new file with mode: 0644]
pentobi/docbook/figures/board_trigon.jpg [new file with mode: 0644]
pentobi/docbook/figures/pieces.png [new file with mode: 0644]
pentobi/docbook/figures/pieces_callisto.png [new file with mode: 0644]
pentobi/docbook/figures/pieces_gembloq.jpg [new file with mode: 0644]
pentobi/docbook/figures/pieces_junior.png [new file with mode: 0644]
pentobi/docbook/figures/pieces_nexos.png [new file with mode: 0644]
pentobi/docbook/figures/pieces_trigon.jpg [new file with mode: 0644]
pentobi/docbook/figures/position_callisto.png [new file with mode: 0644]
pentobi/docbook/figures/position_classic.png [new file with mode: 0644]
pentobi/docbook/figures/position_duo.png [new file with mode: 0644]
pentobi/docbook/figures/position_gembloq.png [new file with mode: 0644]
pentobi/docbook/figures/position_nexos.png [new file with mode: 0644]
pentobi/docbook/figures/position_trigon.jpg [new file with mode: 0644]
pentobi/docbook/figures/rating.jpg [new file with mode: 0644]
pentobi/docbook/figures/stylesheet.css [new file with mode: 0644]
pentobi/docbook/help.qrc [new file with mode: 0644]
pentobi/docbook/index.docbook [new file with mode: 0644]
pentobi/docbook/pentobi-manual.pot [new file with mode: 0644]
pentobi/docbook/po/LINGUAS [new file with mode: 0644]
pentobi/docbook/po/de.po [new file with mode: 0644]
pentobi/docbook/po/es.po [new file with mode: 0644]
pentobi/icon/pentobi-128.svg [new file with mode: 0644]
pentobi/icon/pentobi-48.svg [new file with mode: 0644]
pentobi/icon/pentobi_icon.qrc [new file with mode: 0644]
pentobi/qml/AboutDialog.qml [new file with mode: 0644]
pentobi/qml/AnalyzeDialog.qml [new file with mode: 0644]
pentobi/qml/AnalyzeGame.qml [new file with mode: 0644]
pentobi/qml/AppearanceDialog.qml [new file with mode: 0644]
pentobi/qml/AsciiArtSaveDialog.qml [new file with mode: 0644]
pentobi/qml/Board.qml [new file with mode: 0644]
pentobi/qml/BoardContextMenu.qml [new file with mode: 0644]
pentobi/qml/BusyIndicator.qml [new file with mode: 0644]
pentobi/qml/Button.qml [new file with mode: 0644]
pentobi/qml/ButtonApply.qml [new file with mode: 0644]
pentobi/qml/ButtonCancel.qml [new file with mode: 0644]
pentobi/qml/ButtonClose.qml [new file with mode: 0644]
pentobi/qml/ButtonOk.qml [new file with mode: 0644]
pentobi/qml/ComboBox.qml [new file with mode: 0644]
pentobi/qml/Comment.qml [new file with mode: 0644]
pentobi/qml/ComputerDialog.qml [new file with mode: 0644]
pentobi/qml/Dialog.qml [new file with mode: 0644]
pentobi/qml/DialogButtonBox.qml [new file with mode: 0644]
pentobi/qml/DialogButtonBoxOkCancel.qml [new file with mode: 0644]
pentobi/qml/DialogLoader.qml [new file with mode: 0644]
pentobi/qml/ExportImageDialog.qml [new file with mode: 0644]
pentobi/qml/FatalMessage.qml [new file with mode: 0644]
pentobi/qml/FileDialog.qml [new file with mode: 0644]
pentobi/qml/GameInfoDialog.qml [new file with mode: 0644]
pentobi/qml/GameVariantDialog.qml [new file with mode: 0644]
pentobi/qml/GameView.js [new file with mode: 0644]
pentobi/qml/GameViewDesktop.qml [new file with mode: 0644]
pentobi/qml/GameViewMobile.qml [new file with mode: 0644]
pentobi/qml/GotoMoveDialog.qml [new file with mode: 0644]
pentobi/qml/HelpWindow.qml [new file with mode: 0644]
pentobi/qml/ImageSaveDialog.qml [new file with mode: 0644]
pentobi/qml/InitialRatingDialog.qml [new file with mode: 0644]
pentobi/qml/LineSegment.qml [new file with mode: 0644]
pentobi/qml/Main.js [new file with mode: 0644]
pentobi/qml/Main.qml [new file with mode: 0644]
pentobi/qml/Menu.qml [new file with mode: 0644]
pentobi/qml/MenuComputer.qml [new file with mode: 0644]
pentobi/qml/MenuEdit.qml [new file with mode: 0644]
pentobi/qml/MenuExport.qml [new file with mode: 0644]
pentobi/qml/MenuGame.qml [new file with mode: 0644]
pentobi/qml/MenuGo.qml [new file with mode: 0644]
pentobi/qml/MenuHelp.qml [new file with mode: 0644]
pentobi/qml/MenuItem.qml [new file with mode: 0644]
pentobi/qml/MenuRecentFiles.qml [new file with mode: 0644]
pentobi/qml/MenuSeparator.qml [new file with mode: 0644]
pentobi/qml/MenuTools.qml [new file with mode: 0644]
pentobi/qml/MenuView.qml [new file with mode: 0644]
pentobi/qml/MessageDialog.qml [new file with mode: 0644]
pentobi/qml/MoveAnnotationDialog.qml [new file with mode: 0644]
pentobi/qml/NavigationButtons.qml [new file with mode: 0644]
pentobi/qml/NavigationPanel.qml [new file with mode: 0644]
pentobi/qml/NewFolderDialog.qml [new file with mode: 0644]
pentobi/qml/OpenDialog.qml [new file with mode: 0644]
pentobi/qml/PieceCallisto.qml [new file with mode: 0644]
pentobi/qml/PieceClassic.qml [new file with mode: 0644]
pentobi/qml/PieceGembloQ.qml [new file with mode: 0644]
pentobi/qml/PieceList.qml [new file with mode: 0644]
pentobi/qml/PieceManipulator.qml [new file with mode: 0644]
pentobi/qml/PieceNexos.qml [new file with mode: 0644]
pentobi/qml/PieceRotationAnimation.qml [new file with mode: 0644]
pentobi/qml/PieceSelectorDesktop.qml [new file with mode: 0644]
pentobi/qml/PieceSelectorMobile.qml [new file with mode: 0644]
pentobi/qml/PieceSwitchedFlipAnimation.qml [new file with mode: 0644]
pentobi/qml/PieceTrigon.qml [new file with mode: 0644]
pentobi/qml/QuarterSquare.qml [new file with mode: 0644]
pentobi/qml/QuestionDialog.qml [new file with mode: 0644]
pentobi/qml/RatingDialog.qml [new file with mode: 0644]
pentobi/qml/RatingGraph.qml [new file with mode: 0644]
pentobi/qml/SaveDialog.qml [new file with mode: 0644]
pentobi/qml/ScoreDisplay.qml [new file with mode: 0644]
pentobi/qml/ScoreElement.qml [new file with mode: 0644]
pentobi/qml/ScoreElement2.qml [new file with mode: 0644]
pentobi/qml/Square.qml [new file with mode: 0644]
pentobi/qml/ToolBar.qml [new file with mode: 0644]
pentobi/qml/Triangle.qml [new file with mode: 0644]
pentobi/qml/i18n/qml_de.ts [new file with mode: 0644]
pentobi/qml/i18n/qml_en.ts [new file with mode: 0644]
pentobi/qml/i18n/qml_es.ts [new file with mode: 0644]
pentobi/qml/i18n/qml_fr.ts [new file with mode: 0644]
pentobi/qml/i18n/qml_nb_NO.ts [new file with mode: 0644]
pentobi/qml/i18n/qml_ru.ts [new file with mode: 0644]
pentobi/qml/i18n/qml_zh_CN.ts [new file with mode: 0644]
pentobi/qml/i18n/translations.qrc [new file with mode: 0644]
pentobi/qml/icons/filedialog-folder.svg [new file with mode: 0644]
pentobi/qml/icons/filedialog-newfolder.svg [new file with mode: 0644]
pentobi/qml/icons/filedialog-parent.svg [new file with mode: 0644]
pentobi/qml/themes/colorblind-dark/Theme.qml [new file with mode: 0644]
pentobi/qml/themes/colorblind-light/Theme.qml [new file with mode: 0644]
pentobi/qml/themes/dark/Theme.qml [new file with mode: 0644]
pentobi/qml/themes/dark/pentobi-rated-game.svg [new file with mode: 0644]
pentobi/qml/themes/dark/piece-manipulator-desktop-legal.svg [new file with mode: 0644]
pentobi/qml/themes/dark/piece-manipulator-desktop.svg [new file with mode: 0644]
pentobi/qml/themes/dark/piece-manipulator-legal.svg [new file with mode: 0644]
pentobi/qml/themes/dark/piece-manipulator.svg [new file with mode: 0644]
pentobi/qml/themes/light/Theme.qml [new file with mode: 0644]
pentobi/qml/themes/light/menu-desktop.svg [new file with mode: 0644]
pentobi/qml/themes/light/menu.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-backward.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-backward10.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-beginning.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-computer-colors.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-end.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-forward.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-forward10.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-newgame.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-next-variation.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-play.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-previous-variation.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-rated-game.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-stop.svg [new file with mode: 0644]
pentobi/qml/themes/light/pentobi-undo.svg [new file with mode: 0644]
pentobi/qml/themes/light/piece-manipulator-desktop-legal.svg [new file with mode: 0644]
pentobi/qml/themes/light/piece-manipulator-desktop.svg [new file with mode: 0644]
pentobi/qml/themes/light/piece-manipulator-legal.svg [new file with mode: 0644]
pentobi/qml/themes/light/piece-manipulator.svg [new file with mode: 0644]
pentobi/qml/themes/system/Theme.qml [new file with mode: 0644]
pentobi/qml/themes/themes.qrc [new file with mode: 0644]
pentobi/resources.qrc [new file with mode: 0644]
pentobi/resources_desktop.qrc [new file with mode: 0644]
pentobi/unix/CMakeLists.txt [new file with mode: 0644]
pentobi/unix/application-x-blokus-sgf-128.svg [new file with mode: 0644]
pentobi/unix/application-x-blokus-sgf-48.svg [new file with mode: 0644]
pentobi/unix/create-pot [new file with mode: 0755]
pentobi/unix/io.sourceforge.pentobi.appdata.xml.in [new file with mode: 0644]
pentobi/unix/io.sourceforge.pentobi.desktop.in [new file with mode: 0644]
pentobi/unix/manpage.its [new file with mode: 0644]
pentobi/unix/manpage.xsl [new file with mode: 0644]
pentobi/unix/mime.its [new file with mode: 0644]
pentobi/unix/pentobi-manpage.docbook.in [new file with mode: 0644]
pentobi/unix/pentobi-mime.xml.in [new file with mode: 0644]
pentobi/unix/pentobi-unix.pot [new file with mode: 0644]
pentobi/unix/pentobi.6.in [new file with mode: 0644]
pentobi/unix/po/LINGUAS [new file with mode: 0644]
pentobi/unix/po/de.po [new file with mode: 0644]
pentobi/unix/po/es.po [new file with mode: 0644]
pentobi/unix/po/ru.po [new file with mode: 0644]
pentobi_gtp/CMakeLists.txt [new file with mode: 0644]
pentobi_gtp/GtpEngine.cpp [new file with mode: 0644]
pentobi_gtp/GtpEngine.h [new file with mode: 0644]
pentobi_gtp/Main.cpp [new file with mode: 0644]
pentobi_gtp/Pentobi-GTP.md [new file with mode: 0644]
pentobi_kde_thumbnailer/CMakeLists.txt [new file with mode: 0644]
pentobi_kde_thumbnailer/CTestCustom.cmake [new file with mode: 0644]
pentobi_kde_thumbnailer/PentobiThumbCreator.cpp [new file with mode: 0644]
pentobi_kde_thumbnailer/PentobiThumbCreator.h [new file with mode: 0644]
pentobi_kde_thumbnailer/pentobi-thumbnail.desktop [new file with mode: 0644]
pentobi_thumbnailer/CMakeLists.txt [new file with mode: 0644]
pentobi_thumbnailer/Main.cpp [new file with mode: 0644]
pentobi_thumbnailer/create-pot [new file with mode: 0755]
pentobi_thumbnailer/i18n/de.ts [new file with mode: 0644]
pentobi_thumbnailer/i18n/en.ts [new file with mode: 0644]
pentobi_thumbnailer/i18n/es.ts [new file with mode: 0644]
pentobi_thumbnailer/i18n/ru.ts [new file with mode: 0644]
pentobi_thumbnailer/i18n/translations.qrc [new file with mode: 0644]
pentobi_thumbnailer/pentobi-thumbnailer-manpage.docbook.in [new file with mode: 0644]
pentobi_thumbnailer/pentobi-thumbnailer.pot [new file with mode: 0644]
pentobi_thumbnailer/pentobi.thumbnailer.in [new file with mode: 0644]
pentobi_thumbnailer/po/LINGUAS [new file with mode: 0644]
pentobi_thumbnailer/po/de.po [new file with mode: 0644]
pentobi_thumbnailer/po/es.po [new file with mode: 0644]
pentobi_thumbnailer/po/ru.po [new file with mode: 0644]
twogtp/Analyze.cpp [new file with mode: 0644]
twogtp/Analyze.h [new file with mode: 0644]
twogtp/CMakeLists.txt [new file with mode: 0644]
twogtp/FdStream.cpp [new file with mode: 0644]
twogtp/FdStream.h [new file with mode: 0644]
twogtp/GtpConnection.cpp [new file with mode: 0644]
twogtp/GtpConnection.h [new file with mode: 0644]
twogtp/Main.cpp [new file with mode: 0644]
twogtp/Output.cpp [new file with mode: 0644]
twogtp/Output.h [new file with mode: 0644]
twogtp/OutputTree.cpp [new file with mode: 0644]
twogtp/OutputTree.h [new file with mode: 0644]
twogtp/TwoGtp.cpp [new file with mode: 0644]
twogtp/TwoGtp.h [new file with mode: 0644]

diff --git a/AUTHORS.md b/AUTHORS.md
new file mode 100644 (file)
index 0000000..d1c3dcc
--- /dev/null
@@ -0,0 +1,16 @@
+Pentobi Authors
+===============
+
+Main Developer
+--------------
+
+* Markus Enzenberger <enz@users.sourceforge.net>
+
+Translators
+-----------
+
+* Allan Nordhøy <epost@anotheragency.no> (Norwegian Bokmål)
+* Cherry <taikocherry@126.com> (Simplified Chinese)
+* Francisco Zamorano <pacozamo@gmail.com> (Spanish)
+* Markus Enzenberger <enz@users.sourceforge.net> (German, French)
+* Viktor Erukhin <official159ru@mail.ru> (Russian)
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..38eda43
--- /dev/null
@@ -0,0 +1,78 @@
+cmake_minimum_required(VERSION 3.1.0)
+
+project(Pentobi)
+set(PENTOBI_VERSION 18.3)
+set(PENTOBI_RELEASE_DATE 2020-11-04)
+
+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_THUMBNAILER "Build Gnome thumbnailer" ${UNIX})
+option(PENTOBI_BUILD_KDE_THUMBNAILER "Build KDE thumbnailer" OFF)
+option(PENTOBI_OPEN_HELP_EXTERNALLY "Force using web browser for displaying help" OFF)
+option(BUILD_TESTING "Build tests" OFF)
+
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
+
+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 17)
+if(CMAKE_COMPILER_IS_GNUCXX OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang"))
+  add_compile_options(-ffast-math -Wall -Wextra)
+endif()
+
+if(BUILD_TESTING)
+  if(PENTOBI_BUILD_KDE_THUMBNAILER)
+    configure_file(pentobi_kde_thumbnailer/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(libboardgame_base)
+add_subdirectory(libpentobi_base)
+if(BUILD_TESTING)
+    add_subdirectory(libboardgame_test)
+endif()
+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(libpentobi_gtp)
+    add_subdirectory(pentobi_gtp)
+    if(UNIX)
+        add_subdirectory(twogtp)
+    else()
+        message(STATUS "Not building twogtp, needs POSIX")
+    endif()
+    add_subdirectory(learn_tool)
+endif()
+if(PENTOBI_BUILD_GUI)
+    add_subdirectory(libpentobi_paint)
+    add_subdirectory(pentobi)
+    if(PENTOBI_BUILD_THUMBNAILER)
+        add_subdirectory(libpentobi_thumbnail)
+        add_subdirectory(pentobi_thumbnailer)
+    endif()
+endif()
+if(PENTOBI_BUILD_KDE_THUMBNAILER)
+    add_subdirectory(libpentobi_kde_thumbnailer)
+    add_subdirectory(pentobi_kde_thumbnailer)
+endif()
+
diff --git a/HACKING.md b/HACKING.md
new file mode 100644 (file)
index 0000000..a7906a1
--- /dev/null
@@ -0,0 +1,84 @@
+Pentobi Source Code Overview
+============================
+
+Libboardgame Modules
+--------------------
+
+The Libboardgame modules contain code that is not specific to Blokus and
+could be reused for other board games.
+
+* __libboardgame_base__
+  General utilities and functionality for board games
+* __libboardgame_gtp__
+  Implementation of the [Go Text Protocol](https://en.wikipedia.org/wiki/Go_Text_Protocol) (GTP)
+* __libboardgame_test__
+  Functionality for unit tests
+* __libboardgame_mcts__
+  Abstract Monte-Carlo tree search (MCTS)
+
+Pentobi Engine Modules
+----------------------
+
+The engine modules contain code that is specific to Blokus and the
+computer player used in Pentobi.
+
+* __libpentobi_base__
+  General Blokus-specific functionality. The board implementation is
+  optimized for fast move generation needed in MCTS. For a definition
+  of the game file format, see [Pentobi-SGF](libpentobi_base/Pentobi-SGF.md)
+* __libpentobi_gtp__
+  General Blokus-specific GTP interface based on libboardgame_gtp and
+  libpentobi_base.
+* __libpentobi_mcts__
+  Main Blokus computer player used in Pentobi based on libboardgame_mcts
+* __opening_books__
+  Opening moves in SGF format used by libpentobi_mcts for fast move
+  generation without search in early positions
+* __learn_tool__
+  Tool for learning the move priors used in libpentobi_mcts
+* __pentobi_gtp__
+  GTP interface to the player in libpentobi_mcts.
+  See [Pentobi-GTP](pentobi_gtp/Pentobi-GTP.md) for more information.
+* __twogtp__
+  Tool for playing Blokus games between two GTP engines (currently only
+  supported on Unix)
+
+Pentobi GUI Modules
+-------------------
+
+The GUI modules implement the user interface. They depend on the
+[Qt](https://www.qt.io/) libraries.
+
+* __pentobi__
+  Main program that provides a GUI for the player in libpentobi_mcts
+* __pentobi_thumbnailer__
+  File preview generator for the [Gnome](http://www.gnome.org) desktop
+* __pentobi_kde_thumbnailer__
+  File preview generator for the [KDE](http://www.kde.org) desktop
+* __libpentobi_paint__
+  Common functionality for pentobi and libpentobi_thumbnail
+* __libpentobi_thumbnail__
+  Common functionality for pentobi_thumbnailer and
+  pentobi_kde_thumbnailer
+* __libpentobi_kde_thumbnailer__
+  Only needed for technical reasons during compilation, see comment in
+  libpentobi_kde_thumbnailer/CMakeLists.txt
+
+Translations
+============
+
+Translations can be contributed at [Transifex](https://www.transifex.com/markus-enzenberger/pentobi/).
+Translation components will be included in Pentobi if at least 75
+percent of the strings are translated. The translation components are:
+
+* __Pentobi__ ([QT format](https://doc.qt.io/qt-5/linguist-ts-file-format.html))
+  User interface of Pentobi main program
+* __Pentobi Manual__ ([PO format](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html))
+  User manual
+* __Pentobi Unix Files__ (PO format)
+  Additional Pentobi files installed on Unix (desktop entry, appstream
+  file, manpage, etc.)
+* __Pentobi Thumbnailer__ (QT format)
+  Messages used in Gnome thumbnailer
+* __Pentobi Thumbnailer Unix Files__ (PO format)
+  Additional Pentobi Thumbnailer files installed on Unix (manpage)
diff --git a/INSTALL.md b/INSTALL.md
new file mode 100644 (file)
index 0000000..7eb7d55
--- /dev/null
@@ -0,0 +1,74 @@
+Compiling and Installing Pentobi From the Sources
+=================================================
+
+Requirements
+------------
+
+Building Pentobi requires the following tools and libraries:
+
+* C++ compiler with C++17 support (e.g. GCC >=5)
+* [Qt libraries](https://www.qt.io/) (>=5.12)
+* [CMake](https://cmake.org/) (>=3.1.0)
+* [GNU gettext](https://www.gnu.org/software/gettext/)
+* [AppStream](https://github.com/ximion/appstream)
+* [ITS Tool](http://itstool.org/)
+* [xsltproc](http://xmlsoft.org/XSLT/xsltproc.html)
+* [DocBooc XSL](http://www.sagehill.net/docbookxsl/)
+* [LibRsvg](https://wiki.gnome.org/Projects/LibRsvg)
+
+In Debian-based distributions, they can be installed with the command
+```
+sudo apt install appstream cmake docbook-xsl g++ gettext itstool \
+  libqt5svg5-dev libqt5webview5-dev librsvg2-bin 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 qt5-default qtquickcontrols2-5-dev qttools5-dev \
+  xsltproc
+```
+
+Building
+--------
+
+Pentobi can be compiled from the source directory with the commands
+```
+cmake -DCMAKE_BUILD_TYPE=Release .
+make
+```
+
+Building the KDE Thumbnailer Plugin
+-----------------------------------
+
+A thumbnailer plugin for KDE can be built by using the cmake option
+`-DPENTOBI_BUILD_KDE_THUMBNAILER=ON`. 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 App
+------------------------
+
+The Android app currently needs to be built with the qmake project file
+in `pentobi/Pentobi.pro`. It was tested with Qt 5.15 and currently only
+works for single-ABI builds. The maximum supported targetSdkVersion is
+currently 29 (Android 10) because the code still relies on legacy
+external storage access.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644 (file)
index 0000000..2fb2e74
--- /dev/null
@@ -0,0 +1,675 @@
+### 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/NEWS.md b/NEWS.md
new file mode 100644 (file)
index 0000000..2eb2a45
--- /dev/null
+++ b/NEWS.md
@@ -0,0 +1,906 @@
+Pentobi Release Notes
+=====================
+
+Version 18.3 (04 Nov 2020)
+--------------------------
+
+* Made opening of recent rated games from rating dialog work again.
+* New Russian UI translation (thanks to Viktor Erukhin)
+* Back key now needs to be pressed twice to exit Android app to avoid
+  unintentional exit with gesture navigation on newer Android versions.
+* Better visual feedback of button presses on newer Android devices.
+* Added workaround for white single-pixel line visible on some
+  Android devices when switching fullscreen mode (QTBUG-55600).
+* Added requestLegacyExternalStorage to Android manifest, which supports
+  saving files if compiled with target SDK version 29 (Android 10).
+* Menu shortcuts are no longer supported.
+
+Version 18.2 (10 Oct 2020)
+--------------------------
+
+* Fixed truncated submenu in desktop version with Qt 5.15.
+* Updated bugtracker link.
+
+Version 18.1 (25 Jun 2020)
+--------------------------
+
+* Fixed compilation with Qt 5.15.
+* Updated AndroidManifest.xml for usage with Qt 5.15.
+* HTML manual generation failed with older versions of DocBook XSL.
+
+Version 18.0 (11 May 2020)
+--------------------------
+
+* The minimum required Qt version is now 5.12.
+* Experimental support for landscape orientation on Android (not
+  enabled by default yet, see comment in GameViewMobile.qml).
+* Fixed crash if application was closed while game analysis was running.
+* Added missing include that broke the compilation with GCC 10 if
+  compiled with PENTOBI_BUILD_GTP=1.
+
+Version 17.3 (05 Nov 2019)
+--------------------------
+
+* Don't use Fusion style on desktop anymore because it is currently
+  broken on some Linux distributions (QTBUG-77107).
+* Added more search hints for location of DocBook XSL.
+
+Version 17.2 (13 Sep 2019)
+--------------------------
+
+* New Spanish translation (thanks to Francisco Zamorano).
+* Support for different analysis speeds on Android (only fast and normal
+  speed).
+* Added more search hints for location of DocBook XSL.
+* PENTOBI_OPEN_HELP_EXTERNALLY will no longer be set automatically if
+  Qt5WebView is not found, but must be set explicitly to avoid that it
+  is accidentally used if the Qt5WebView package exists on the platform
+  but has not been installed.
+* New Game button and menu item were not enabled if no moves were played
+  but an analysis graph existed (e.g. after analyzing a game and then
+  undoing all moves).
+* Clear autosaved analysis if autosaved game corresponded to a file
+  that no longer exists.
+
+Version 17.1 (12 Jun 2019)
+--------------------------
+
+* Move generation hung if a move generation using search was aborted
+  and the next move generation used the opening book.
+* Added search hint for DocBook XSL for compilation on OpenSUSE.
+
+Version 17.0 (05 Jun 2019)
+--------------------------
+
+* Small increase in playing strength, mainly in Callisto and Classic.
+* The user manual, desktop entry, AppData, MIME info and manpages are
+  now translatable (GitHub issue #6). See INSTALL for the new build-time
+  dependencies.
+* New UI translation: Simplified Chinese (thanks to Cherry)
+* Show message if move generation fails, which can happen in certain
+  setup positions.
+* Recommend that Blokus SGF files start with the GM property and reduced
+  the maximum pattern offset for automatic MIME type detection to reduce
+  the probability of false positives.
+* The compiler now needs to support C++17.
+* The SVG to bitmap conversions at build time are now done using
+  LibRsvg instead of a custom compiled Qt-based build helper to make
+  cross-compilation easier.
+* The MetaInfo file for the KDE thumbnailer was removed as current
+  software installations GUIs do not show addons anyway.
+* Renamed build option PENTOBI_BUILD_TESTS to BUILD_TESTING for
+  compatibility with CTest.
+
+### Bug Fixes
+
+* KDE thumbnailer crashed with unhandled exception on certain invalid
+  Blokus SGF files.
+
+Version 16.3 (17 Apr 2019)
+--------------------------
+
+* Piece in GembloQ could temporarily disappear if the rotate backward
+  button was hit quickly multiple times.
+* Piece could become stuck partially flipped if orientation change was
+  triggered while last animation was still running.
+* Current file name was not cleared and button New Game not enabled
+  after opening a file from clipboard.
+* Comment scrolling did not always work.
+* Avoid jumping of splashscreen icon on Android.
+* Shared MIME Info was missing a pattern for detecting Classic 3-player
+  game files independent of the file extension.
+* The Android version now interactively asks for permission when opening
+  or saving a game if storage permission has not yet been granted.
+* Development tool twogtp did not resolve ties as a win for the second
+  player in two-player Callisto.
+* Disabled QtQuickCompiler for desktop version to avoid the need for
+  recompilation after upgrading the Qt libraries (see also Ubuntu bug
+  #1824560).
+
+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.md b/README.md
new file mode 100644 (file)
index 0000000..18d6d1b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,43 @@
+Pentobi Readme
+==============
+
+![Screenshot](https://pentobi.sourceforge.io/pentobi-classic-small.png)
+
+Pentobi is a computer opponent for the board game
+[Blokus](https://en.wikipedia.org/wiki/Blokus). It has a strong Blokus engine
+with different playing levels. The supported game variants are Classic, Duo,
+Trigon, Junior, Nexos, GembloQ and Callisto.
+
+See [INSTALL](INSTALL.md) for instructions how to build and install
+the program from the sources. See [NEWS](NEWS.md) for release notes.
+See [HACKING](HACKING.md) for an overview of the source code.
+
+Contact
+-------
+
+The homepage of Pentobi is at https://pentobi.sourceforge.io.
+The maintainer of Pentobi is Markus Enzenberger. Bugs can be reported
+at the [issue tracker](https://github.com/enz/pentobi/issues) at GitHub.
+
+License
+-------
+
+Copyright (C) 2011-2020 Markus Enzenberger.
+See [AUTHORS](AUTHORS.md) for a full list of authors.
+
+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](LICENSE.md) 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/cmake/FindDocBookXSL.cmake b/cmake/FindDocBookXSL.cmake
new file mode 100644 (file)
index 0000000..4d397fe
--- /dev/null
@@ -0,0 +1,14 @@
+include(FindPackageHandleStandardArgs)
+find_path(DOCBOOKXSL_DIR html/chunk.xsl
+    HINTS
+    /usr/share/xml/docbook/stylesheet/docbook-xsl # Debian
+    /usr/local/share/xsl/docbook # FreeBSD
+    /usr/share/xml/docbook/stylesheet/nwalsh/current # OpenSUSE
+    /usr/share/sgml/docbook/xsl-ns-stylesheets # Fedora
+    /usr/share/sgml/docbook/xsl-stylesheets # Fedora
+    )
+find_package_handle_standard_args(DocBookXSL
+    "Could NOT find DocBook XSL stylesheets. Install docbook-xsl and/or use \
+    -DDOCBOOKXSL_DIR=<dir> to define its location."
+    DOCBOOKXSL_DIR)
+mark_as_advanced(DOCBOOKXSL_DIR)
diff --git a/learn_tool/CMakeLists.txt b/learn_tool/CMakeLists.txt
new file mode 100644 (file)
index 0000000..b44fbda
--- /dev/null
@@ -0,0 +1,8 @@
+find_package(Threads)
+
+add_executable(learn-tool Main.cpp)
+
+target_link_libraries(learn-tool
+  pentobi_mcts
+  Threads::Threads
+)
diff --git a/learn_tool/Main.cpp b/learn_tool/Main.cpp
new file mode 100644 (file)
index 0000000..c771486
--- /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_base/FmtSaver.h"
+#include "libboardgame_base/Log.h"
+#include "libboardgame_base/Options.h"
+#include "libboardgame_base/TreeReader.h"
+#include "libpentobi_base/Game.h"
+#include "libpentobi_base/MoveMarker.h"
+#include "libpentobi_mcts/LocalPoints.h"
+
+using namespace std;
+using libboardgame_base::split;
+using libboardgame_base::FmtSaver;
+using libboardgame_base::Options;
+using libboardgame_base::TreeReader;
+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 happens 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_base::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/libboardgame_base/ArrayList.h b/libboardgame_base/ArrayList.h
new file mode 100644 (file)
index 0000000..1db1988
--- /dev/null
@@ -0,0 +1,302 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/ArrayList.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_ARRAY_LIST_H
+#define LIBBOARDGAME_BASE_ARRAY_LIST_H
+
+#include <algorithm>
+#include <array>
+#include <initializer_list>
+#include <iostream>
+#include "Assert.h"
+
+namespace libboardgame_base {
+
+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 constexpr 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) { *this = 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() { return m_a.begin(); }
+
+    const_iterator begin() const { return m_a.begin(); }
+
+    iterator end() { return begin() + m_size; }
+
+    const_iterator end() const { return begin() + m_size; }
+
+    T& back();
+
+    const T& back() const;
+
+    I size() const { return m_size; }
+
+    bool empty() const { return m_size == 0; }
+
+    const T& pop_back();
+
+    void push_back(const T& t);
+
+    void clear() { m_size = 0; }
+
+    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 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>
+bool ArrayList<T, M, I>::contains(const T& t) const
+{
+    return find(begin(), end(), t) != end();
+}
+
+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>
+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_base
+
+#endif // LIBBOARDGAME_BASE_ARRAY_LIST_H
diff --git a/libboardgame_base/Assert.cpp b/libboardgame_base/Assert.cpp
new file mode 100644 (file)
index 0000000..d0d92d6
--- /dev/null
@@ -0,0 +1,72 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/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
+
+namespace libboardgame_base {
+
+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(
+        [[maybe_unused]] const char* expression,
+        [[maybe_unused]] const char* file, [[maybe_unused]] int line)
+{
+    static bool is_during_handle_assertion = false;
+    LIBBOARDGAME_LOG(file, ":", line, ": Assertion '", expression, "' failed");
+    flush_log();
+    if (! is_during_handle_assertion)
+    {
+        is_during_handle_assertion = true;
+        for_each(get_all_handlers().begin(), get_all_handlers().end(),
+                 mem_fn(&AssertionHandler::run));
+    }
+    abort();
+}
+
+#endif
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
diff --git a/libboardgame_base/Assert.h b/libboardgame_base/Assert.h
new file mode 100644 (file)
index 0000000..5247943
--- /dev/null
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Assert.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_ASSERT_H
+#define LIBBOARDGAME_BASE_ASSERT_H
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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_base
+
+//-----------------------------------------------------------------------------
+
+/** @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_base::handle_assertion(#expr, __FILE__, __LINE__))
+#else
+#define LIBBOARDGAME_ASSERT(expr) (static_cast<void>(0))
+#endif
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBBOARDGAME_BASE_ASSERT_H
diff --git a/libboardgame_base/Barrier.cpp b/libboardgame_base/Barrier.cpp
new file mode 100644 (file)
index 0000000..38a765c
--- /dev/null
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Barrier.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Barrier.h"
+
+#include "Assert.h"
+
+namespace libboardgame_base {
+
+//----------------------------------------------------------------------------
+
+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_base
diff --git a/libboardgame_base/Barrier.h b/libboardgame_base/Barrier.h
new file mode 100644 (file)
index 0000000..512309c
--- /dev/null
@@ -0,0 +1,43 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Barrier.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_BARRIER_H
+#define LIBBOARDGAME_BASE_BARRIER_H
+
+#include <condition_variable>
+#include <mutex>
+
+namespace libboardgame_base {
+
+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_base
+
+#endif // LIBBOARDGAME_BASE_BARRIER_H
diff --git a/libboardgame_base/CMakeLists.txt b/libboardgame_base/CMakeLists.txt
new file mode 100644 (file)
index 0000000..bcfce04
--- /dev/null
@@ -0,0 +1,78 @@
+add_library(boardgame_base STATIC
+    ArrayList.h
+    Assert.h
+    Assert.cpp
+    Barrier.h
+    Barrier.cpp
+    Compiler.h
+    CoordPoint.h
+    CoordPoint.cpp
+    CpuTime.h
+    CpuTime.cpp
+    CpuTimeSource.h
+    CpuTimeSource.cpp
+    FmtSaver.h
+    Geometry.h
+    GeometryUtil.h
+    Grid.h
+    IntervalChecker.h
+    IntervalChecker.cpp
+    Log.h
+    Log.cpp
+    Marker.h
+    MathUtil.h
+    Memory.h
+    Memory.cpp
+    Options.h
+    Options.cpp
+    Point.h
+    PointTransform.h
+    RandomGenerator.h
+    RandomGenerator.cpp
+    Range.h
+    Rating.h
+    Rating.cpp
+    Reader.h
+    Reader.cpp
+    RectGeometry.h
+    RectTransform.h
+    RectTransform.cpp
+    SgfError.h
+    SgfError.cpp
+    SgfNode.h
+    SgfNode.cpp
+    SgfTree.h
+    SgfTree.cpp
+    SgfUtil.h
+    SgfUtil.cpp
+    Statistics.h
+    StringRep.h
+    StringRep.cpp
+    StringUtil.h
+    StringUtil.cpp
+    TimeIntervalChecker.h
+    TimeIntervalChecker.cpp
+    Timer.h
+    Timer.cpp
+    TimeSource.h
+    TimeSource.cpp
+    Transform.h
+    Transform.cpp
+    TreeReader.h
+    TreeReader.cpp
+    TreeWriter.h
+    TreeWriter.cpp
+    WallTimeSource.h
+    WallTimeSource.cpp
+    Writer.h
+    Writer.cpp
+    )
+
+target_compile_options(boardgame_base PUBLIC
+    "$<$<CONFIG:DEBUG>:-DLIBBOARDGAME_DEBUG>")
+
+target_include_directories(boardgame_base PUBLIC ..)
+
+if(BUILD_TESTING)
+    add_subdirectory(tests)
+endif()
diff --git a/libboardgame_base/Compiler.h b/libboardgame_base/Compiler.h
new file mode 100644 (file)
index 0000000..5088a8c
--- /dev/null
@@ -0,0 +1,66 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Compiler.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_COMPILER_H
+#define LIBBOARDGAME_BASE_COMPILER_H
+
+#include <string>
+#include <typeinfo>
+#if defined __GNUC__ && __has_include(<cxxabi.h>)
+#include <cstdlib>
+#include <cxxabi.h>
+#endif
+
+namespace libboardgame_base {
+
+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)
+{
+#if defined __GNUC__ && __has_include(<cxxabi.h>)
+    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_base
+
+#endif // LIBBOARDGAME_BASE_COMPILER_H
diff --git a/libboardgame_base/CoordPoint.cpp b/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/libboardgame_base/CoordPoint.h b/libboardgame_base/CoordPoint.h
new file mode 100644 (file)
index 0000000..c2369c8
--- /dev/null
@@ -0,0 +1,129 @@
+//-----------------------------------------------------------------------------
+/** @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 "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 { return {x - p.x, y - p.y}; }
+
+    CoordPoint& operator+=(CoordPoint p);
+
+    CoordPoint& operator-=(CoordPoint p);
+
+    bool is_null() const { return x == numeric_limits<int>::max(); }
+
+    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 < static_cast<unsigned>(numeric_limits<int>::max()));
+    LIBBOARDGAME_ASSERT(y < static_cast<unsigned>(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)
+{
+    *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);
+}
+
+//-----------------------------------------------------------------------------
+
+ostream& operator<<(ostream& out, CoordPoint p);
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_COORD_POINT_H
diff --git a/libboardgame_base/CpuTime.cpp b/libboardgame_base/CpuTime.cpp
new file mode 100644 (file)
index 0000000..72e7908
--- /dev/null
@@ -0,0 +1,56 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/CpuTime.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CpuTime.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+#if __has_include(<unistd.h>)
+#include <unistd.h>
+#endif
+
+#if __has_include(<sys/times.h>)
+#include <sys/times.h>
+#endif
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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 __has_include(<unistd.h>) && __has_include(<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_base
diff --git a/libboardgame_base/CpuTime.h b/libboardgame_base/CpuTime.h
new file mode 100644 (file)
index 0000000..f1db6b3
--- /dev/null
@@ -0,0 +1,23 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/CpuTime.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_CPU_TIME_H
+#define LIBBOARDGAME_BASE_CPU_TIME_H
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+/** 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_base
+
+#endif // LIBBOARDGAME_BASE_CPU_TIME_H
diff --git a/libboardgame_base/CpuTimeSource.cpp b/libboardgame_base/CpuTimeSource.cpp
new file mode 100644 (file)
index 0000000..9f22ad4
--- /dev/null
@@ -0,0 +1,22 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/CpuTimeSource.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CpuTimeSource.h"
+
+#include "CpuTime.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+double CpuTimeSource::operator()()
+{
+    return cpu_time();
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
diff --git a/libboardgame_base/CpuTimeSource.h b/libboardgame_base/CpuTimeSource.h
new file mode 100644 (file)
index 0000000..11a67ad
--- /dev/null
@@ -0,0 +1,27 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/CpuTimeSource.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_CPU_TIME_SOURCE_H
+#define LIBBOARDGAME_BASE_CPU_TIME_SOURCE_H
+
+#include "TimeSource.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+/** CPU time. */
+class CpuTimeSource
+    : public TimeSource
+{
+public:
+    double operator()() override;
+};
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_CPU_TIME_SOURCE_H
diff --git a/libboardgame_base/FmtSaver.h b/libboardgame_base/FmtSaver.h
new file mode 100644 (file)
index 0000000..4eb585e
--- /dev/null
@@ -0,0 +1,37 @@
+//----------------------------------------------------------------------------
+/** @file libboardgame_base/FmtSaver.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_FMT_SAVER_H
+#define LIBBOARDGAME_BASE_FMT_SAVER_H
+
+#include <iostream>
+
+namespace libboardgame_base {
+
+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_base
+
+#endif // LIBBOARDGAME_BASE_FMT_SAVER_H
diff --git a/libboardgame_base/Geometry.h b/libboardgame_base/Geometry.h
new file mode 100644 (file)
index 0000000..942ff04
--- /dev/null
@@ -0,0 +1,336 @@
+//-----------------------------------------------------------------------------
+/** @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 "ArrayList.h"
+#include "CoordPoint.h"
+#include "StringRep.h"
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** %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 Point (or compatible class) */
+template<class P>
+class Geometry
+{
+public:
+    using Point = P;
+
+    using IntType = typename Point::IntType;
+
+    static constexpr unsigned max_adj = 4;
+
+    static constexpr 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({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 { return p.to_int() < m_range; }
+#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();
+}
+
+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/libboardgame_base/GeometryUtil.h b/libboardgame_base/GeometryUtil.h
new file mode 100644 (file)
index 0000000..5cc6be9
--- /dev/null
@@ -0,0 +1,87 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+
+//-----------------------------------------------------------------------------
+
+/** 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 libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_GEOMETRY_UTIL_H
diff --git a/libboardgame_base/Grid.h b/libboardgame_base/Grid.h
new file mode 100644 (file)
index 0000000..94592ec
--- /dev/null
@@ -0,0 +1,214 @@
+//-----------------------------------------------------------------------------
+/** @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 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)
+{
+    static_assert(is_trivially_copyable<T>::value);
+    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) { return m_a[p.to_int()]; }
+
+    const T& operator[](const Point& p) const { return m_a[p.to_int()]; }
+
+    /** 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 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/libboardgame_base/IntervalChecker.cpp b/libboardgame_base/IntervalChecker.cpp
new file mode 100644 (file)
index 0000000..6837c50
--- /dev/null
@@ -0,0 +1,79 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/IntervalChecker.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "IntervalChecker.h"
+
+#include <limits>
+#include "Assert.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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_base
diff --git a/libboardgame_base/IntervalChecker.h b/libboardgame_base/IntervalChecker.h
new file mode 100644 (file)
index 0000000..908fb76
--- /dev/null
@@ -0,0 +1,79 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/IntervalChecker.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_INTERVAL_CHECKER_H
+#define LIBBOARDGAME_BASE_INTERVAL_CHECKER_H
+
+#include <functional>
+#include "TimeSource.h"
+
+namespace libboardgame_base {
+
+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 The time source. The lifetime of this
+        parameter must exceed the lifetime of the class instance.
+        @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_base
+
+#endif // LIBBOARDGAME_BASE_INTERVAL_CHECKER_H
diff --git a/libboardgame_base/Log.cpp b/libboardgame_base/Log.cpp
new file mode 100644 (file)
index 0000000..908e392
--- /dev/null
@@ -0,0 +1,121 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/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_base {
+
+//-----------------------------------------------------------------------------
+
+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 constexpr 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_base
+
+//-----------------------------------------------------------------------------
+
+#endif // ! LIBBOARDGAME_DISABLE_LOG
diff --git a/libboardgame_base/Log.h b/libboardgame_base/Log.h
new file mode 100644 (file)
index 0000000..cdfe4c4
--- /dev/null
@@ -0,0 +1,115 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Log.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_LOG_H
+#define LIBBOARDGAME_BASE_LOG_H
+
+#include <sstream>
+#include <string>
+
+namespace libboardgame_base {
+
+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();
+
+/** 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;
+    (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_base
+
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_DISABLE_LOG
+#define LIBBOARDGAME_LOG(...) libboardgame_base::_log(__VA_ARGS__)
+#else
+#define LIBBOARDGAME_LOG(...) (static_cast<void>(0))
+#endif
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBBOARDGAME_BASE_LOG_H
diff --git a/libboardgame_base/Marker.h b/libboardgame_base/Marker.h
new file mode 100644 (file)
index 0000000..935b975
--- /dev/null
@@ -0,0 +1,91 @@
+//-----------------------------------------------------------------------------
+/** @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 Point */
+template<class P>
+class Marker
+{
+public:
+    using Point = P;
+
+
+    Marker() { reset(); }
+
+    void clear();
+
+    /** Mark a point.
+        @return true if the point was already marked. */
+    bool set(Point p);
+
+    bool operator[](Point p) const { return m_a[p.to_int()] == m_current; }
+
+    /** 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 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/libboardgame_base/MathUtil.h b/libboardgame_base/MathUtil.h
new file mode 100644 (file)
index 0000000..3bd2934
--- /dev/null
@@ -0,0 +1,41 @@
+//----------------------------------------------------------------------------
+/** @file libboardgame_base/MathUtil.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_MATH_UTIL_H
+#define LIBBOARDGAME_BASE_MATH_UTIL_H
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+/** 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_base
+
+#endif // LIBBOARDGAME_BASE_MATH_UTIL_H
diff --git a/libboardgame_base/Memory.cpp b/libboardgame_base/Memory.cpp
new file mode 100644 (file)
index 0000000..a20a888
--- /dev/null
@@ -0,0 +1,51 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/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
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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);
+
+#else
+
+#error "Determining memory size on this platform not (yet) supported"
+
+#endif
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
diff --git a/libboardgame_base/Memory.h b/libboardgame_base/Memory.h
new file mode 100644 (file)
index 0000000..251e965
--- /dev/null
@@ -0,0 +1,24 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Memory.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_MEMORY_H
+#define LIBBOARDGAME_BASE_MEMORY_H
+
+#include <cstddef>
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+/** Get the physical memory available on the system.
+    @return The memory in bytes or 0 if the memory could not be determined. */
+std::size_t get_memory();
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_MEMORY_H
diff --git a/libboardgame_base/Options.cpp b/libboardgame_base/Options.cpp
new file mode 100644 (file)
index 0000000..1470f4c
--- /dev/null
@@ -0,0 +1,155 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Options.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Options.h"
+
+namespace libboardgame_base {
+
+//----------------------------------------------------------------------------
+
+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({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({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_base
diff --git a/libboardgame_base/Options.h b/libboardgame_base/Options.h
new file mode 100644 (file)
index 0000000..da560f2
--- /dev/null
@@ -0,0 +1,118 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Options.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_OPTIONS_H
+#define LIBBOARDGAME_BASE_OPTIONS_H
+
+#include <map>
+#include <set>
+#include <stdexcept>
+#include <string>
+#include <vector>
+#include "Compiler.h"
+#include "StringUtil.h"
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//----------------------------------------------------------------------------
+
+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 { return m_args; }
+
+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);
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_OPTIONS_H
diff --git a/libboardgame_base/Point.h b/libboardgame_base/Point.h
new file mode 100644 (file)
index 0000000..7d26db8
--- /dev/null
@@ -0,0 +1,144 @@
+//-----------------------------------------------------------------------------
+/** @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 "Assert.h"
+#include "Compiler.h"
+
+namespace libboardgame_base {
+
+using namespace std;
+using namespace libboardgame_base;
+
+//-----------------------------------------------------------------------------
+
+/** 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 constexpr unsigned range_onboard = M;
+
+    static constexpr unsigned max_width = W;
+
+    static constexpr 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 constexpr unsigned range = range_onboard + 1;
+
+
+    static Point null() { return Point(value_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 constexpr IntType value_uninitialized = range;
+
+    static constexpr 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>::to_int() const -> IntType
+{
+    LIBBOARDGAME_ASSERT(is_initialized());
+    return m_i;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_POINT_H
diff --git a/libboardgame_base/PointTransform.h b/libboardgame_base/PointTransform.h
new file mode 100644 (file)
index 0000000..e1939c9
--- /dev/null
@@ -0,0 +1,409 @@
+//-----------------------------------------------------------------------------
+/** @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"
+
+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, [[maybe_unused]] const Geometry<P>& geo) const
+{
+    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/libboardgame_base/RandomGenerator.cpp b/libboardgame_base/RandomGenerator.cpp
new file mode 100644 (file)
index 0000000..40c0178
--- /dev/null
@@ -0,0 +1,70 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/RandomGenerator.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "RandomGenerator.h"
+
+#include <list>
+
+namespace libboardgame_base {
+
+//----------------------------------------------------------------------------
+
+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_base
diff --git a/libboardgame_base/RandomGenerator.h b/libboardgame_base/RandomGenerator.h
new file mode 100644 (file)
index 0000000..33c2476
--- /dev/null
@@ -0,0 +1,92 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/RandomGenerator.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_RANDOM_GENERATOR_H
+#define LIBBOARDGAME_BASE_RANDOM_GENERATOR_H
+
+#include <random>
+
+namespace libboardgame_base {
+
+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.
+    Thread-safe 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) { m_generator.seed(seed); }
+
+    ResultType generate() { return m_generator(); }
+
+    /** 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 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);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_RANDOM_GENERATOR_H
diff --git a/libboardgame_base/Range.h b/libboardgame_base/Range.h
new file mode 100644 (file)
index 0000000..ccff665
--- /dev/null
@@ -0,0 +1,56 @@
+//----------------------------------------------------------------------------
+/** @file libboardgame_base/Range.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_RANGE_H
+#define LIBBOARDGAME_BASE_RANGE_H
+
+#include <cstddef>
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+template<typename T>
+class Range
+{
+public:
+    Range() = default;
+
+    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_base
+
+#endif // LIBBOARDGAME_BASE_RANGE_H
diff --git a/libboardgame_base/Rating.cpp b/libboardgame_base/Rating.cpp
new file mode 100644 (file)
index 0000000..deda597
--- /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 "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/libboardgame_base/Rating.h b/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/libboardgame_base/Reader.cpp b/libboardgame_base/Reader.cpp
new file mode 100644 (file)
index 0000000..2077d9c
--- /dev/null
@@ -0,0 +1,242 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Reader.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Reader.h"
+
+#include <cctype>
+#include <cstdio>
+#include <fstream>
+#include "Assert.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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
+
+//-----------------------------------------------------------------------------
+
+Reader::~Reader() = default; // Non-inline to avoid GCC -Winline warning
+
+void Reader::consume_char([[maybe_unused]] char expected)
+{
+    [[maybe_unused]] char c = read_char();
+    LIBBOARDGAME_ASSERT(c == expected);
+}
+
+void Reader::consume_whitespace()
+{
+    while (is_ascii_space(peek()))
+        m_in->get();
+}
+
+void Reader::on_begin_node([[maybe_unused]] bool is_root)
+{
+    // Default implementation does nothing
+}
+
+void Reader::on_begin_tree([[maybe_unused]] bool is_root)
+{
+    // Default implementation does nothing
+}
+
+void Reader::on_end_node()
+{
+    // Default implementation does nothing
+}
+
+void Reader::on_end_tree([[maybe_unused]] bool is_root)
+{
+    // Default implementation does nothing
+}
+
+void Reader::on_property([[maybe_unused]] const string& id,
+                         [[maybe_unused]] const vector<string>& values)
+{
+    // Default implementation does nothing
+}
+
+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_base
diff --git a/libboardgame_base/Reader.h b/libboardgame_base/Reader.h
new file mode 100644 (file)
index 0000000..37b759d
--- /dev/null
@@ -0,0 +1,105 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Reader.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_READER_H
+#define LIBBOARDGAME_BASE_READER_H
+
+#include <iosfwd>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+class Reader
+{
+public:
+    class ReadError
+        : public runtime_error
+    {
+        using runtime_error::runtime_error;
+    };
+
+
+    virtual ~Reader();
+
+
+    virtual void on_begin_tree(bool is_root);
+
+    virtual void on_end_tree(bool is_root);
+
+    virtual void on_begin_node(bool is_root);
+
+    virtual void on_end_node();
+
+    virtual void on_property(const string& id, const vector<string>& values);
+
+    /** Read only the main variation.
+        Reduces CPU time and memory if only the main variation is needed. */
+    void set_read_only_main_variation(bool enable);
+
+    /** Read a game tree from 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_base
+
+#endif // LIBBOARDGAME_BASE_READER_H
diff --git a/libboardgame_base/RectGeometry.h b/libboardgame_base/RectGeometry.h
new file mode 100644 (file)
index 0000000..498531e
--- /dev/null
@@ -0,0 +1,127 @@
+//-----------------------------------------------------------------------------
+/** @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"
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Geometry of a regular rectangular grid.
+    @tparam P An instantiation of 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;
+
+
+    /** Create or reuse an already created geometry with a given size. */
+    static const RectGeometry& get(unsigned width, unsigned height);
+
+
+    RectGeometry(unsigned width, unsigned height);
+
+    AdjCoordList get_adj_coord(int x, int y) const override;
+
+    DiagCoordList get_diag_coord(int x, int y) const override;
+
+    unsigned get_point_type(int x, int y) const override;
+
+    unsigned get_period_x() const override;
+
+    unsigned get_period_y() const override;
+
+protected:
+    bool init_is_onboard(unsigned x, unsigned y) const override;
+};
+
+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;
+
+    pair key(width, height);
+    auto pos = s_geometry.find(key);
+    if (pos != s_geometry.end())
+        return *pos->second;
+    auto geometry = make_shared<RectGeometry>(width, height);
+    s_geometry.insert({key, geometry});
+    return *geometry;
+}
+
+template<class P>
+auto RectGeometry<P>::get_adj_coord(int x, int y) const -> AdjCoordList
+{
+    AdjCoordList l;
+    l.push_back({x, y - 1});
+    l.push_back({x - 1, y});
+    l.push_back({x + 1, y});
+    l.push_back({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({x - 1, y - 1});
+    l.push_back({x + 1, y + 1});
+    l.push_back({x + 1, y - 1});
+    l.push_back({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(
+        [[maybe_unused]] int x, [[maybe_unused]] int y) const
+{
+    return 0;
+}
+
+template<class P>
+bool RectGeometry<P>::init_is_onboard(
+        [[maybe_unused]] unsigned x, [[maybe_unused]] unsigned y) const
+{
+    return true;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_RECT_GEOMETRY_H
diff --git a/libboardgame_base/RectTransform.cpp b/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/libboardgame_base/RectTransform.h b/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/libboardgame_base/SgfError.cpp b/libboardgame_base/SgfError.cpp
new file mode 100644 (file)
index 0000000..2d0f8c5
--- /dev/null
@@ -0,0 +1,22 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/SgfError.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "SgfError.h"
+
+#include <string>
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+MissingProperty::MissingProperty(const string& id)
+    : SgfError("Missing SGF property '" + id + "'")
+{
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
diff --git a/libboardgame_base/SgfError.h b/libboardgame_base/SgfError.h
new file mode 100644 (file)
index 0000000..dbe70a6
--- /dev/null
@@ -0,0 +1,73 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/SgfError.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_SGF_ERROR_H
+#define LIBBOARDGAME_BASE_SGF_ERROR_H
+
+#include <sstream>
+#include <stdexcept>
+
+namespace libboardgame_base {
+
+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_base
+
+#endif // LIBBOARDGAME_BASE_SGF_ERROR_H
diff --git a/libboardgame_base/SgfNode.cpp b/libboardgame_base/SgfNode.cpp
new file mode 100644 (file)
index 0000000..45a12c2
--- /dev/null
@@ -0,0 +1,287 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/SgfNode.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "SgfNode.h"
+
+#include <algorithm>
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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;
+    auto& 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)
+        {
+            auto 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();
+    auto 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)
+    {
+        auto 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;
+            auto 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;
+            }
+            auto 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)
+{
+    auto 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)
+        {
+            auto 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_base
diff --git a/libboardgame_base/SgfNode.h b/libboardgame_base/SgfNode.h
new file mode 100644 (file)
index 0000000..90c0c34
--- /dev/null
@@ -0,0 +1,324 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/SgfNode.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_SGF_NODE_H
+#define LIBBOARDGAME_BASE_SGF_NODE_H
+
+#include <forward_list>
+#include <memory>
+#include <string>
+#include <vector>
+#include "SgfError.h"
+#include "Assert.h"
+#include "StringUtil.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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() { return m_sibling.get(); }
+
+    SgfNode& get_first_child();
+
+    const SgfNode& get_first_child() const;
+
+    SgfNode* get_first_child_or_null() { return m_first_child.get(); }
+
+    const SgfNode* get_first_child_or_null() const {
+        return m_first_child.get(); }
+
+    const SgfNode* get_sibling() const { return m_sibling.get(); }
+
+    const SgfNode* get_previous_sibling() const;
+
+    bool has_children() const { return static_cast<bool>(m_first_child); }
+
+    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 { return m_parent != nullptr; }
+
+    /** 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 { return m_parent; }
+
+    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() { m_first_child.reset(); }
+
+    /** @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 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 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);
+}
+
+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));
+    auto 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_base
+
+#endif // LIBBOARDGAME_BASE_SGF_NODE_H
diff --git a/libboardgame_base/SgfTree.cpp b/libboardgame_base/SgfTree.cpp
new file mode 100644 (file)
index 0000000..d786e69
--- /dev/null
@@ -0,0 +1,262 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/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 "StringUtil.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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_base
diff --git a/libboardgame_base/SgfTree.h b/libboardgame_base/SgfTree.h
new file mode 100644 (file)
index 0000000..5939de9
--- /dev/null
@@ -0,0 +1,240 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/SgfTree.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_SGF_TREE_H
+#define LIBBOARDGAME_BASE_SGF_TREE_H
+
+#include "SgfNode.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+/** 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 { return m_modified; }
+
+    void set_modified(bool is_modified = true) { m_modified = is_modified; }
+
+    void clear_modified() { m_modified = false; }
+
+    const SgfNode& get_root() const { return *m_root; }
+
+    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 { return m_root->get_property("DT", ""); }
+
+    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 { return m_root->get_property("EV", ""); }
+
+    void set_event(const string& event);
+
+    string get_round() const { return m_root->get_property("RO", ""); }
+
+    void set_round(const string& round);
+
+    string get_time() const { return m_root->get_property("TM", ""); }
+
+    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 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);
+}
+
+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_base
+
+#endif // LIBBOARDGAME_BASE_SGF_TREE_H
diff --git a/libboardgame_base/SgfUtil.cpp b/libboardgame_base/SgfUtil.cpp
new file mode 100644 (file)
index 0000000..e2bebaf
--- /dev/null
@@ -0,0 +1,192 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/SgfUtil.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "SgfUtil.h"
+
+#include <algorithm>
+#include <sstream>
+#include "StringUtil.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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_base
diff --git a/libboardgame_base/SgfUtil.h b/libboardgame_base/SgfUtil.h
new file mode 100644 (file)
index 0000000..da7ecc1
--- /dev/null
@@ -0,0 +1,70 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/SgfUtil.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_SGF_UTIL_H
+#define LIBBOARDGAME_BASE_SGF_UTIL_H
+
+#include <string>
+#include "SgfTree.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+/** 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_base
+
+#endif // LIBBOARDGAME_BASE_SGF_UTIL_H
diff --git a/libboardgame_base/Statistics.h b/libboardgame_base/Statistics.h
new file mode 100644 (file)
index 0000000..33c012b
--- /dev/null
@@ -0,0 +1,334 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Statistics.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_STATISTICS_H
+#define LIBBOARDGAME_BASE_STATISTICS_H
+
+#include <atomic>
+#include <cmath>
+#include <iomanip>
+#include <iosfwd>
+#include <limits>
+#include <sstream>
+#include <string>
+#include "FmtSaver.h"
+
+namespace libboardgame_base {
+
+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) { clear(init_val); }
+
+    void add(FLOAT val);
+
+    void clear(FLOAT init_val = 0);
+
+    FLOAT get_count() const { return m_count; }
+
+    FLOAT get_mean() const { return m_mean; }
+
+    void write(ostream& out, bool fixed = false, int precision = 6) const;
+
+private:
+    FLOAT m_count;
+
+    FLOAT m_mean;
+};
+
+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>
+void StatisticsBase<FLOAT>::write(ostream& out, bool fixed,
+                                  int 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) { clear(init_val); }
+
+    void add(FLOAT val);
+
+    void clear(FLOAT init_val = 0);
+
+    FLOAT get_mean() const { return m_statistics_base.get_mean(); }
+
+    FLOAT get_count() const { return m_statistics_base.get_count(); }
+
+    FLOAT get_deviation() const;
+
+    FLOAT get_error() const;
+
+    FLOAT get_variance() const { return m_variance; }
+
+    void write(ostream& out, bool fixed = false, int precision = 6) const;
+
+private:
+    StatisticsBase<FLOAT> m_statistics_base;
+
+    FLOAT m_variance;
+};
+
+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_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>
+void Statistics<FLOAT>::write(ostream& out, bool fixed, int precision) const
+{
+    FmtSaver saver(out);
+    if (fixed)
+        out << std::fixed;
+    out << setprecision(precision) << get_mean() << u8" σ="
+        << get_deviation();
+}
+
+//----------------------------------------------------------------------------
+
+template<typename FLOAT = double>
+class StatisticsExt
+{
+public:
+    explicit StatisticsExt(FLOAT init_val = 0) { clear(init_val); }
+
+    void add(FLOAT val);
+
+    void clear(FLOAT init_val = 0);
+
+    FLOAT get_mean() const { return m_statistics.get_mean(); }
+
+    FLOAT get_error() const { return m_statistics.get_error(); }
+
+    FLOAT get_count() const { return m_statistics.get_count(); }
+
+    FLOAT get_max() const { return m_max; }
+
+    FLOAT get_min() const { return m_min; }
+
+    FLOAT get_deviation() const { return m_statistics.get_deviation(); }
+
+    FLOAT get_variance() const { return m_statistics.get_variance(); }
+
+    void write(ostream& out, bool fixed = false, int precision = 6,
+               bool integer_values = false, bool with_error = false) const;
+
+    string to_string(bool fixed = false, int 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>
+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>
+string StatisticsExt<FLOAT>::to_string(bool fixed, int 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, int 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 << u8"±" << get_error();
+    out << u8" σ=" << get_deviation();
+    if (m_min != numeric_limits<FLOAT>::max()
+            && m_max != -numeric_limits<FLOAT>::max() && m_min != m_max)
+    {
+        if (integer_values)
+            out << setprecision(0);
+        out << " [" << m_min << u8"…" << 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 StatisticsDirty
+{
+public:
+    /** Constructor.
+        @param init_val See StatisticBase::StatisticBase() */
+    explicit StatisticsDirty(FLOAT init_val = 0) { clear(init_val); }
+
+    StatisticsDirty& operator=(const StatisticsDirty& s);
+
+    void add(FLOAT val, FLOAT weight = 1);
+
+    void clear(FLOAT init_val = 0) { init(init_val, 0); }
+
+    void init(FLOAT mean, FLOAT count);
+
+    FLOAT get_count() const { return m_count.load(memory_order_relaxed); }
+
+    FLOAT get_mean() const { return m_mean.load(memory_order_relaxed); }
+
+    void write(ostream& out, bool fixed = false, int precision = 6) const;
+
+private:
+    atomic<FLOAT> m_count;
+
+    atomic<FLOAT> m_mean;
+};
+
+template<typename FLOAT>
+StatisticsDirty<FLOAT>&
+StatisticsDirty<FLOAT>::operator=(const StatisticsDirty& s)
+{
+    m_count = s.m_count.load();
+    m_mean = s.m_mean.load();
+    return *this;
+}
+
+template<typename FLOAT>
+void StatisticsDirty<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 StatisticsDirty<FLOAT>::init(FLOAT mean, FLOAT count)
+{
+    m_count = count;
+    m_mean = mean;
+}
+
+template<typename FLOAT>
+void StatisticsDirty<FLOAT>::write(ostream& out, bool fixed,
+                                           int 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_base
+
+#endif // LIBBOARDGAME_BASE_STATISTICS_H
diff --git a/libboardgame_base/StringRep.cpp b/libboardgame_base/StringRep.cpp
new file mode 100644 (file)
index 0000000..fef7bf1
--- /dev/null
@@ -0,0 +1,68 @@
+//-----------------------------------------------------------------------------
+/** @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 "StringUtil.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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, [[maybe_unused]] unsigned width,
+        unsigned height) const
+{
+    out << get_letter_coord(x) << (height - y);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
diff --git a/libboardgame_base/StringRep.h b/libboardgame_base/StringRep.h
new file mode 100644 (file)
index 0000000..3cbfc26
--- /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 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/libboardgame_base/StringUtil.cpp b/libboardgame_base/StringUtil.cpp
new file mode 100644 (file)
index 0000000..310cb38
--- /dev/null
@@ -0,0 +1,94 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/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_base {
+
+//-----------------------------------------------------------------------------
+
+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);
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
diff --git a/libboardgame_base/StringUtil.h b/libboardgame_base/StringUtil.h
new file mode 100644 (file)
index 0000000..c6e81cb
--- /dev/null
@@ -0,0 +1,56 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/StringUtil.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_STRING_UTIL_H
+#define LIBBOARDGAME_BASE_STRING_UTIL_H
+
+#include <sstream>
+#include <string>
+#include <vector>
+
+namespace libboardgame_base {
+
+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);
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_STRING_UTIL_H
diff --git a/libboardgame_base/TimeIntervalChecker.cpp b/libboardgame_base/TimeIntervalChecker.cpp
new file mode 100644 (file)
index 0000000..7aa6f29
--- /dev/null
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/TimeIntervalChecker.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "TimeIntervalChecker.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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_base
diff --git a/libboardgame_base/TimeIntervalChecker.h b/libboardgame_base/TimeIntervalChecker.h
new file mode 100644 (file)
index 0000000..3664463
--- /dev/null
@@ -0,0 +1,41 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/TimeIntervalChecker.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_TIME_INTERVAL_CHECKER_H
+#define LIBBOARDGAME_BASE_TIME_INTERVAL_CHECKER_H
+
+#include "IntervalChecker.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+/** 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_base
+
+#endif // LIBBOARDGAME_BASE_TIME_INTERVAL_CHECKER_H
diff --git a/libboardgame_base/TimeSource.cpp b/libboardgame_base/TimeSource.cpp
new file mode 100644 (file)
index 0000000..3c825fe
--- /dev/null
@@ -0,0 +1,17 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/TimeSource.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "TimeSource.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+TimeSource::~TimeSource() = default;
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
diff --git a/libboardgame_base/TimeSource.h b/libboardgame_base/TimeSource.h
new file mode 100644 (file)
index 0000000..71d8b37
--- /dev/null
@@ -0,0 +1,32 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/TimeSource.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_TIME_SOURCE_H
+#define LIBBOARDGAME_BASE_TIME_SOURCE_H
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+/** 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). */
+class TimeSource
+{
+public:
+    virtual ~TimeSource();
+
+    /** Get the current time in seconds. */
+    virtual double operator()() = 0;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_TIME_SOURCE_H
diff --git a/libboardgame_base/Timer.cpp b/libboardgame_base/Timer.cpp
new file mode 100644 (file)
index 0000000..34b5bca
--- /dev/null
@@ -0,0 +1,32 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Timer.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Timer.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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_base
diff --git a/libboardgame_base/Timer.h b/libboardgame_base/Timer.h
new file mode 100644 (file)
index 0000000..5f83ab0
--- /dev/null
@@ -0,0 +1,53 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Timer.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_TIMER_H
+#define LIBBOARDGAME_BASE_TIMER_H
+
+#include "Assert.h"
+#include "TimeSource.h"
+
+namespace libboardgame_base {
+
+class Timer
+{
+public:
+    /** Constructor without time source.
+        If constructed without time source, the timer cannot be used before
+        reset(TimeSource&) was called. */
+    Timer() {
+#ifdef LIBBOARDGAME_DEBUG
+        m_time_source = nullptr;
+#endif
+    }
+
+    /** Constructor with time_source.
+        @param time_source The time source. The lifetime of this
+        parameter must exceed the lifetime of the class instance. */
+    explicit Timer(TimeSource& time_source) { reset(time_source); }
+
+    /** Get time since construction or last reset */
+    double operator()() const;
+
+    /** Reset timer. */
+    void reset();
+
+    /** Set time source and reset timer.
+        @param time_source The time source. The lifetime of this
+        parameter must exceed the lifetime of the class instance. */
+    void reset(TimeSource& time_source);
+
+private:
+    double m_start;
+
+    TimeSource* m_time_source;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_TIMER_H
diff --git a/libboardgame_base/Transform.cpp b/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/libboardgame_base/Transform.h b/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/libboardgame_base/TreeReader.cpp b/libboardgame_base/TreeReader.cpp
new file mode 100644 (file)
index 0000000..09669f4
--- /dev/null
@@ -0,0 +1,61 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/TreeReader.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "TreeReader.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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_base
diff --git a/libboardgame_base/TreeReader.h b/libboardgame_base/TreeReader.h
new file mode 100644 (file)
index 0000000..74bb96f
--- /dev/null
@@ -0,0 +1,55 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/TreeReader.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_TREE_READER_H
+#define LIBBOARDGAME_BASE_TREE_READER_H
+
+#include <memory>
+#include <stack>
+#include "Reader.h"
+#include "SgfNode.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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_base
+
+#endif // LIBBOARDGAME_BASE_TREE_READER_H
diff --git a/libboardgame_base/TreeWriter.cpp b/libboardgame_base/TreeWriter.cpp
new file mode 100644 (file)
index 0000000..91bd92f
--- /dev/null
@@ -0,0 +1,52 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/TreeWriter.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "TreeWriter.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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_base
diff --git a/libboardgame_base/TreeWriter.h b/libboardgame_base/TreeWriter.h
new file mode 100644 (file)
index 0000000..50382de
--- /dev/null
@@ -0,0 +1,62 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/TreeWriter.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_TREE_WRITER_H
+#define LIBBOARDGAME_BASE_TREE_WRITER_H
+
+#include "SgfNode.h"
+#include "Writer.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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) {
+        m_writer.set_one_prop_per_line(enable);
+    }
+
+    void set_one_prop_value_per_line(bool enable) {
+        m_writer.set_one_prop_value_per_line(enable);
+    }
+
+    void set_indent(int indent) { m_writer.set_indent(indent); }
+
+    /** @} */ // @name
+
+
+    void write();
+
+private:
+    const SgfNode& m_root;
+
+    Writer m_writer;
+
+    void write_node(const SgfNode& node);
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_TREE_WRITER_H
diff --git a/libboardgame_base/WallTimeSource.cpp b/libboardgame_base/WallTimeSource.cpp
new file mode 100644 (file)
index 0000000..5fee7fb
--- /dev/null
@@ -0,0 +1,25 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/WallTimeSource.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "WallTimeSource.h"
+
+#include <chrono>
+
+namespace libboardgame_base {
+
+using namespace std::chrono;
+
+//-----------------------------------------------------------------------------
+
+double WallTimeSource::operator()()
+{
+    auto t = system_clock::now().time_since_epoch();
+    return duration_cast<duration<double>>(t).count();
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
diff --git a/libboardgame_base/WallTimeSource.h b/libboardgame_base/WallTimeSource.h
new file mode 100644 (file)
index 0000000..907afe1
--- /dev/null
@@ -0,0 +1,27 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/WallTimeSource.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_WALL_TIME_SOURCE_H
+#define LIBBOARDGAME_BASE_WALL_TIME_SOURCE_H
+
+#include "TimeSource.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+/** Wall time. */
+class WallTimeSource
+    : public TimeSource
+{
+public:
+    double operator()() override;
+};
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_WALL_TIME_SOURCE_H
diff --git a/libboardgame_base/Writer.cpp b/libboardgame_base/Writer.cpp
new file mode 100644 (file)
index 0000000..c33610e
--- /dev/null
@@ -0,0 +1,80 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Writer.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Writer.h"
+
+#include <sstream>
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+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_base
diff --git a/libboardgame_base/Writer.h b/libboardgame_base/Writer.h
new file mode 100644 (file)
index 0000000..72f3685
--- /dev/null
@@ -0,0 +1,125 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Writer.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_WRITER_H
+#define LIBBOARDGAME_BASE_WRITER_H
+
+#include <iosfwd>
+#include <string>
+#include <vector>
+#include "StringUtil.h"
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+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) { m_one_prop_per_line = enable; }
+
+    void set_one_prop_value_per_line(bool enable) {
+        m_one_prop_value_per_line = enable;
+    }
+
+    /** @param indent The number of spaces to indent subtrees, -1 means
+        to not even use newlines. */
+    void set_indent(int indent) { m_indent = 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::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_base
+
+#endif // LIBBOARDGAME_BASE_WRITER_H
diff --git a/libboardgame_base/tests/ArrayListTest.cpp b/libboardgame_base/tests/ArrayListTest.cpp
new file mode 100644 (file)
index 0000000..877a949
--- /dev/null
@@ -0,0 +1,63 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/ArrayListTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/ArrayList.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_base;
+
+//-----------------------------------------------------------------------------
+
+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/libboardgame_base/tests/CMakeLists.txt b/libboardgame_base/tests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..c11fded
--- /dev/null
@@ -0,0 +1,22 @@
+add_executable(test_libboardgame_base
+    ArrayListTest.cpp
+    MarkerTest.cpp
+    OptionsTest.cpp
+    PointTransformTest.cpp
+    RatingTest.cpp
+    RectGeometryTest.cpp
+    SgfNodeTest.cpp
+    SgfTreeTest.cpp
+    SgfUtilTest.cpp
+    StatisticsTest.cpp
+    StringRepTest.cpp
+    StringUtilTest.cpp
+    TreeReaderTest.cpp
+    )
+
+target_link_libraries(test_libboardgame_base
+    boardgame_test_main
+    boardgame_base
+    )
+
+add_test(libboardgame_base test_libboardgame_base)
diff --git a/libboardgame_base/tests/MarkerTest.cpp b/libboardgame_base/tests/MarkerTest.cpp
new file mode 100644 (file)
index 0000000..5d80091
--- /dev/null
@@ -0,0 +1,57 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/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/libboardgame_base/tests/OptionsTest.cpp b/libboardgame_base/tests/OptionsTest.cpp
new file mode 100644 (file)
index 0000000..627f3de
--- /dev/null
@@ -0,0 +1,87 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/OptionsTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/Options.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_base;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(libboardgame_base_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_base_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_base_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_base_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_base_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_base_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/libboardgame_base/tests/PointTransformTest.cpp b/libboardgame_base/tests/PointTransformTest.cpp
new file mode 100644 (file)
index 0000000..85f1192
--- /dev/null
@@ -0,0 +1,42 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/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/libboardgame_base/tests/RatingTest.cpp b/libboardgame_base/tests/RatingTest.cpp
new file mode 100644 (file)
index 0000000..f2d566d
--- /dev/null
@@ -0,0 +1,66 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/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/libboardgame_base/tests/RectGeometryTest.cpp b/libboardgame_base/tests/RectGeometryTest.cpp
new file mode 100644 (file)
index 0000000..c3a48b6
--- /dev/null
@@ -0,0 +1,97 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/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_base::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/libboardgame_base/tests/SgfNodeTest.cpp b/libboardgame_base/tests/SgfNodeTest.cpp
new file mode 100644 (file)
index 0000000..c676e02
--- /dev/null
@@ -0,0 +1,37 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/SgfNodeTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include <memory>
+#include "libboardgame_base/SgfNode.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_base;
+
+//-----------------------------------------------------------------------------
+
+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/libboardgame_base/tests/SgfTreeTest.cpp b/libboardgame_base/tests/SgfTreeTest.cpp
new file mode 100644 (file)
index 0000000..aa83c2a
--- /dev/null
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/SgfTreeTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/SgfTree.h"
+#include "libboardgame_test/Test.h"
+
+using namespace libboardgame_base;
+
+//-----------------------------------------------------------------------------
+
+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/libboardgame_base/tests/SgfUtilTest.cpp b/libboardgame_base/tests/SgfUtilTest.cpp
new file mode 100644 (file)
index 0000000..1f0a5e3
--- /dev/null
@@ -0,0 +1,27 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/SgfUtilTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/SgfUtil.h"
+
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_base;
+
+//-----------------------------------------------------------------------------
+
+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/libboardgame_base/tests/StatisticsTest.cpp b/libboardgame_base/tests/StatisticsTest.cpp
new file mode 100644 (file)
index 0000000..13130a4
--- /dev/null
@@ -0,0 +1,29 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/StatisticsTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/Statistics.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_base;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(libboardgame_base_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/libboardgame_base/tests/StringRepTest.cpp b/libboardgame_base/tests/StringRepTest.cpp
new file mode 100644 (file)
index 0000000..90c9d65
--- /dev/null
@@ -0,0 +1,80 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/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/libboardgame_base/tests/StringUtilTest.cpp b/libboardgame_base/tests/StringUtilTest.cpp
new file mode 100644 (file)
index 0000000..30bb59b
--- /dev/null
@@ -0,0 +1,85 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/StringUtilTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/StringUtil.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_base;
+
+//----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(libboardgame_base_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_base_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_base_to_lower)
+{
+    LIBBOARDGAME_CHECK_EQUAL(to_lower("AabC "), "aabc ");
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_base_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(""), "");
+}
+
+//----------------------------------------------------------------------------
diff --git a/libboardgame_base/tests/TreeReaderTest.cpp b/libboardgame_base/tests/TreeReaderTest.cpp
new file mode 100644 (file)
index 0000000..dd73f07
--- /dev/null
@@ -0,0 +1,133 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/tests/TreeReaderTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/TreeReader.h"
+
+#include <sstream>
+#include "libboardgame_base/TreeWriter.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_base;
+
+//-----------------------------------------------------------------------------
+
+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 = u8"ü";
+    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/libboardgame_gtp/Arguments.cpp b/libboardgame_gtp/Arguments.cpp
new file mode 100644 (file)
index 0000000..7584047
--- /dev/null
@@ -0,0 +1,76 @@
+//-----------------------------------------------------------------------------
+/** @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());
+}
+
+template<>
+string_view 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());
+}
+
+template<>
+string Arguments::get(unsigned i) const
+{
+    auto s = get(i);
+    return string(&*s.begin(), s.size());
+}
+
+string Arguments::get_tolower(unsigned i) const
+{
+    auto value = get<string>(i);
+    for (auto& c : value)
+        c = static_cast<char>(tolower(c));
+    return value;
+}
+
+string_view 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/libboardgame_gtp/Arguments.h b/libboardgame_gtp/Arguments.h
new file mode 100644 (file)
index 0000000..4d20d29
--- /dev/null
@@ -0,0 +1,173 @@
+//-----------------------------------------------------------------------------
+/** @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
+
+#if defined __GNUC__ && __has_include(<cxxabi.h>)
+#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. The lifetime of this parameter
+        must exceed the lifetime of the class instance. */
+    explicit Arguments(const CmdLine& line) : m_line(line) { }
+
+    /** Get argument.        
+        @param i Argument index starting with 0
+        @return Argument value        
+        @throws Failure If no such argument
+        @tparam T The type the argument should be converted to. The type
+        must implement operator<< */
+    template<typename T = string_view>
+    T get(unsigned i) const;
+
+    /** Get single argument.        
+        Like get(unsigned) but throws if there is not exactly one argument. */
+    template<typename T = string_view>
+    T 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 argument and check against a minimum value.
+        Like get(unsigned) but throws if the argument is less than the minimum
+        value. */
+    template<typename T>
+    T get_min(unsigned i, T min) const;
+
+    /** Check that command has no arguments.
+        @throws Failure If command has arguments
+    */
+    void check_empty() const { check_size(0); }
+
+    /** 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. */
+    string_view 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 */
+    string_view get_remaining_line(unsigned i) const;
+
+private:
+    const CmdLine& m_line;
+
+    template<typename T>
+    static string get_type_name();
+};
+
+inline string_view 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()
+{
+#if defined __GNUC__ && __has_include(<cxxabi.h>)
+    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::get() const
+{
+    check_size(1);
+    return get<T>(0);
+}
+
+template<>
+string_view Arguments::get(unsigned i) const;
+
+template<>
+string Arguments::get(unsigned i) const;
+
+template<typename T>
+T Arguments::get(unsigned i) const
+{
+    auto s = get<string>(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::get_min(unsigned i, T min) const
+{
+    auto result = get<T>(i);
+    if (result < min)
+    {
+        ostringstream msg;
+        msg << "argument " << (i + 1) << " must be greater or equal " << min;
+        throw Failure(msg.str());
+    }
+    return result;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
+
+#endif // LIBBOARDGAME_GTP_ARGUMENTS_H
diff --git a/libboardgame_gtp/CMakeLists.txt b/libboardgame_gtp/CMakeLists.txt
new file mode 100644 (file)
index 0000000..17c2385
--- /dev/null
@@ -0,0 +1,17 @@
+add_library(boardgame_gtp STATIC
+  Arguments.h
+  Arguments.cpp
+  CmdLine.h
+  CmdLine.cpp
+  GtpEngine.h
+  GtpEngine.cpp
+  Failure.h
+  Response.h
+  Response.cpp
+)
+
+target_include_directories(boardgame_gtp PUBLIC ..)
+
+if(BUILD_TESTING)
+    add_subdirectory(tests)
+endif()
diff --git a/libboardgame_gtp/CmdLine.cpp b/libboardgame_gtp/CmdLine.cpp
new file mode 100644 (file)
index 0000000..a34b18e
--- /dev/null
@@ -0,0 +1,116 @@
+//-----------------------------------------------------------------------------
+/** @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 - begin);
+}
+
+/** 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);
+            begin = i + 1;
+        }
+        escape = (c == '\\' && ! escape);
+    }
+    if (i > begin)
+        m_elem.emplace_back(&*begin, m_line.end() - begin);
+}
+
+string_view CmdLine::get_trimmed_line_after_elem(unsigned i) const
+{
+    assert(i < m_elem.size());
+    auto& e = m_elem[i];
+    auto begin = &*e.end();
+    auto end = &*m_line.end();
+    if (begin < end && *begin == '"')
+        ++begin;
+    while (begin < end && isspace(static_cast<unsigned char>(*begin)) != 0)
+        ++begin;
+    while (end > begin && isspace(static_cast<unsigned char>(*(end - 1))) != 0)
+        --end;
+    return {begin, static_cast<string_view::size_type>(end - begin)};
+}
+
+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 - begin);
+    }
+}
+
+void CmdLine::parse_id()
+{
+    m_idx_name = 0;
+    if (m_elem.size() < 2)
+        return;
+    istringstream in(string(&*m_elem[0].begin(), m_elem[0].size()));
+    int id;
+    in >> id;
+    if (in)
+        m_idx_name = 1;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
diff --git a/libboardgame_gtp/CmdLine.h b/libboardgame_gtp/CmdLine.h
new file mode 100644 (file)
index 0000000..bb9dce4
--- /dev/null
@@ -0,0 +1,92 @@
+//-----------------------------------------------------------------------------
+/** @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 <cassert>
+#include <iostream>
+#include <string>
+#include <string_view>
+#include <vector>
+
+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. */
+    string_view get_name() const { return m_elem[m_idx_name]; }
+
+
+    void write_id(ostream& out) const;
+
+    string_view get_trimmed_line_after_elem(unsigned i) const;
+
+    const vector<string_view>& get_elements() const { return m_elem; }
+
+
+    const string_view& 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<string_view> m_elem;
+
+    void add_elem(string::const_iterator begin, string::const_iterator end);
+
+    void find_elem();
+
+    void parse_id();
+};
+
+inline const string_view& 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)
+        out << m_elem[0];
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
+
+#endif // LIBBOARDGAME_GTP_CMDLINE_H
diff --git a/libboardgame_gtp/Failure.h b/libboardgame_gtp/Failure.h
new file mode 100644 (file)
index 0000000..f729580
--- /dev/null
@@ -0,0 +1,29 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+
+//-----------------------------------------------------------------------------
+
+/** GTP failure.
+    Command handlers generate a GTP error response by throwing an instance
+    of Failure. */
+class Failure
+    : public std::runtime_error
+{
+    using runtime_error::runtime_error;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
+
+#endif // LIBBOARDGAME_GTP_FAILURE_H
diff --git a/libboardgame_gtp/GtpEngine.cpp b/libboardgame_gtp/GtpEngine.cpp
new file mode 100644 (file)
index 0000000..2ec1cd2
--- /dev/null
@@ -0,0 +1,201 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/GtpEngine.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "GtpEngine.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
+
+//-----------------------------------------------------------------------------
+
+GtpEngine::GtpEngine()
+{
+    add("known_command", &GtpEngine::cmd_known_command);
+    add("list_commands", &GtpEngine::cmd_list_commands);
+    add("quit", &GtpEngine::cmd_quit);
+}
+
+GtpEngine::~GtpEngine() = default; // Non-inline to avoid GCC -Winline warning
+
+void GtpEngine::add(const string& name, const Handler& f)
+{
+    m_handlers[name] = f;
+}
+
+void GtpEngine::add(const string& name, const HandlerNoArgs& f)
+{
+    add(name, [f](Arguments args, Response& response) {
+        args.check_empty();
+        f(response);
+    });
+}
+
+void GtpEngine::add(const string& name, const HandlerNoResponse& f)
+{
+    add(name, [f](Arguments args, Response&) {
+        f(args);
+    });
+}
+
+void GtpEngine::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 GtpEngine::cmd_known_command(Arguments args, Response& response)
+{
+    response.set(contains(args.get<string>()) ? "true" : "false");
+}
+
+/** List all known commands. */
+void GtpEngine::cmd_list_commands(Response& response)
+{
+    for (auto& i : m_handlers)
+        response << i.first << '\n';
+}
+
+/** Quit command loop. */
+void GtpEngine::cmd_quit()
+{
+    m_quit = true;
+}
+
+bool GtpEngine::contains(const string& name) const
+{
+    return m_handlers.count(name) > 0;
+}
+
+bool GtpEngine::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 GtpEngine::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 GtpEngine::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(string(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 GtpEngine::on_handle_cmd_begin()
+{
+    // Default implementation does nothing
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
diff --git a/libboardgame_gtp/GtpEngine.h b/libboardgame_gtp/GtpEngine.h
new file mode 100644 (file)
index 0000000..417b94c
--- /dev/null
@@ -0,0 +1,188 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/GtpEngine.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_GTP_GTP_ENGINE_H
+#define LIBBOARDGAME_GTP_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. */
+class GtpEngine
+{
+public:
+    using Handler = function<void(Arguments, Response&)>;
+
+    using HandlerNoArgs = function<void(Response&)>;
+
+    using HandlerNoResponse = function<void(Arguments)>;
+
+    using HandlerNoArgsNoResponse = function<void()>;
+
+
+    /** @name Command handlers */
+    /** @{ */
+    void cmd_known_command(Arguments args, Response& response);
+    void cmd_list_commands(Response& response);
+    void cmd_quit();
+    /** @} */ // @name
+
+    GtpEngine();
+
+    GtpEngine(const GtpEngine&) = delete;
+
+    GtpEngine& operator=(const GtpEngine&) const = delete;
+
+    virtual ~GtpEngine();
+
+    /** 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 GtpEngine::add(const string& name, void (T::*f)(Arguments, Response&))
+{
+    add(name, f, dynamic_cast<T*>(this));
+}
+
+template<class T>
+void GtpEngine::add(const string& name, void (T::*f)(Response&))
+{
+    add(name, f, dynamic_cast<T*>(this));
+}
+
+template<class T>
+void GtpEngine::add(const string& name, void (T::*f)(Arguments))
+{
+    add(name, f, dynamic_cast<T*>(this));
+}
+
+template<class T>
+void GtpEngine::add(const string& name, void (T::*f)())
+{
+    add(name, f, dynamic_cast<T*>(this));
+}
+
+template<class T>
+void GtpEngine::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 GtpEngine::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 GtpEngine::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 GtpEngine::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_GTP_ENGINE_H
diff --git a/libboardgame_gtp/Response.cpp b/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/libboardgame_gtp/Response.h b/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/libboardgame_gtp/tests/ArgumentsTest.cpp b/libboardgame_gtp/tests/ArgumentsTest.cpp
new file mode 100644 (file)
index 0000000..1439f1f
--- /dev/null
@@ -0,0 +1,144 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/tests/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.get<bool>(0));
+    }
+    {
+        CmdLine line("command 1");
+        Arguments args(line);
+        LIBBOARDGAME_CHECK(args.get<bool>(0));
+    }
+    {
+        CmdLine line("command 2");
+        Arguments args(line);
+        LIBBOARDGAME_CHECK_THROW(args.get<bool>(0), Failure);
+    }
+    {
+        CmdLine line("command arg1");
+        Arguments args(line);
+        LIBBOARDGAME_CHECK_THROW(args.get<bool>(0), Failure);
+    }
+    {
+        CmdLine line("command");
+        Arguments args(line);
+        LIBBOARDGAME_CHECK_THROW(args.get<bool>(0), Failure);
+    }
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_float)
+{
+    CmdLine line("command abc 5.5");
+    Arguments args(line);
+    LIBBOARDGAME_CHECK_THROW(args.get<float>(0), Failure);
+    LIBBOARDGAME_CHECK_CLOSE(5.5f, args.get<float>(1), 1e-4f);
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_int)
+{
+    CmdLine line("command 5 arg");
+    Arguments args(line);
+    LIBBOARDGAME_CHECK_EQUAL(5, args.get<int>(0));
+    LIBBOARDGAME_CHECK_THROW(args.get<int>(1), Failure);
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_min_int)
+{
+    CmdLine line("command 5");
+    Arguments args(line);
+    LIBBOARDGAME_CHECK_EQUAL(5, args.get_min<int>(0, 3));
+    LIBBOARDGAME_CHECK_THROW(args.get_min<int>(0, 7), Failure);
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_single_int)
+{
+    {
+        CmdLine line("command 5");
+        Arguments args(line);
+        LIBBOARDGAME_CHECK_EQUAL(5, args.get<int>());
+    }
+    {
+        CmdLine line("command 5 10");
+        Arguments args(line);
+        LIBBOARDGAME_CHECK_THROW(args.get<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/libboardgame_gtp/tests/CMakeLists.txt b/libboardgame_gtp/tests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..5910201
--- /dev/null
@@ -0,0 +1,13 @@
+add_executable(test_libboardgame_gtp
+  ArgumentsTest.cpp
+  CmdLineTest.cpp
+  GtpEngineTest.cpp
+  ResponseTest.cpp
+)
+
+target_link_libraries(test_libboardgame_gtp
+    boardgame_test_main
+    boardgame_gtp
+    )
+
+add_test(libboardgame_gtp test_libboardgame_gtp)
diff --git a/libboardgame_gtp/tests/CmdLineTest.cpp b/libboardgame_gtp/tests/CmdLineTest.cpp
new file mode 100644 (file)
index 0000000..630f90c
--- /dev/null
@@ -0,0 +1,66 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/tests/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/libboardgame_gtp/tests/GtpEngineTest.cpp b/libboardgame_gtp/tests/GtpEngineTest.cpp
new file mode 100644 (file)
index 0000000..b6f33e9
--- /dev/null
@@ -0,0 +1,118 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/tests/GtpEngineTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_gtp/GtpEngine.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::GtpEngine::exec_main_loop). */
+class InvalidResponseEngine
+    : public GtpEngine
+{
+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;
+    GtpEngine 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;
+    GtpEngine 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;
+    GtpEngine 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/libboardgame_gtp/tests/ResponseTest.cpp b/libboardgame_gtp/tests/ResponseTest.cpp
new file mode 100644 (file)
index 0000000..5591767
--- /dev/null
@@ -0,0 +1,24 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/tests/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/libboardgame_mcts/Atomic.h b/libboardgame_mcts/Atomic.h
new file mode 100644 (file)
index 0000000..e47a68c
--- /dev/null
@@ -0,0 +1,98 @@
+//-----------------------------------------------------------------------------
+/** @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>
+
+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([[maybe_unused]] memory_order order = memory_order_seq_cst) const
+    {
+        return val;
+    }
+
+    void store(T t, [[maybe_unused]] memory_order order = memory_order_seq_cst)
+    {
+        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/libboardgame_mcts/CMakeLists.txt b/libboardgame_mcts/CMakeLists.txt
new file mode 100644 (file)
index 0000000..2e87014
--- /dev/null
@@ -0,0 +1,19 @@
+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 Threads::Threads)
+
+if(BUILD_TESTING)
+    add_subdirectory(tests)
+endif()
diff --git a/libboardgame_mcts/LastGoodReply.h b/libboardgame_mcts/LastGoodReply.h
new file mode 100644 (file)
index 0000000..8b86702
--- /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 constexpr unsigned max_players = P;
+
+    static constexpr 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/libboardgame_mcts/Node.h b/libboardgame_mcts/Node.h
new file mode 100644 (file)
index 0000000..95d1cec
--- /dev/null
@@ -0,0 +1,314 @@
+//-----------------------------------------------------------------------------
+/** @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 <limits>
+#include "Atomic.h"
+#include "libboardgame_base/Assert.h"
+
+namespace libboardgame_mcts {
+
+//-----------------------------------------------------------------------------
+
+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 M. Enzenberger, M. Mueller: A Lock-free Multithreaded Monte-Carlo Tree
+    Search Algorithm. Advances in Computer Games 2009. */
+template<typename M, typename F, bool MT>
+class Node
+{
+public:
+    using Move = M;
+
+    using Float = F;
+
+    /** Value returned by get_nu_children() if node has not been expanded. */
+    static constexpr short value_unexpanded = -2;
+
+    /** Value returned by get_nu_children() if node is currently expanding. */
+    static constexpr short value_expanding = -1;
+
+    static constexpr unsigned max_children = numeric_limits<short>::max();
+
+    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 is_unexpanded() const { return get_nu_children() == value_unexpanded; }
+
+    void set_expanding();
+
+    /** Get the number of children or the expansion state.
+        @return The number of children, if the node has been expanded (0 means
+        terminal game state), otherwise the negative values value_unexpanded
+        or value_expanding. */
+    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 nu_children);
+
+    /** Faster version of link_children() for single-threaded parts of the
+        code. */
+    void link_children_st(NodeIdx first_child, unsigned 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 get_nu_children() > 0. Note that in lock-free search, it can
+        happen that get_nu_children() was greater 0 but becomes negative
+        again if two threads expand the node simultaneosly and one thread
+        sets nu_children to value_expanding because it missed that the other
+        thread already expanded it. But since nodes never get deleted during
+        the lock-free search, and the number of children is deterministic,
+        any value greater 0 returned by get_nu_children() will be in a
+        consistent state with the children pointed to by get_first_child(). */
+    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;
+
+    /** See get_nu_children() */
+    Atomic<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<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
+{
+    return m_first_child.load(memory_order_acquire);
+}
+
+template<typename M, typename F, bool MT>
+inline 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 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(value_unexpanded, 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(value_unexpanded, memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+inline void Node<M, F, MT>::link_children(NodeIdx first_child,
+                                          unsigned nu_children)
+{
+    LIBBOARDGAME_ASSERT(nu_children < max_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);
+    // Note that we need release/acquire order for both m_nu_children and
+    // m_first_child because because the lock-free search cannot guarantee that
+    // a node is not expanded by two threads simultaneously (even if it tries
+    // to minimize this probability). Therefore it can happen that
+    // m_nu_children had already been set to greater 0 by the first thread and
+    // then the children of the second thread must be visible to all threads as
+    // soon as the second thread overwrites m_first_child.
+    m_first_child.store(first_child, memory_order_release);
+    m_nu_children.store(static_cast<short>(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 nu_children)
+{
+    LIBBOARDGAME_ASSERT(nu_children < max_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(static_cast<short>(nu_children), memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+void Node<M, F, MT>::set_expanding()
+{
+    m_nu_children.store(value_expanding, 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(value_unexpanded, memory_order_relaxed);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_mcts
+
+#endif // LIBBOARDGAME_MCTS_NODE_H
diff --git a/libboardgame_mcts/PlayerMove.h b/libboardgame_mcts/PlayerMove.h
new file mode 100644 (file)
index 0000000..b4d23c0
--- /dev/null
@@ -0,0 +1,32 @@
+//-----------------------------------------------------------------------------
+/** @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;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_mcts
+
+#endif // LIBBOARDGAME_MCTS_PLAYER_MOVE_H
diff --git a/libboardgame_mcts/SearchBase.h b/libboardgame_mcts/SearchBase.h
new file mode 100644 (file)
index 0000000..614b896
--- /dev/null
@@ -0,0 +1,1513 @@
+//-----------------------------------------------------------------------------
+/** @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_base/ArrayList.h"
+#include "libboardgame_base/Barrier.h"
+#include "libboardgame_base/Compiler.h"
+#include "libboardgame_base/IntervalChecker.h"
+#include "libboardgame_base/Log.h"
+#include "libboardgame_base/RandomGenerator.h"
+#include "libboardgame_base/Statistics.h"
+#include "libboardgame_base/StringUtil.h"
+#include "libboardgame_base/TimeIntervalChecker.h"
+#include "libboardgame_base/Timer.h"
+
+namespace libboardgame_mcts {
+
+using namespace std;
+using libboardgame_base::time_to_string;
+using libboardgame_base::to_string;
+using libboardgame_base::ArrayList;
+using libboardgame_base::Barrier;
+using libboardgame_base::IntervalChecker;
+using libboardgame_base::RandomGenerator;
+using libboardgame_base::StatisticsBase;
+using libboardgame_base::StatisticsDirty;
+using libboardgame_base::StatisticsExt;
+using libboardgame_base::Timer;
+using libboardgame_base::TimeIntervalChecker;
+using libboardgame_base::TimeSource;
+using libboardgame_mcts::find_node;
+
+//-----------------------------------------------------------------------------
+
+#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 constexpr PlayerInt max_players = 2;
+
+    /** The maximum length of a game. */
+    static constexpr 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 constexpr bool multithread = true;
+
+    /** Use RAVE. */
+    static constexpr 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 constexpr bool rave_dist_weighting = false;
+
+    /** Enable Last-Good-Reply heuristic.
+        @see LastGoodReply */
+    static constexpr bool use_lgr = false;
+
+    /** See LastGoodReply::hash_table_size.
+        Must be greater 0 if use_lgr is true. */
+    static constexpr 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 constexpr bool virtual_loss = false;
+
+    /** 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 S. Gelly, D. Silver: Combining Online and Offline Knowledge in
+    UCT. Proceedings of the 24th international conference on Machine learning,
+    pp. 273-280, 2007) 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 a variation of the one used in AlphaGo (see
+    D. Silver, A. Huang, et al.: Mastering the game of Go with deep neural
+    networks and tree search. Nature 529 (7587), pp. 484-489, 2016), which
+    produced better results in Pentobi (in Blokus and some similar games).
+    It has the form
+    @f$ c P_{move} \sqrt{N_{parent}} \log(N_{parent} + 1) / N_{child} @f$
+    with an exploration constant c and move priors P. Children counts are
+    assumed to be initialized greater than 0.
+
+    @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 constexpr 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 constexpr PlayerInt max_players = SearchParamConst::max_players;
+
+    static constexpr unsigned max_moves = SearchParamConst::max_moves;
+
+    static constexpr 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 */
+    /** @{ */
+
+    /** See class description for the exploration term. */
+    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 StatisticsDirty<Float>& get_root_val(PlayerInt player) const;
+
+    /** Get evaluation for get_player() at root node. */
+    const StatisticsDirty<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;
+
+    /** Abort a running search before the time limit or maximum number
+        of simulations is reached. */
+    void abort() { m_abort = true; }
+
+    /** Was the last search aborted? */
+    bool was_aborted() const { return m_abort; }
+
+    /** 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_base::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<StatisticsDirty<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;
+
+    atomic<bool> m_abort = 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 = 0;
+
+    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,
+                             const typename Tree::Children& children);
+
+    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 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 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 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_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(
+        [[maybe_unused]] const ThreadState& thread_state) const
+{
+    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 (m_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(
+        [[maybe_unused]] ThreadState& thread_state, Float remaining) const
+{
+    // 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;
+    // 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;
+    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(
+        [[maybe_unused]] ArrayList<Move, max_moves>& 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 StatisticsDirty<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 StatisticsDirty<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([[maybe_unused]] bool is_followup)
+{
+    // Default implementation does nothing
+}
+
+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;
+    typename Tree::Children children;
+    while (! (children = m_tree.get_children(*node)).empty())
+    {
+        node = select_child(*node, children);
+        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({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 && node->is_unexpanded())
+    {
+        m_tree.set_expanding(*node);
+        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({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 {};
+    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()
+      << ", Vst " << 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 {};
+}
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::prune(
+        TimeSource& time_source, [[maybe_unused]] double time,
+        Float prune_min_count, Float& new_prune_min_count)
+{
+    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.5f * 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_abort))
+            || (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;
+    m_abort = false;
+    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.get_nu_children() <= 0)
+    {
+        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);
+    }
+
+    auto nu_children = root.get_nu_children();
+    if (nu_children <= 0)
+        LIBBOARDGAME_LOG("No legal moves at root");
+    else if (nu_children == 1 && min_simulations == 0)
+        LIBBOARDGAME_LOG("Root has only one child");
+    else
+        while (true)
+        {
+            for (unsigned i = 1; i < nu_threads; ++i)
+                m_threads[i]->start_search();
+            search_loop(thread_state_0);
+            for (unsigned i = 1; i < nu_threads; ++i)
+                m_threads[i]->wait_search_finished();
+            bool is_out_of_mem = false;
+            for (unsigned i = 0; i < nu_threads; ++i)
+                if (m_threads[i]->thread_state.is_out_of_mem)
+                {
+                    is_out_of_mem = true;
+                    break;
+                }
+            if (! is_out_of_mem)
+                break;
+            double time = m_timer();
+            prune(time_source, time, prune_min_count, prune_min_count);
+        }
+
+    m_last_time = m_timer();
+    LIBBOARDGAME_LOG(get_info());
+    bool result = select_move(mv);
+    m_time_source = nullptr;
+    return result;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::search_loop(ThreadState& thread_state)
+{
+    auto& state = *thread_state.state;
+    auto& simulation = thread_state.simulation;
+    simulation.nodes.assign(&m_tree.get_root());
+    simulation.moves.clear();
+    double time_interval = 0.1;
+    if (m_max_count == 0 && m_max_time < 1)
+        time_interval = 0.1 * m_max_time;
+    IntervalChecker expensive_abort_checker(
+                *m_time_source, time_interval,
+                bind(&SearchBase::check_abort_expensive, this,
+                     ref(thread_state)));
+    if (m_deterministic)
+    {
+        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);
+    }
+}
+
+/** Select child in in-tree phase of the search.
+    @param node The parent node.
+    @param children The children. This is passed as an argument because due to
+    the lock-free it can occur that the parent was already expanded by one
+    thread but is set to expanding state again by another thread. This is no
+    problem because nodes are never deleted during the parallel search.
+    @pre ! children.empty() */
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::select_child(
+        const Node& node,
+        const typename Tree::Children& children) -> const Node*
+{
+    auto parent_count = node.get_visit_count();
+    // See class description for the exploration term
+    Float expl_factor =
+            m_exploration_constant * sqrt(parent_count)
+            * log(parent_count + 1);
+    static_assert(SearchParamConst::child_min_count > 0);
+    auto expl_limit =
+            expl_factor * SearchParamConst::max_move_prior
+            / SearchParamConst::child_min_count;
+    auto i = children.begin();
+    auto value =
+            i->get_value()
+            + i->get_move_prior() * expl_factor / i->get_value_count();
+    auto best_value = value;
+    auto limit = best_value - expl_limit;
+    auto best_child = i;
+    while (++i != children.end())
+    {
+        value = i->get_value();
+        if (value <= limit)
+            continue;
+        value += i->get_move_prior() * expl_factor / i->get_value_count();
+        if (value > best_value)
+        {
+            best_value = value;
+            limit = best_value - expl_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(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);
+        for (auto& it : m_tree.get_children(*node))
+        {
+            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);
+        }
+        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/libboardgame_mcts/Tree.h b/libboardgame_mcts/Tree.h
new file mode 100644 (file)
index 0000000..9fa9b64
--- /dev/null
@@ -0,0 +1,456 @@
+//-----------------------------------------------------------------------------
+/** @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_base::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_root_children() const { return get_children(get_root()); }
+
+    size_t get_nu_nodes() const;
+
+    const Node& get_node(NodeIdx i) const;
+
+    void set_expanding(const Node& node) { non_const(node).set_expanding(); }
+
+    void link_children(const Node& node, const Node* first_child,
+                       unsigned 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, [[maybe_unused]] Float child_min_count,
+        [[maybe_unused]] 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;
+#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();
+    if (nu_children > 0)
+    {
+        auto begin = &get_node(node.get_first_child());
+        return Children(begin, begin + nu_children);
+    }
+    return Children(nullptr, nullptr);
+}
+
+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>(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.get_nu_children() > 0)
+        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));
+    LIBBOARDGAME_ASSERT(node.get_nu_children() > 0);
+    auto nu_children = static_cast<unsigned>(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;
+    LIBBOARDGAME_ASSERT(thread_storage.next < thread_storage.end);
+    auto end = &first_child + nu_children;
+    for (auto i = &first_child; i != end; ++i, ++target_child)
+    {
+        target_child->copy_data_from(*i);
+        if (i->get_nu_children() <= 0 || 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().get_nu_children() <= 0);
+    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 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/libboardgame_mcts/TreeUtil.h b/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/libboardgame_mcts/tests/CMakeLists.txt b/libboardgame_mcts/tests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..4b433b1
--- /dev/null
@@ -0,0 +1,10 @@
+add_executable(test_libboardgame_mcts
+  NodeTest.cpp
+)
+
+target_link_libraries(test_libboardgame_mcts
+    boardgame_test_main
+    boardgame_mcts
+    )
+
+add_test(libboardgame_mcts test_libboardgame_mcts)
diff --git a/libboardgame_mcts/tests/NodeTest.cpp b/libboardgame_mcts/tests/NodeTest.cpp
new file mode 100644 (file)
index 0000000..0d2cec0
--- /dev/null
@@ -0,0 +1,37 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_mcts/tests/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/libboardgame_test/CMakeLists.txt b/libboardgame_test/CMakeLists.txt
new file mode 100644 (file)
index 0000000..13791bd
--- /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_base)
+
+add_library(boardgame_test_main STATIC Main.cpp)
+
+target_link_libraries(boardgame_test_main boardgame_test)
diff --git a/libboardgame_test/Main.cpp b/libboardgame_test/Main.cpp
new file mode 100644 (file)
index 0000000..35f1c81
--- /dev/null
@@ -0,0 +1,16 @@
+//-----------------------------------------------------------------------------
+/** @file libboardgame_test/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/libboardgame_test/Test.cpp b/libboardgame_test/Test.cpp
new file mode 100644 (file)
index 0000000..99fd78b
--- /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_base/Assert.h"
+#include "libboardgame_base/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({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_base::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/libboardgame_test/Test.h b/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/libpentobi_base/Board.cpp b/libpentobi_base/Board.cpp
new file mode 100644 (file)
index 0000000..c93c952
--- /dev/null
@@ -0,0 +1,870 @@
+//-----------------------------------------------------------------------------
+/** @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
+
+//-----------------------------------------------------------------------------
+
+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({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/libpentobi_base/Board.h b/libpentobi_base/Board.h
new file mode 100644 (file)
index 0000000..509b0dd
--- /dev/null
@@ -0,0 +1,917 @@
+//-----------------------------------------------------------------------------
+/** @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 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. */
+class Board
+{
+public:
+    using PointStateGrid = Grid<PointState>;
+
+    /** Maximum number of pieces per player in any game variant. */
+    static constexpr unsigned max_pieces = Setup::max_pieces;
+
+    using PiecesLeftList = ArrayList<Piece, Piece::max_pieces>;
+
+    static constexpr unsigned max_player_moves = max_pieces;
+
+    /** Maximum number of moves in any game variant. */
+    static constexpr unsigned max_moves = Color::range * max_player_moves;
+
+    /** Use ANSI escape sequences for colored text output in operator>> */
+    static inline bool color_output = false;
+
+
+    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;
+
+    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;
+}
+
+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/libpentobi_base/BoardConst.cpp b/libpentobi_base/BoardConst.cpp
new file mode 100644 (file)
index 0000000..9816cf8
--- /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_base/Compiler.h"
+#include "libboardgame_base/Log.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::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/libpentobi_base/BoardConst.h b/libpentobi_base/BoardConst.h
new file mode 100644 (file)
index 0000000..7b51327
--- /dev/null
@@ -0,0 +1,346 @@
+//-----------------------------------------------------------------------------
+/** @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_base/Range.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::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/libpentobi_base/BoardUpdater.cpp b/libpentobi_base/BoardUpdater.cpp
new file mode 100644 (file)
index 0000000..98a401f
--- /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_base/SgfUtil.h"
+
+namespace libpentobi_base {
+
+using libpentobi_base::get_current_position_as_setup;
+using libboardgame_base::SgfError;
+using libboardgame_base::get_path_from_root;
+
+//-----------------------------------------------------------------------------
+
+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/libpentobi_base/BoardUpdater.h b/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/libpentobi_base/BoardUtil.cpp b/libpentobi_base/BoardUtil.cpp
new file mode 100644 (file)
index 0000000..e999776
--- /dev/null
@@ -0,0 +1,84 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+
+//-----------------------------------------------------------------------------
+
+#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/libpentobi_base/BoardUtil.h b/libpentobi_base/BoardUtil.h
new file mode 100644 (file)
index 0000000..214d2c6
--- /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_base/Writer.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::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/libpentobi_base/Book.cpp b/libpentobi_base/Book.cpp
new file mode 100644 (file)
index 0000000..6d11add
--- /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_base/Log.h"
+#include "libboardgame_base/TreeReader.h"
+
+//-----------------------------------------------------------------------------
+
+namespace libpentobi_base {
+
+using libboardgame_base::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/libpentobi_base/Book.h b/libpentobi_base/Book.h
new file mode 100644 (file)
index 0000000..9608fbe
--- /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_base/RandomGenerator.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::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/libpentobi_base/CMakeLists.txt b/libpentobi_base/CMakeLists.txt
new file mode 100644 (file)
index 0000000..ac8d6de
--- /dev/null
@@ -0,0 +1,80 @@
+add_library(pentobi_base STATIC
+  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
+)
+
+target_link_libraries(pentobi_base boardgame_base)
+target_include_directories(pentobi_base PUBLIC ..)
+
+if(BUILD_TESTING)
+    add_subdirectory(tests)
+endif()
diff --git a/libpentobi_base/CallistoGeometry.cpp b/libpentobi_base/CallistoGeometry.cpp
new file mode 100644 (file)
index 0000000..b9ce871
--- /dev/null
@@ -0,0 +1,121 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/CallistoGeometry.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CallistoGeometry.h"
+
+#include <map>
+#include <memory>
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+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;
+    auto geometry = make_shared<CallistoGeometry>(nu_colors);
+    s_geometry.insert({nu_colors, geometry});
+    return *geometry;
+}
+
+auto CallistoGeometry::get_adj_coord(
+        [[maybe_unused]] int x, [[maybe_unused]] int y) const -> AdjCoordList
+{
+    return {};
+}
+
+auto CallistoGeometry::get_diag_coord(int x, int y) const -> DiagCoordList
+{
+    DiagCoordList l;
+    l.push_back({x, y - 1});
+    l.push_back({x - 1, y});
+    l.push_back({x + 1, y});
+    l.push_back({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(
+        [[maybe_unused]] int x, [[maybe_unused]] int y) const
+{
+    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/libpentobi_base/CallistoGeometry.h b/libpentobi_base/CallistoGeometry.h
new file mode 100644 (file)
index 0000000..6cc093a
--- /dev/null
@@ -0,0 +1,54 @@
+//-----------------------------------------------------------------------------
+/** @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);
+
+
+    explicit CallistoGeometry(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;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_CALLISTO_GEOMETRY_H
diff --git a/libpentobi_base/Color.h b/libpentobi_base/Color.h
new file mode 100644 (file)
index 0000000..3854620
--- /dev/null
@@ -0,0 +1,145 @@
+//-----------------------------------------------------------------------------
+/** @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_base/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 constexpr IntType range = 4;
+
+    Color();
+
+    explicit Color(IntType i);
+
+    bool operator==(Color c) const;
+
+    bool operator!=(Color c) const { return ! operator==(c); }
+
+    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 constexpr IntType value_uninitialized = range;
+
+    IntType m_i;
+
+#ifdef LIBBOARDGAME_DEBUG
+    bool is_initialized() const { return m_i < value_uninitialized; }
+#endif
+};
+
+
+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
+{
+    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 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/libpentobi_base/ColorMap.h b/libpentobi_base/ColorMap.h
new file mode 100644 (file)
index 0000000..df4108b
--- /dev/null
@@ -0,0 +1,43 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+
+//-----------------------------------------------------------------------------
+
+/** 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) { fill(val); }
+
+    T& operator[](Color c) { return m_a[c.to_int()]; }
+
+    const T& operator[](Color c) const { return m_a[c.to_int()]; }
+
+    void fill(const T& val) { m_a.fill(val); }
+
+private:
+    array<T, Color::range> m_a;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_COLOR_MAP_H
diff --git a/libpentobi_base/ColorMove.h b/libpentobi_base/ColorMove.h
new file mode 100644 (file)
index 0000000..670a353
--- /dev/null
@@ -0,0 +1,64 @@
+//-----------------------------------------------------------------------------
+/** @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() { return {Color(0), Move::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 { return ! operator==(mv); }
+
+    bool is_null() const { return move.is_null(); }
+};
+
+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;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_BASE_COLOR_MOVE_H
diff --git a/libpentobi_base/Game.cpp b/libpentobi_base/Game.cpp
new file mode 100644 (file)
index 0000000..ded25fc
--- /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_base/SgfError.h"
+#include "libboardgame_base/SgfUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::back_to_main_variation;
+using libboardgame_base::is_main_variation;
+using libboardgame_base::SgfError;
+
+//-----------------------------------------------------------------------------
+
+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(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/libpentobi_base/Game.h b/libpentobi_base/Game.h
new file mode 100644 (file)
index 0000000..1eab29e
--- /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_base::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/libpentobi_base/GembloQGeometry.cpp b/libpentobi_base/GembloQGeometry.cpp
new file mode 100644 (file)
index 0000000..04f53e4
--- /dev/null
@@ -0,0 +1,164 @@
+//-----------------------------------------------------------------------------
+/** @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_base/MathUtil.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_base::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;
+    auto geometry = make_shared<GembloQGeometry>(nu_players);
+    s_geometry.insert({nu_players, geometry});
+    return *geometry;
+}
+
+auto GembloQGeometry::get_adj_coord(int x, int y) const -> AdjCoordList
+{
+    AdjCoordList l;
+    l.push_back({x + 1, y});
+    l.push_back({x - 1, y});
+    switch (get_point_type(x, y))
+    {
+    case 0:
+    case 3:
+        l.push_back({x, y - 1});
+        break;
+    case 1:
+    case 2:
+        l.push_back({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({x + 2, y - 1});
+        l.push_back({x - 1, y + 1});
+        l.push_back({x - 1, y - 1});
+        l.push_back({x, y + 1});
+        l.push_back({x + 3, y});
+        l.push_back({x - 2, y + 1});
+        l.push_back({x + 1, y + 1});
+        l.push_back({x + 3, y - 1});
+        l.push_back({x - 2, y});
+        l.push_back({x + 2, y});
+        l.push_back({x + 1, y - 1});
+        break;
+    case 1:
+        l.push_back({x - 2, y + 1});
+        l.push_back({x + 1, y - 1});
+        l.push_back({x + 1, y + 1});
+        l.push_back({x, y - 1});
+        l.push_back({x - 3, y});
+        l.push_back({x + 2, y - 1});
+        l.push_back({x - 1, y - 1});
+        l.push_back({x - 3, y + 1});
+        l.push_back({x + 2, y});
+        l.push_back({x - 2, y});
+        l.push_back({x - 1, y + 1});
+        break;
+    case 2:
+        l.push_back({x - 2, y - 1});
+        l.push_back({x + 3, y + 1});
+        l.push_back({x - 1, y + 1});
+        l.push_back({x, y - 1});
+        l.push_back({x + 3, y});
+        l.push_back({x + 2, y + 1});
+        l.push_back({x + 1, y - 1});
+        l.push_back({x - 2, y});
+        l.push_back({x + 2, y});
+        l.push_back({x - 1, y - 1});
+        l.push_back({x + 1, y + 1});
+        break;
+    case 3:
+        l.push_back({x - 3, y - 1});
+        l.push_back({x + 2, y + 1});
+        l.push_back({x + 1, y - 1});
+        l.push_back({x, y + 1});
+        l.push_back({x - 3, y});
+        l.push_back({x - 2, y - 1});
+        l.push_back({x - 1, y + 1});
+        l.push_back({x + 2, y});
+        l.push_back({x - 2, y});
+        l.push_back({x + 1, y + 1});
+        l.push_back({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/libpentobi_base/GembloQGeometry.h b/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/libpentobi_base/GembloQTransform.cpp b/libpentobi_base/GembloQTransform.cpp
new file mode 100644 (file)
index 0000000..3c2faff
--- /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_base/MathUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::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/libpentobi_base/GembloQTransform.h b/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/libpentobi_base/Geometry.h b/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/libpentobi_base/Grid.h b/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/libpentobi_base/Marker.h b/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/libpentobi_base/Move.h b/libpentobi_base/Move.h
new file mode 100644 (file)
index 0000000..489a851
--- /dev/null
@@ -0,0 +1,130 @@
+//-----------------------------------------------------------------------------
+/** @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_base/Assert.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+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 = std::uint_least16_t;
+
+    static constexpr IntType onboard_moves_classic = 30433;
+
+    static constexpr IntType onboard_moves_trigon = 32131;
+
+    static constexpr IntType onboard_moves_trigon_3 = 24859;
+
+    static constexpr IntType onboard_moves_duo = 13729;
+
+    static constexpr IntType onboard_moves_junior = 7217;
+
+    static constexpr IntType onboard_moves_nexos = 15157;
+
+    static constexpr IntType onboard_moves_callisto = 9433;
+
+    static constexpr IntType onboard_moves_callisto_2 = 4265;
+
+    static constexpr IntType onboard_moves_callisto_3 = 6885;
+
+    static constexpr IntType onboard_moves_gembloq = 31254;
+
+    static constexpr IntType onboard_moves_gembloq_2 = 15018;
+
+    static constexpr 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 constexpr IntType range = onboard_moves_trigon + 1;
+
+
+    static Move null() { return Move(0); }
+
+
+    Move();
+
+    explicit Move(IntType i);
+
+    bool operator==(Move mv) const;
+
+    bool operator!=(Move mv) const { return ! operator==(mv); }
+
+    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 constexpr IntType value_uninitialized = range;
+
+    IntType m_i;
+
+
+#ifdef LIBBOARDGAME_DEBUG
+    bool is_initialized() const { return m_i < value_uninitialized; }
+#endif
+};
+
+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
+{
+    LIBBOARDGAME_ASSERT(is_initialized());
+    LIBBOARDGAME_ASSERT(mv.is_initialized());
+    return m_i < mv.m_i;
+}
+
+inline bool Move::is_null() const
+{
+    LIBBOARDGAME_ASSERT(is_initialized());
+    return m_i == 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/libpentobi_base/MoveInfo.h b/libpentobi_base/MoveInfo.h
new file mode 100644 (file)
index 0000000..8592036
--- /dev/null
@@ -0,0 +1,131 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+
+//-----------------------------------------------------------------------------
+
+/** 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/libpentobi_base/MoveList.h b/libpentobi_base/MoveList.h
new file mode 100644 (file)
index 0000000..4c94da6
--- /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_base/ArrayList.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** List that can hold all possible moves, not including Move::null() */
+using MoveList = libboardgame_base::ArrayList<Move, Move::range - 1>;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_MOVE_LIST_H
diff --git a/libpentobi_base/MoveMarker.h b/libpentobi_base/MoveMarker.h
new file mode 100644 (file)
index 0000000..3709b1e
--- /dev/null
@@ -0,0 +1,54 @@
+//-----------------------------------------------------------------------------
+/** @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:
+    std::array<bool, Move::range> m_a;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_MOVE_MARKER_H
diff --git a/libpentobi_base/MovePoints.h b/libpentobi_base/MovePoints.h
new file mode 100644 (file)
index 0000000..f397f17
--- /dev/null
@@ -0,0 +1,27 @@
+//-----------------------------------------------------------------------------
+/** @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_base/ArrayList.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+using MovePoints =
+    libboardgame_base::ArrayList<Point, PieceInfo::max_size, unsigned short>;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_BASE_MOVE_POINTS_H
diff --git a/libpentobi_base/NexosGeometry.cpp b/libpentobi_base/NexosGeometry.cpp
new file mode 100644 (file)
index 0000000..7000e02
--- /dev/null
@@ -0,0 +1,87 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/NexosGeometry.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "NexosGeometry.h"
+
+#include <memory>
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+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(
+        [[maybe_unused]] int x, [[maybe_unused]] int y) const -> AdjCoordList
+{
+    return {};
+}
+
+auto NexosGeometry::get_diag_coord(int x, int y) const -> DiagCoordList
+{
+    DiagCoordList l;
+    if (get_point_type(x, y) == 1)
+    {
+        l.push_back({x - 2, y});
+        l.push_back({x + 2, y});
+        l.push_back({x - 1, y - 1});
+        l.push_back({x + 1, y + 1});
+        l.push_back({x - 1, y + 1});
+        l.push_back({x + 1, y - 1});
+    }
+    else if (get_point_type(x, y) == 2)
+    {
+        l.push_back({x, y - 2});
+        l.push_back({x, y + 2});
+        l.push_back({x - 1, y - 1});
+        l.push_back({x + 1, y + 1});
+        l.push_back({x - 1, y + 1});
+        l.push_back({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/libpentobi_base/NexosGeometry.h b/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/libpentobi_base/NodeUtil.cpp b/libpentobi_base/NodeUtil.cpp
new file mode 100644 (file)
index 0000000..443814f
--- /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_base::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/libpentobi_base/NodeUtil.h b/libpentobi_base/NodeUtil.h
new file mode 100644 (file)
index 0000000..90d1c1a
--- /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_base/SgfNode.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::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/libpentobi_base/Pentobi-SGF.md b/libpentobi_base/Pentobi-SGF.md
new file mode 100644 (file)
index 0000000..836931f
--- /dev/null
@@ -0,0 +1,219 @@
+Pentobi SGF Files
+=================
+
+This document describes the file format for
+[Blokus](https://en.wikipedia.org/wiki/Blokus) game records as used by
+the program [Pentobi](https://pentobi.sourceforge.io). The most recent
+version of this document can be found in the source code distribution of
+Pentobi.
+
+Introduction
+------------
+
+The file format is a derivative of the
+[Smart Game Format](https://www.red-bean.com/sgf/) (SGF). The current
+SGF version 4 does not define standard properties for Blokus. Therefore,
+a number of game-specific properties and value types had to be defined.
+The definitions follow the recommendations of SGF 4 and the proposals
+for multi-player games from the
+[discussions](http://www.red-bean.com/sgf/ff5/ff5.htm) about the future
+SGF version 5.
+
+### Note
+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.
+
+File Extension and MIME Type
+----------------------------
+
+The file extension `.blksgf` and the
+[MIME type](https://en.wikipedia.org/wiki/Internet_media_type)
+`application/x-blokus-sgf` are used for Blokus SGF files.
+
+### Note
+Since this is a non-standard MIME type, links to Blokus SGF files on web
+servers will not automatically open the file with Pentobi even if
+Pentobi is installed locally and registered as a handler for Blokus SGF
+files. To make this work, you can put a file named
+[`.htaccess`](https://en.wikipedia.org/wiki/.htaccess) on the web server
+in the same directory that contains the `.blksgf` files or in one of its
+parent directories. This file needs to contain the line:
+```
+AddType application/x-blokus-sgf blksgf
+```
+
+Character Set
+-------------
+
+UTF-8 should be used as the character set. Pentobi always writes files
+in UTF-8 and indicates that with the `CA` 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 `CA` property.
+
+Game Property
+-------------
+
+Since there is no number for Blokus defined in SGF 4, a string instead
+of a number is used as the value for the `GM` property. Currently, the
+following strings are used:
+
+* `Blokus`
+* `Blokus Two-Player`
+* `Blokus Three-Player`
+* `Blokus Duo`
+* `Blokus Trigon`
+* `Blokus Trigon Two-Player`
+* `Blokus Trigon Three-Player`
+* `Blokus Junior`
+* `Nexos`
+* `Nexos Two-Player`
+* `Callisto`
+* `Callisto Two-Player`
+* `Callisto Two-Player Four-Color`
+* `Callisto Three-Player`
+* `GembloQ`
+* `GembloQ Two-Player`
+* `GembloQ Three-Player`
+* `GembloQ Two-Player Four-Color`
+
+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.
+
+### Note
+Although the SGF standard does not require an ordering of properties,
+it is recommended to put the `GM` property at the beginning because
+Pentobi's automatic MIME type detection looks only at the beginning
+of files.
+
+Color and Player Properties
+---------------------------
+
+In game variants with two players and two colors, `B` denotes the first
+player or color, `W` the second player or color. In game variants with
+three or four players and one color per player, `1`, `2`, `3`, `4`
+denote the first, second, third, and fourth player or color. In game
+variants with two players and four colors, `B` denotes the first player,
+`W` the second player, and `1`, `2`, `3`, `4` denote the first, second,
+third, and fourth color. This applies to move properties and properties
+related to a player or a color.
+
+### Example 1
+In the game variant Blokus Two-Player `PB` is the name of the first
+player, and `1` is a move of the first color.
+
+### Example 2
+In the game variant Blokus Two-Player, one could either use the `BL`,
+`WL` properties to indicate the time left for a player, if the game is
+played with a time limit for each player, or one could use the `1L`,
+`2L`, `3L`, `4L` properties to indicate the time left for a color, if
+the game is played with a time limit for each color. (This is only an
+example how the properties should be interpreted. Pentobi currently has
+no support for game clocks.)
+
+### Note
+Pentobi versions before 0.2 used the properties `BLUE`, `YELLOW`, `RED`,
+`GREEN` in the four-color game variants, which did not reflect the
+current state of discussion for SGF 5. Pentobi 12.0 erroneously used
+multi-player properties for two-player Callisto. Current versions of
+Pentobi can still read games written by older versions and will convert
+old properties.
+
+Coordinate System
+-----------------
+
+Fields on the board (called points in SGF) are identified by a
+case-insensitive string with a letter for the column followed by a
+number for the row. The letters start with 'a', the numbers start with
+'1'. The lower left corner of the board is 'a1'. The strings must not
+contain whitespaces. Note that, unlike the common convention in the game
+of Go, the letter 'i' is used.
+
+If there are more than 26 columns, the columns continue with 'aa', 'ab',
+..., 'ba', 'bb', ... More than 26 columns are presently required for
+Trigon and GembloQ.
+
+For Trigon, hexagonal boards are mapped to rectangular coordinates as in
+the following example of a hexagon with edge size 3:
+```
+6     / \ / \ / \ / \
+5   / \ / \ / \ / \ / \
+4 / \ / \ / \ / \ / \ / \
+3 \ / \ / \ / \ / \ / \ /
+2   \ / \ / \ / \ / \ /
+1     \ / \ / \ / \ /
+   a b c d e f g h i j k
+```
+
+In Nexos, the 13×13 line grid is mapped to a 25×25 coordinate system, in
+which rows with horizontal line segments and intersections alternate
+with rows with vertical line segments and holes:
+```
+6 |   |   |
+5 + - + - + -
+4 |   |   |
+3 + - + - + -
+2 |   |   |
+1 + - + - + -
+  a b c d e f
+```
+
+In GembloQ, each square field is divided into four triangles with their
+own coordinates, like in this example:
+```
+4 | / | \ | / | \ | /
+3 | \ | / | \ | / | \
+2 | / | \ | / | \ | /
+1 | \ | / | \ | / | \
+   a b c d e f g h i
+```
+
+Move Properties
+---------------
+
+The value of a move property is a string with the coordinates of the
+played piece on the board separated by commas. No whitespace characters
+are allowed before, after, or in-between the coordinates.
+
+Pentobi currently does not require a certain order of the coordinates of
+a move. However, move properties should be written with an ordered list
+of coordinates (using the order a1, b1, …, a2, b2, …) such that each
+move has a unique string representation.
+
+### Example
+`B[f9,e10,f10,g10,f11]`
+
+In Nexos, moves contain only the coordinates of line segments occupied
+by the piece, no coordinates of junctions.
+
+### Note
+Old versions of Pentobi (before version 0.3) used to represent moves by
+a list of points, which did not follow the convention used by other
+games in SGF to use single-value properties for moves. Current versions
+of Pentobi can still read games containing the old move property values
+but they are deprecated and should no longer be used.
+
+Setup Properties
+----------------
+
+The setup properties `AB`, `AW`, `A1`, `A2`, `A3`, `A4` can be used to
+place several pieces simultaneously on the board. The setup property
+`AE` can be used to remove pieces from the board. All these properties
+can have multiple values, each value represents a piece by its
+coordinates as in the move properties. The `PL` can be used to set the
+color to play in a setup position.
+
+### Example
+```
+AB[e8,e9,f9,d10,e10][g6,f7,g7,h7,g8]
+AW[i4,h5,i5,j5,i6][j7,j8,j9,k9,j10]
+PL[B]
+```
+
+### Note
+Older versions of Pentobi (before version 2.0) did not support setup
+properties, you need a newer version of Pentobi to read such files.
+Currently, Pentobi is able to read files with setup properties in any
+node, but can create only files with setup in the root node.
diff --git a/libpentobi_base/PentobiSgfUtil.cpp b/libpentobi_base/PentobiSgfUtil.cpp
new file mode 100644 (file)
index 0000000..183d8b4
--- /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_base/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/libpentobi_base/PentobiSgfUtil.h b/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/libpentobi_base/PentobiTree.cpp b/libpentobi_base/PentobiTree.cpp
new file mode 100644 (file)
index 0000000..33bda55
--- /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_base::InvalidProperty;
+using libboardgame_base::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/libpentobi_base/PentobiTree.h b/libpentobi_base/PentobiTree.h
new file mode 100644 (file)
index 0000000..93df549
--- /dev/null
@@ -0,0 +1,151 @@
+//-----------------------------------------------------------------------------
+/** @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_base/SgfTree.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::SgfNode;
+using libboardgame_base::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/libpentobi_base/PentobiTreeWriter.cpp b/libpentobi_base/PentobiTreeWriter.cpp
new file mode 100644 (file)
index 0000000..f28706f
--- /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_base::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_base::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_base::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_base::TreeWriter::write_property("B", values);
+            return;
+        }
+        if (id == "2")
+        {
+            libboardgame_base::TreeWriter::write_property("W", values);
+            return;
+        }
+    }
+    libboardgame_base::TreeWriter::write_property(id, values);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
diff --git a/libpentobi_base/PentobiTreeWriter.h b/libpentobi_base/PentobiTreeWriter.h
new file mode 100644 (file)
index 0000000..216be91
--- /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_base/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_base::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/libpentobi_base/Piece.h b/libpentobi_base/Piece.h
new file mode 100644 (file)
index 0000000..d3a0f08
--- /dev/null
@@ -0,0 +1,94 @@
+//-----------------------------------------------------------------------------
+/** @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_base/Assert.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** Wrapper around an integer representing a piece type in a certain
+    game variant. */
+class Piece
+{
+public:
+    using IntType = std::uint_fast8_t;
+
+    /** Maximum number of unique pieces per color. */
+    static constexpr IntType max_pieces = 24;
+
+    /** Integer range used for unique pieces without the null piece. */
+    static constexpr IntType range_not_null = max_pieces;
+
+    /** Integer range used for unique pieces including the null piece */
+    static constexpr IntType range = max_pieces + 1;
+
+
+    static Piece null() { return Piece(value_null); }
+
+
+    Piece();
+
+    explicit Piece(IntType i);
+
+    bool operator==(Piece piece) const { return m_i == piece.m_i; }
+
+    bool operator!=(Piece piece) const { return ! operator==(piece); }
+
+    bool is_null() const;
+
+    /** Return move as an integer between 0 and Piece::range */
+    IntType to_int() const;
+
+private:
+    static constexpr IntType value_null = range - 1;
+
+    static constexpr IntType value_uninitialized = range;
+
+    IntType m_i;
+
+
+#ifdef LIBBOARDGAME_DEBUG
+    bool is_initialized() const { return m_i < value_uninitialized; }
+#endif
+};
+
+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::is_null() const
+{
+    LIBBOARDGAME_ASSERT(is_initialized());
+    return m_i == 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/libpentobi_base/PieceInfo.cpp b/libpentobi_base/PieceInfo.cpp
new file mode 100644 (file)
index 0000000..785dba8
--- /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_base/Assert.h"
+#include "libboardgame_base/Compiler.h"
+#include "libboardgame_base/Log.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::get_type_name;
+using libboardgame_base::normalize_offset;
+using libboardgame_base::type_match_shift;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+const bool log_piece_creation = false;
+
+struct NormalizedPoints
+{
+    /** The normalized points of the transformed piece.
+        The points were shifted using GeometryUtil::normalize_offset(). */
+    PiecePoints points;
+
+    /** The point type of (0,0) in the normalized points. */
+    unsigned point_type;
+
+    bool operator==(const NormalizedPoints& n) const
+    {
+        return points == n.points && point_type == n.point_type;
+    }
+};
+
+#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/libpentobi_base/PieceInfo.h b/libpentobi_base/PieceInfo.h
new file mode 100644 (file)
index 0000000..8d375b2
--- /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_base/ArrayList.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_base::ArrayList;
+using libboardgame_base::CoordPoint;
+using libboardgame_base::Transform;
+
+//-----------------------------------------------------------------------------
+
+using ScoreType = float;
+
+//-----------------------------------------------------------------------------
+
+class PieceInfo
+{
+public:
+    /** Maximum number of points in a piece. */
+    static constexpr 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 constexpr unsigned max_scored_size = 22;
+
+    /** Maximum number of instances of a piece per player. */
+    static constexpr 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/libpentobi_base/PieceMap.h b/libpentobi_base/PieceMap.h
new file mode 100644 (file)
index 0000000..5f7981a
--- /dev/null
@@ -0,0 +1,64 @@
+//-----------------------------------------------------------------------------
+/** @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) { fill(val); }
+
+    bool operator==(const PieceMap& piece_map) const;
+
+    T& operator[](Piece piece);
+
+    const T& operator[](Piece piece) const;
+
+    void fill(const T& val) { m_a.fill(val); }
+
+private:
+    std::array<T, Piece::range_not_null> m_a;
+};
+
+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()];
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_PIECE_MAP_H
diff --git a/libpentobi_base/PieceTransforms.cpp b/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/libpentobi_base/PieceTransforms.h b/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/libpentobi_base/PieceTransformsClassic.cpp b/libpentobi_base/PieceTransformsClassic.cpp
new file mode 100644 (file)
index 0000000..3fccbcd
--- /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_base/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/libpentobi_base/PieceTransformsClassic.h b/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/libpentobi_base/PieceTransformsGembloQ.cpp b/libpentobi_base/PieceTransformsGembloQ.cpp
new file mode 100644 (file)
index 0000000..d3a85d5
--- /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_base/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/libpentobi_base/PieceTransformsGembloQ.h b/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/libpentobi_base/PieceTransformsTrigon.cpp b/libpentobi_base/PieceTransformsTrigon.cpp
new file mode 100644 (file)
index 0000000..907480d
--- /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_base/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/libpentobi_base/PieceTransformsTrigon.h b/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/libpentobi_base/PlayerBase.cpp b/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/libpentobi_base/PlayerBase.h b/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/libpentobi_base/Point.h b/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/libpentobi_base/PointList.h b/libpentobi_base/PointList.h
new file mode 100644 (file)
index 0000000..aea2a25
--- /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_base/ArrayList.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+using PointList = libboardgame_base::ArrayList<Point, Point::range_onboard>;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_POINT_LIST_H
diff --git a/libpentobi_base/PointState.h b/libpentobi_base/PointState.h
new file mode 100644 (file)
index 0000000..1689445
--- /dev/null
@@ -0,0 +1,105 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+
+//-----------------------------------------------------------------------------
+
+/** State of an on-board point, which can be a color or empty */
+class PointState
+{
+public:
+    using IntType = Color::IntType;
+
+    static constexpr IntType range = Color::range + 1;
+
+    static constexpr IntType value_empty = range - 1;
+
+
+    static PointState empty() { return PointState(value_empty); }
+
+
+    PointState();
+
+    explicit PointState(Color c) { m_i = c.to_int(); }
+
+    explicit PointState(IntType i);
+
+    bool operator==(PointState s) const { return m_i == s.m_i; }
+
+    bool operator!=(PointState s) const { return ! operator==(s); }
+
+    bool operator==(Color c) const { return m_i == c.to_int(); }
+
+    bool operator!=(Color c) const { return ! operator==(c); }
+
+    IntType to_int() const;
+
+    bool is_empty() const;
+
+    bool is_color() const;
+
+    Color to_color() const;
+
+private:
+    static constexpr IntType value_uninitialized = range;
+
+    IntType m_i;
+
+
+#ifdef LIBBOARDGAME_DEBUG
+    bool is_initialized() const { return m_i < value_uninitialized; }
+#endif
+};
+
+
+inline PointState::PointState()
+{
+#ifdef LIBBOARDGAME_DEBUG
+    m_i = value_uninitialized;
+#endif
+}
+
+inline PointState::PointState(IntType i)
+{
+    LIBBOARDGAME_ASSERT(i < range);
+    m_i = i;
+}
+
+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/libpentobi_base/PrecompMoves.h b/libpentobi_base/PrecompMoves.h
new file mode 100644 (file)
index 0000000..6fa0f81
--- /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_base/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 constexpr unsigned adj_status_nu_adj = 5;
+#else
+    static constexpr unsigned adj_status_nu_adj = 6;
+#endif
+
+    /** The maximum sum of the sizes of all precomputed move lists in any
+        game variant. */
+    static constexpr 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 constexpr unsigned nu_adj_status = 1 << adj_status_nu_adj;
+
+    /** Begin/end range for lists with moves at a given point. */
+    using Range = libboardgame_base::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/libpentobi_base/ScoreUtil.h b/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/libpentobi_base/Setup.h b/libpentobi_base/Setup.h
new file mode 100644 (file)
index 0000000..061758e
--- /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_base/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 constexpr unsigned max_pieces = 24;
+
+    using PlacementList = libboardgame_base::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/libpentobi_base/StartingPoints.cpp b/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/libpentobi_base/StartingPoints.h b/libpentobi_base/StartingPoints.h
new file mode 100644 (file)
index 0000000..f12ecee
--- /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_base/ArrayList.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::ArrayList;
+
+//-----------------------------------------------------------------------------
+
+class StartingPoints
+{
+public:
+    static constexpr 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/libpentobi_base/SymmetricPoints.cpp b/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/libpentobi_base/SymmetricPoints.h b/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/libpentobi_base/TreeUtil.cpp b/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/libpentobi_base/TreeUtil.h b/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/libpentobi_base/TrigonGeometry.cpp b/libpentobi_base/TrigonGeometry.cpp
new file mode 100644 (file)
index 0000000..cc610f2
--- /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;
+
+//-----------------------------------------------------------------------------
+
+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;
+    auto geometry = make_shared<TrigonGeometry>(sz);
+    s_geometry.insert({sz, geometry});
+    return *geometry;
+}
+
+auto TrigonGeometry::get_adj_coord(int x, int y) const -> AdjCoordList
+{
+    AdjCoordList l;
+    if (get_point_type(x, y) == 0)
+    {
+        l.push_back({x - 1, y});
+        l.push_back({x + 1, y});
+        l.push_back({x, y + 1});
+    }
+    else
+    {
+        l.push_back({x, y - 1});
+        l.push_back({x - 1, y});
+        l.push_back({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({x - 2, y});
+        l.push_back({x + 2, y});
+        l.push_back({x - 1, y - 1});
+        l.push_back({x + 1, y - 1});
+        l.push_back({x + 1, y + 1});
+        l.push_back({x - 1, y + 1});
+        l.push_back({x, y - 1});
+        l.push_back({x - 2, y + 1});
+        l.push_back({x + 2, y + 1});
+    }
+    else
+    {
+        l.push_back({x - 2, y});
+        l.push_back({x + 2, y});
+        l.push_back({x - 1, y + 1});
+        l.push_back({x + 1, y + 1});
+        l.push_back({x + 1, y - 1});
+        l.push_back({x - 1, y - 1});
+        l.push_back({x, y + 1});
+        l.push_back({x - 2, y - 1});
+        l.push_back({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/libpentobi_base/TrigonGeometry.h b/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/libpentobi_base/TrigonTransform.cpp b/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/libpentobi_base/TrigonTransform.h b/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/libpentobi_base/Variant.cpp b/libpentobi_base/Variant.cpp
new file mode 100644 (file)
index 0000000..dbcb944
--- /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_base/StringUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::trim;
+using libboardgame_base::to_lower;
+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;
+
+//-----------------------------------------------------------------------------
+
+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/libpentobi_base/Variant.h b/libpentobi_base/Variant.h
new file mode 100644 (file)
index 0000000..0657d13
--- /dev/null
@@ -0,0 +1,189 @@
+//-----------------------------------------------------------------------------
+/** @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", "classic_3", "duo", "junior",
+    "trigon", "trigon_2", "trigon_3", "nexos", "nexos_2", "callisto",
+    "callisto_2", "callisto_2_4", "callisto_3", "gembloq", "gembloq_2",
+    "gembloq_2_4", "gembloq_3". */
+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/libpentobi_base/tests/BoardConstTest.cpp b/libpentobi_base/tests/BoardConstTest.cpp
new file mode 100644 (file)
index 0000000..c2426fc
--- /dev/null
@@ -0,0 +1,50 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/tests/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/libpentobi_base/tests/BoardTest.cpp b/libpentobi_base/tests/BoardTest.cpp
new file mode 100644 (file)
index 0000000..8ee91ec
--- /dev/null
@@ -0,0 +1,164 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/tests/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;
+    [[maybe_unused]] auto ok = bd.from_string(mv, s);
+    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/libpentobi_base/tests/BoardUpdaterTest.cpp b/libpentobi_base/tests/BoardUpdaterTest.cpp
new file mode 100644 (file)
index 0000000..2bbf684
--- /dev/null
@@ -0,0 +1,142 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/tests/BoardUpdaterTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libpentobi_base/BoardUpdater.h"
+
+#include "libboardgame_base/SgfUtil.h"
+#include "libboardgame_base/TreeReader.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libpentobi_base;
+using libboardgame_base::TreeReader;
+using libboardgame_base::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/libpentobi_base/tests/CMakeLists.txt b/libpentobi_base/tests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..cebe496
--- /dev/null
@@ -0,0 +1,15 @@
+add_executable(test_libpentobi_base
+  BoardConstTest.cpp
+  BoardTest.cpp
+  BoardUpdaterTest.cpp
+  GameTest.cpp
+  PentobiTreeTest.cpp
+  PentobiSgfUtilTest.cpp
+)
+
+target_link_libraries(test_libpentobi_base
+    boardgame_test_main
+    pentobi_base
+    )
+
+add_test(libpentobi_base test_libpentobi_base)
diff --git a/libpentobi_base/tests/GameTest.cpp b/libpentobi_base/tests/GameTest.cpp
new file mode 100644 (file)
index 0000000..cda9e36
--- /dev/null
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/tests/GameTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libpentobi_base/Game.h"
+
+#include "libboardgame_base/TreeReader.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libpentobi_base;
+using libboardgame_base::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/libpentobi_base/tests/PentobiSgfUtilTest.cpp b/libpentobi_base/tests/PentobiSgfUtilTest.cpp
new file mode 100644 (file)
index 0000000..6bac684
--- /dev/null
@@ -0,0 +1,112 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/tests/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/libpentobi_base/tests/PentobiTreeTest.cpp b/libpentobi_base/tests/PentobiTreeTest.cpp
new file mode 100644 (file)
index 0000000..94ce7be
--- /dev/null
@@ -0,0 +1,218 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/tests/PentobiTreeTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/TreeReader.h"
+#include "libboardgame_test/Test.h"
+#include "libpentobi_base/PentobiTree.h"
+
+using namespace std;
+using namespace libpentobi_base;
+using libboardgame_base::InvalidProperty;
+using libboardgame_base::MissingProperty;
+using libboardgame_base::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/libpentobi_gtp/CMakeLists.txt b/libpentobi_gtp/CMakeLists.txt
new file mode 100644 (file)
index 0000000..9518bc7
--- /dev/null
@@ -0,0 +1,8 @@
+add_library(pentobi_gtp STATIC
+  GtpEngine.h
+  GtpEngine.cpp
+)
+
+target_include_directories(pentobi_gtp PUBLIC ..)
+
+target_link_libraries(pentobi_gtp boardgame_gtp pentobi_base)
diff --git a/libpentobi_gtp/GtpEngine.cpp b/libpentobi_gtp/GtpEngine.cpp
new file mode 100644 (file)
index 0000000..83d12aa
--- /dev/null
@@ -0,0 +1,396 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gtp/GtpEngine.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "GtpEngine.h"
+
+#include <fstream>
+#include <memory>
+#include "libboardgame_base/CpuTime.h"
+#include "libboardgame_base/Log.h"
+#include "libboardgame_base/RandomGenerator.h"
+#include "libboardgame_base/SgfUtil.h"
+#include "libboardgame_base/TreeReader.h"
+#include "libpentobi_base/MoveMarker.h"
+#include "libpentobi_base/PentobiTreeWriter.h"
+
+namespace libpentobi_gtp {
+
+using namespace std;
+using libboardgame_base::RandomGenerator;
+using libboardgame_base::TreeReader;
+using libboardgame_base::get_last_node;
+using libboardgame_gtp::Failure;
+using libpentobi_base::Grid;
+using libpentobi_base::Move;
+using libpentobi_base::MoveList;
+using libpentobi_base::MoveMarker;
+using libpentobi_base::PentobiTreeWriter;
+using libpentobi_base::Point;
+using libpentobi_base::SgfNode;
+
+//-----------------------------------------------------------------------------
+
+GtpEngine::GtpEngine(Variant variant)
+    : m_game(variant)
+{
+    add("all_legal", &GtpEngine::cmd_all_legal);
+    add("clear_board", &GtpEngine::cmd_clear_board);
+    add("cputime", &GtpEngine::cmd_cputime);
+    add("final_score", &GtpEngine::cmd_final_score);
+    add("loadsgf", &GtpEngine::cmd_loadsgf);
+    add("point_integers", &GtpEngine::cmd_point_integers);
+    add("move_info", &GtpEngine::cmd_move_info);
+    add("p", &GtpEngine::cmd_p);
+    add("param_base", &GtpEngine::cmd_param_base);
+    add("play", &GtpEngine::cmd_play);
+    add("savesgf", &GtpEngine::cmd_savesgf);
+    add("set_game", &GtpEngine::cmd_set_game);
+    add("set_random_seed", &GtpEngine::cmd_set_random_seed);
+    add("showboard", &GtpEngine::cmd_showboard);
+    add("undo", &GtpEngine::cmd_undo);
+}
+
+void GtpEngine::board_changed()
+{
+    if (m_show_board)
+        LIBBOARDGAME_LOG(get_board());
+}
+
+void GtpEngine::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 (auto mv : *moves)
+        response << bd.to_string(mv, false) << '\n';
+}
+
+void GtpEngine::cmd_clear_board()
+{
+    m_game.init();
+    board_changed();
+}
+
+void GtpEngine::cmd_cputime(Response& response)
+{
+    auto time = libboardgame_base::cpu_time();
+    if (time < 0)
+        throw Failure("cannot determine cpu time");
+    response << time;
+}
+
+void GtpEngine::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 GtpEngine::cmd_g(Response& response)
+{
+    genmove(get_board().get_effective_to_play(), response);
+}
+
+void GtpEngine::cmd_genmove(Arguments args, Response& response)
+{
+    genmove(get_color_arg(args), response);
+}
+
+void GtpEngine::cmd_loadsgf(Arguments args)
+{
+    args.check_size_less_equal(2);
+    auto file = args.get<string>(0);
+    unsigned move_number = 0;
+    if (args.get_size() == 2)
+        move_number = args.get_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 GtpEngine::cmd_move_info(Arguments args, Response& response)
+{
+    auto& bd = get_board();
+    Move mv;
+    try
+    {
+        mv = Move(args.get<Move::IntType>());
+    }
+    catch (const Failure&)
+    {
+        if (! bd.from_string(mv, args.get<string>()))
+        {
+            ostringstream msg;
+            msg << "invalid argument '" << args.get()
+                << "' (expected move or move ID)";
+            throw Failure(msg.str());
+        }
+    }
+    auto& geo = bd.get_geometry();
+    auto 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 (auto 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 GtpEngine::cmd_p(Arguments args)
+{
+    play(get_board().get_to_play(), args, 0);
+}
+
+void GtpEngine::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);
+        auto name = args.get(0);
+        if (name == "accept_illegal")
+            m_accept_illegal = args.get<bool>(1);
+        else if (name == "resign")
+            m_resign = args.get<bool>(1);
+        else
+        {
+            ostringstream msg;
+            msg << "unknown parameter '" << name << "'";
+            throw Failure(msg.str());
+        }
+    }
+}
+
+void GtpEngine::cmd_play(Arguments args)
+{
+    play(get_color_arg(args, 0), args, 1);
+}
+
+void GtpEngine::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 GtpEngine::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 GtpEngine::cmd_savesgf(Arguments args)
+{
+    ofstream out(args.get<string>());
+    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 GtpEngine::cmd_set_game(Arguments args)
+{
+    Variant variant;
+    string line(&*args.get_line().begin(), args.get_line().size());
+    if (! parse_variant(line, variant))
+        throw Failure("invalid argument");
+    m_game.init(variant);
+    board_changed();
+}
+
+/** Set global random seed.
+    Arguments: random seed */
+void GtpEngine::cmd_set_random_seed(Arguments args)
+{
+    RandomGenerator::set_global_seed(args.get<RandomGenerator::ResultType>());
+}
+
+void GtpEngine::cmd_showboard(Response& response)
+{
+    response << '\n' << get_board();
+}
+
+void GtpEngine::cmd_undo()
+{
+    auto& bd = get_board();
+    if (bd.get_nu_moves() == 0)
+        throw Failure("cannot undo");
+    m_game.undo();
+    board_changed();
+}
+
+void GtpEngine::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 GtpEngine::get_color_arg(Arguments args) const
+{
+    if (args.get_size() > 1)
+        throw Failure("too many arguments");
+    return get_color_arg(args, 0);
+}
+
+Color GtpEngine::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& GtpEngine::get_player() const
+{
+    if (m_player == nullptr)
+        throw Failure("no player set");
+    return *m_player;
+}
+
+void GtpEngine::on_handle_cmd_begin()
+{
+    libboardgame_base::flush_log();
+}
+
+void GtpEngine::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)
+    {
+        auto line = args.get_line();
+        if (! bd.from_string(mv, string(&*line.begin(), line.size())))
+            throw Failure("invalid move ");
+    }
+    else
+    {
+        auto line = args.get_remaining_line(arg_move_begin - 1);
+        if (! bd.from_string(mv, string(&*line.begin(), line.size())))
+            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 GtpEngine::set_player(PlayerBase& player)
+{
+    m_player = &player;
+    add("genmove", &GtpEngine::cmd_genmove);
+    add("g", &GtpEngine::cmd_g);
+    add("reg_genmove", &GtpEngine::cmd_reg_genmove);
+}
+
+void GtpEngine::set_show_board(bool enable)
+{
+    if (enable && ! m_show_board)
+        LIBBOARDGAME_LOG(get_board());
+    m_show_board = enable;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_gtp
diff --git a/libpentobi_gtp/GtpEngine.h b/libpentobi_gtp/GtpEngine.h
new file mode 100644 (file)
index 0000000..b582823
--- /dev/null
@@ -0,0 +1,97 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gtp/GtpEngine.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GTP_GTP_ENGINE_H
+#define LIBPENTOBI_GTP_GTP_ENGINE_H
+
+#include "libboardgame_gtp/GtpEngine.h"
+#include "libpentobi_base/Game.h"
+#include "libpentobi_base/PlayerBase.h"
+
+namespace libpentobi_gtp {
+
+using libpentobi_base::Board;
+using libpentobi_base::Color;
+using libpentobi_base::Game;
+using libpentobi_base::PlayerBase;
+using libpentobi_base::Variant;
+using libboardgame_gtp::Arguments;
+using libboardgame_gtp::Response;
+
+//-----------------------------------------------------------------------------
+
+/** GTP Blokus engine. */
+class GtpEngine
+    : public libboardgame_gtp::GtpEngine
+{
+public:
+    explicit GtpEngine(Variant variant);
+
+    void cmd_all_legal(Arguments args, Response& response);
+    void cmd_clear_board();
+    void cmd_cputime(Response& response);
+    void cmd_final_score(Response& response);
+    void cmd_g(Response& response);
+    void cmd_genmove(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_set_random_seed(Arguments args);
+    void cmd_undo();
+
+    /** Set the player.
+        @param player The player. The lifetime of this parameter must
+        exceed the lifetime of the class instance. */
+    void set_player(PlayerBase& player);
+
+    void set_accept_illegal(bool enable) { m_accept_illegal = enable; }
+
+    /** Enable or disable resigning. */
+    void set_resign(bool enable) { m_resign = enable; }
+
+    void set_show_board(bool enable);
+
+    const Board& get_board() const { return m_game.get_board(); }
+
+protected:
+    Color get_color_arg(Arguments args, unsigned i) const;
+
+    Color get_color_arg(Arguments args) const;
+
+    void on_handle_cmd_begin() override;
+
+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);
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_gtp
+
+#endif // LIBPENTOBI_GTP_GTP_ENGINE_H
diff --git a/libpentobi_kde_thumbnailer/CMakeLists.txt b/libpentobi_kde_thumbnailer/CMakeLists.txt
new file mode 100644 (file)
index 0000000..22ae071
--- /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)
+
+add_library(pentobi_kde_thumbnailer STATIC
+  ../libboardgame_base/Assert.cpp
+  ../libboardgame_base/Reader.cpp
+  ../libboardgame_base/SgfError.cpp
+  ../libboardgame_base/SgfNode.cpp
+  ../libboardgame_base/SgfTree.cpp
+  ../libboardgame_base/StringRep.cpp
+  ../libboardgame_base/StringUtil.cpp
+  ../libboardgame_base/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_options(pentobi_kde_thumbnailer PRIVATE
+    ${CMAKE_SHARED_LIBRARY_CXX_FLAGS})
+
+target_compile_definitions(pentobi_kde_thumbnailer PRIVATE
+    QT_DEPRECATED_WARNINGS
+    QT_DISABLE_DEPRECATED_BEFORE=0x051200)
+
+target_link_libraries(pentobi_kde_thumbnailer Qt5::Gui)
diff --git a/libpentobi_mcts/AnalyzeGame.cpp b/libpentobi_mcts/AnalyzeGame.cpp
new file mode 100644 (file)
index 0000000..a4d87a1
--- /dev/null
@@ -0,0 +1,127 @@
+//-----------------------------------------------------------------------------
+/** @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_base/Log.h"
+#include "libboardgame_base/WallTimeSource.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_base::SgfError;
+using libboardgame_base::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;
+    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 (search.was_aborted())
+                        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 (search.was_aborted())
+                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/libpentobi_mcts/AnalyzeGame.h b/libpentobi_mcts/AnalyzeGame.h
new file mode 100644 (file)
index 0000000..e3fe5ec
--- /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
+        Search::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/libpentobi_mcts/CMakeLists.txt b/libpentobi_mcts/CMakeLists.txt
new file mode 100644 (file)
index 0000000..1dd0902
--- /dev/null
@@ -0,0 +1,41 @@
+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)
+
+if(BUILD_TESTING)
+    add_subdirectory(tests)
+endif()
diff --git a/libpentobi_mcts/Float.h b/libpentobi_mcts/Float.h
new file mode 100644 (file)
index 0000000..3e9179a
--- /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/libpentobi_mcts/History.cpp b/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/libpentobi_mcts/History.h b/libpentobi_mcts/History.h
new file mode 100644 (file)
index 0000000..fd99ddc
--- /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_base::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/libpentobi_mcts/LocalPoints.cpp b/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/libpentobi_mcts/LocalPoints.h b/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/libpentobi_mcts/Player.cpp b/libpentobi_mcts/Player.cpp
new file mode 100644 (file)
index 0000000..513d743
--- /dev/null
@@ -0,0 +1,368 @@
+//-----------------------------------------------------------------------------
+/** @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_base/CpuTimeSource.h"
+#include "libboardgame_base/Memory.h"
+#include "libboardgame_base/WallTimeSource.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_base::CpuTimeSource;
+using libboardgame_base::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.
+
+constexpr float counts_classic[Player::max_supported_level] =
+    { 3, 30, 90, 181, 667, 5028, 69809, 349044, 1745221 };
+
+constexpr float counts_duo[Player::max_supported_level] =
+    { 3, 21, 77, 213, 861, 7280, 221867, 1109339, 5546695 };
+
+constexpr float counts_trigon[Player::max_supported_level] =
+    { 100, 246, 457, 876, 1882, 5506, 19819, 99092, 495465 };
+
+constexpr float counts_nexos[Player::max_supported_level] =
+    { 250, 347, 625, 1223, 3117, 8270, 20954, 104774, 523877 };
+
+constexpr float counts_callisto_2[Player::max_supported_level] =
+    { 30, 87, 300, 1017, 4729, 20435, 122778, 613905, 3069529 };
+
+/** Suggest how much memory to use for the trees depending on the maximum
+    level used. */
+size_t get_memory(unsigned max_level)
+{
+    auto available = libboardgame_base::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 (max_level < Player::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.
+        static_assert(Player::max_supported_level >= 5);
+        auto factor = pow(counts_trigon[Player::max_supported_level - 1]
+                          / counts_trigon[max(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;
+}
+
+} // 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_search(initial_variant, nu_threads, get_memory(max_level)),
+      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;
+    m_was_aborted = 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();
+    m_was_aborted = m_search.was_aborted();
+    // Resign only in two-player game variants
+    if (get_nu_players(variant) == 2)
+        if (m_search.get_root_visit_count() > 500
+                && m_search.get_root_val().get_mean() < 0.09f)
+            m_resign = true;
+    return mv;
+}
+
+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).
+    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[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[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[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[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/libpentobi_mcts/Player.h b/libpentobi_mcts/Player.h
new file mode 100644 (file)
index 0000000..0a86ea5
--- /dev/null
@@ -0,0 +1,191 @@
+//-----------------------------------------------------------------------------
+/** @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 constexpr 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;
+
+    /** Was last move generation based on an aborted search? */
+    bool was_aborted() const { return m_was_aborted; }
+
+private:
+    bool m_is_book_loaded;
+
+    bool m_use_book;
+
+    bool m_resign;
+
+    bool m_was_aborted;
+
+    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;
+
+    double m_fixed_time;
+
+    Search m_search;
+
+    Book m_book;
+
+    unique_ptr<TimeSource> m_time_source;
+
+
+    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/libpentobi_mcts/PlayoutFeatures.h b/libpentobi_mcts/PlayoutFeatures.h
new file mode 100644 (file)
index 0000000..11333ef
--- /dev/null
@@ -0,0 +1,227 @@
+//-----------------------------------------------------------------------------
+/** @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 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 constexpr 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/libpentobi_mcts/PriorKnowledge.cpp b/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/libpentobi_mcts/PriorKnowledge.h b/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/libpentobi_mcts/Search.cpp b/libpentobi_mcts/Search.cpp
new file mode 100644 (file)
index 0000000..505d148
--- /dev/null
@@ -0,0 +1,172 @@
+//-----------------------------------------------------------------------------
+/** @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)
+{
+    // We need to be sure that the maximum number of legal moves fits into
+    // the integer type used in Node. Currently, this is even true for all
+    // moves, which is an upper limit to all legal moves.
+    LIBBOARDGAME_ASSERT(bd.get_board_const().get_range() <= Node::max_children);
+    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);
+    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(0.4f);
+        set_rave_parent_max(50000);
+        break;
+    case Variant::duo:
+    case Variant::junior:
+    case Variant::gembloq_2:
+        // Tuned for duo
+        set_exploration_constant(0.5f);
+        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(0.87f);
+        set_rave_parent_max(50000);
+        break;
+    case Variant::nexos:
+    case Variant::nexos_2:
+        // Tuned for nexos_2
+        set_exploration_constant(0.55f);
+        set_rave_parent_max(50000);
+        break;
+    case Variant::callisto_2:
+        set_exploration_constant(0.4f);
+        set_rave_parent_max(25000);
+        break;
+    }
+}
+
+string Search::get_info() const
+{
+    if (get_nu_simulations() == 0)
+        return {};
+    auto& root = get_tree().get_root();
+    auto nu_children = root.get_nu_children();
+    if (nu_children <= 0)
+        return {};
+    ostringstream s;
+    s << SearchBase::get_info() << "Mov " << 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/libpentobi_mcts/Search.h b/libpentobi_mcts/Search.h
new file mode 100644 (file)
index 0000000..896eeb0
--- /dev/null
@@ -0,0 +1,146 @@
+//-----------------------------------------------------------------------------
+/** @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_base::TimeSource;
+using libboardgame_mcts::PlayerInt;
+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 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. */
+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/libpentobi_mcts/SearchParamConst.h b/libpentobi_mcts/SearchParamConst.h
new file mode 100644 (file)
index 0000000..7074c2e
--- /dev/null
@@ -0,0 +1,83 @@
+//-----------------------------------------------------------------------------
+/** @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 constexpr 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. Note that we do not assume that a color passes only
+        if it has no legal moves because the search might prune legal moves
+        and it could happen that moves are generated again in later
+        positions (although this should only happen in pathological setup
+        positions and we don't want to spend time to try to handle these cases
+        well, the simulation still ends after all colors passed). */
+    static constexpr unsigned max_moves =
+            Color::range * (Color::range * Board::max_pieces + 1);
+
+#ifdef LIBBOARDGAME_MCTS_SINGLE_THREAD
+    static constexpr bool multithread = false;
+#else
+    static constexpr bool multithread = true;
+#endif
+
+    static constexpr bool rave = true;
+
+    static constexpr bool rave_dist_weighting = true;
+
+    static constexpr bool use_lgr = true;
+
+#ifdef PENTOBI_LOW_RESOURCES
+    static constexpr size_t lgr_hash_table_size = (1 << 20);
+#else
+    static constexpr size_t lgr_hash_table_size = (1 << 21);
+#endif
+
+    static constexpr bool virtual_loss = 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/libpentobi_mcts/SharedConst.cpp b/libpentobi_mcts/SharedConst.cpp
new file mode 100644 (file)
index 0000000..93b7ce5
--- /dev/null
@@ -0,0 +1,368 @@
+//-----------------------------------------------------------------------------
+/** @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;
+    [[maybe_unused]] bool found = bc.get_piece_by_name(name, piece);
+    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/libpentobi_mcts/SharedConst.h b/libpentobi_mcts/SharedConst.h
new file mode 100644 (file)
index 0000000..b302027
--- /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_base::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/libpentobi_mcts/State.cpp b/libpentobi_mcts/State.cpp
new file mode 100644 (file)
index 0000000..b1b7da0
--- /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_base/MathUtil.h"
+#include "libpentobi_base/ScoreUtil.h"
+#ifdef LIBBOARDGAME_DEBUG
+#include "libpentobi_base/BoardUtil.h"
+#endif
+
+namespace libpentobi_mcts {
+
+using libboardgame_base::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 (m_is_callisto && s == 0)
+        // Tie a loss for the first color in Callisto, but we use 0.1 to
+        // encourage the second color to win with a positive score
+        res = 0.1f;
+    else if (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& 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 = {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_colors() == 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([[maybe_unused]] size_t 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/libpentobi_mcts/State.h b/libpentobi_mcts/State.h
new file mode 100644 (file)
index 0000000..538b161
--- /dev/null
@@ -0,0 +1,545 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/State.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_STATE_H
+#define LIBPENTOBI_MCTS_STATE_H
+
+#include "PlayoutFeatures.h"
+#include "PriorKnowledge.h"
+#include "SharedConst.h"
+#include "StateUtil.h"
+#include "libboardgame_mcts/LastGoodReply.h"
+#include "libboardgame_mcts/PlayerMove.h"
+#include "libboardgame_base/RandomGenerator.h"
+#include "libboardgame_base/Statistics.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_base::RandomGenerator;
+using libboardgame_base::Statistics;
+using libboardgame_mcts::LastGoodReply;
+using libboardgame_mcts::PlayerInt;
+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>;
+
+    using PlayerMove = libboardgame_mcts::PlayerMove<Move>;
+
+
+    /** 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 The state shared between all threads, which
+        ist not modified during the search. The lifetime of this
+        parameter must exceed the lifetime of the class instance. */
+    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& 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& 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& 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 = {player, lgr2};
+        return true;
+    }
+    Move lgr1 = lgr.get_lgr1(player, last);
+    if (check_lgr(lgr1))
+    {
+        mv = {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([[maybe_unused]] Move mv) const
+{
+    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/libpentobi_mcts/StateUtil.cpp b/libpentobi_mcts/StateUtil.cpp
new file mode 100644 (file)
index 0000000..70cf979
--- /dev/null
@@ -0,0 +1,96 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/StateUtil.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "StateUtil.h"
+
+namespace libpentobi_mcts {
+
+using namespace std;
+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/libpentobi_mcts/StateUtil.h b/libpentobi_mcts/StateUtil.h
new file mode 100644 (file)
index 0000000..aefcc04
--- /dev/null
@@ -0,0 +1,24 @@
+//-----------------------------------------------------------------------------
+/** @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 libpentobi_base::Board;
+
+//-----------------------------------------------------------------------------
+
+bool check_symmetry_broken(const Board& bd);
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_STATE_UTIL_H
diff --git a/libpentobi_mcts/Util.cpp b/libpentobi_mcts/Util.cpp
new file mode 100644 (file)
index 0000000..1d916c6
--- /dev/null
@@ -0,0 +1,115 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/Util.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Util.h"
+
+#include <thread>
+#include "libboardgame_base/Log.h"
+#include "libboardgame_base/Writer.h"
+#include "libpentobi_base/BoardUtil.h"
+#include "libpentobi_base/PentobiSgfUtil.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_base::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();
+    auto children = tree.get_children(node);
+    if (children.empty())
+        return;
+    Color next_to_play = to_play.get_next(get_nu_colors(variant));    
+    vector<const Search::Node*> sorted_children;
+    sorted_children.reserve(children.size());
+    for (auto& i : tree.get_children(node))
+        sorted_children.push_back(&i);
+    sort(sorted_children.begin(), sorted_children.end(), compare_node);
+    for (const auto i : sorted_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/libpentobi_mcts/Util.h b/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/libpentobi_mcts/tests/CMakeLists.txt b/libpentobi_mcts/tests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..d51036f
--- /dev/null
@@ -0,0 +1,10 @@
+add_executable(test_libpentobi_mcts
+  SearchTest.cpp
+)
+
+target_link_libraries(test_libpentobi_mcts
+    boardgame_test_main
+    pentobi_mcts
+    )
+
+add_test(libpentobi_mcts test_libpentobi_mcts)
diff --git a/libpentobi_mcts/tests/SearchTest.cpp b/libpentobi_mcts/tests/SearchTest.cpp
new file mode 100644 (file)
index 0000000..c935e8d
--- /dev/null
@@ -0,0 +1,109 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/tests/SearchTest.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libpentobi_mcts/Search.h"
+
+#include "libboardgame_base/SgfUtil.h"
+#include "libboardgame_base/TreeReader.h"
+#include "libboardgame_test/Test.h"
+#include "libboardgame_base/CpuTimeSource.h"
+#include "libpentobi_base/BoardUpdater.h"
+#include "libpentobi_base/PentobiTree.h"
+
+using namespace std;
+using namespace libpentobi_mcts;
+using libboardgame_base::CpuTimeSource;
+using libboardgame_base::SgfNode;
+using libboardgame_base::TreeReader;
+using libboardgame_base::get_last_node;
+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());
+}
+
+//-----------------------------------------------------------------------------
diff --git a/libpentobi_paint/CMakeLists.txt b/libpentobi_paint/CMakeLists.txt
new file mode 100644 (file)
index 0000000..d7475c3
--- /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=0x060000)
+
+target_link_libraries(pentobi_paint pentobi_base Qt5::Gui)
diff --git a/libpentobi_paint/Paint.cpp b/libpentobi_paint/Paint.cpp
new file mode 100644 (file)
index 0000000..efdb44d
--- /dev/null
@@ -0,0 +1,896 @@
+//-----------------------------------------------------------------------------
+/** @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_base::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] =
+    {
+        {x, y},
+        {x + width, y},
+        {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] =
+    {
+        {x, y + height},
+        {x, y + 0.9 * height},
+        {x + 0.9 * width, y},
+        {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] =
+        {
+            {border, height - border},
+            {width - border, height - border},
+            {width, height},
+            {0, height}
+        };
+    const QPointF right[4] =
+        {
+            {width - border, height - border},
+            {width - border, border},
+            {width, 0},
+            {width, height}
+        };
+    const QPointF up[4] =
+        {
+            {0, 0},
+            {width, 0},
+            {width - border, border},
+            {border, border}
+        };
+    const QPointF left[4] =
+        {
+            {0, 0},
+            {border, border},
+            {border, height - border},
+            {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] =
+    {
+        {0.5 * width, height},
+        {0.5 * width, height - 2 * border},
+        {width - 1.732 * border, border},
+        {width, 0}
+    };
+    const QPointF right[4] =
+    {
+        {0.5 * width, height},
+        {0.5 * width, height - 2 * border},
+        {1.732 * border, border},
+        {0, 0}
+    };
+    const QPointF up[4] =
+    {
+        {width, 0},
+        {width - 1.732 * border, border},
+        {1.732 * border, border},
+        {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] =
+    {
+        {0, height},
+        {width, height},
+        {width - 1.732 * border, height - border},
+        {1.732 * border, height - border}
+    };
+    const QPointF left[4] =
+    {
+        {0.5 * width, 0},
+        {0.5 * width, 2 * border},
+        {1.732 * border, height - border},
+        {0, height}
+    };
+    const QPointF right[4] =
+    {
+        {0.5 * width, 0},
+        {0.5 * width, 2 * border},
+        {width - 1.732 * border, height - border},
+        {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);
+        painter.save();
+        painter.translate(x * gridWidth, y * gridHeight);
+        painter.scale(gridWidth, gridHeight);
+        painter.fillRect(QRectF(0, 0, 1, 1), base);
+        painter.save();
+        painter.translate(0.025, 0.025);
+        painter.scale(0.95, 0.95);
+        if (CallistoGeometry::is_center_section(x, y, nuColors))
+        {
+            painter.fillRect(QRectF(0, 0, 1, 1), centerBase);
+            paintSquareFrame(painter, 0, 0, 1, 1, centerDark, centerLight);
+        }
+        else
+            paintSquareFrame(painter, 0, 0, 1, 1, dark, light);
+        painter.restore();
+        painter.restore();
+    }
+}
+
+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;
+    qreal 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] =
+    {
+        {distX, 0},
+        {width - distX, 0},
+        {width, distY},
+        {width, height - distY},
+        {width - distX, height},
+        {distX, height},
+        {0, height - distY},
+        {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] =
+    {
+        {dist, 0},
+        {width - dist, 0},
+        {width, height / 2},
+        {width - dist, height},
+        {dist, height},
+        {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)]);
+        bool hasRightDown =
+                (geo.is_onboard(x + 1, y + 1)
+                 && pieceId[p] == pieceId[geo.get_point(x + 1, y + 1)]);
+        painter.save();
+        painter.translate((x + 0.025) * gridWidth, (y + 0.025) * gridHeight);
+        painter.scale(0.95 * gridWidth, 0.95 * gridHeight);
+        if (! hasLeft && ! hasRight && ! hasUp && ! hasDown)
+            paintCallistoOnePiece(painter, 0, 0, 1, 1,
+                                  base[c], light[c], dark[c]);
+        else
+        {
+            auto d = 0.05 / 0.95;
+            if (hasRight)
+                painter.fillRect(QRectF(1, 0, d, 1), base[c]);
+            if (hasDown)
+                painter.fillRect(QRectF(0, 1, 1, d), base[c]);
+            if (hasRightDown)
+                painter.fillRect(QRectF(1, 1, d, d), base[c]);
+            paintSquare(painter, 0, 0, 1, 1, base[c], light[c], dark[c]);
+        }
+        painter.restore();
+    }
+}
+
+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] =
+    {
+        {x + dx, y + height},
+        {x + width, y + height},
+        {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] =
+    {
+        {x, y},
+        {x + width, y},
+        {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] =
+    {
+        {x, y + height},
+        {x + width, y + height},
+        {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/libpentobi_paint/Paint.h b/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/libpentobi_thumbnail/CMakeLists.txt b/libpentobi_thumbnail/CMakeLists.txt
new file mode 100644 (file)
index 0000000..07977fe
--- /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=0x060000)
+
+target_link_libraries(pentobi_thumbnail pentobi_paint)
diff --git a/libpentobi_thumbnail/CreateThumbnail.cpp b/libpentobi_thumbnail/CreateThumbnail.cpp
new file mode 100644 (file)
index 0000000..1a7e014
--- /dev/null
@@ -0,0 +1,184 @@
+//-----------------------------------------------------------------------------
+/** @file libpentobi_thumbnail/CreateThumbnail.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CreateThumbnail.h"
+
+#include <QPainter>
+#include "libboardgame_base/TreeReader.h"
+#include "libpentobi_base/NodeUtil.h"
+#include "libpentobi_paint/Paint.h"
+
+using namespace std;
+using libboardgame_base::SgfError;
+using libboardgame_base::SgfNode;
+using libboardgame_base::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)
+{
+    try
+    {
+        image.fill(Qt::transparent);
+        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);
+        if (! painter.isActive())
+            return false;
+        painter.translate(QPointF((width - paintWidth) / 2,
+                                  (height - paintHeight) / 2));
+        libpentobi_paint::paint(painter, paintWidth, paintHeight, variant,
+                                *geo, pointState, pieceId);
+        return true;
+    }
+    catch (const SgfError&)
+    {
+        return false;
+    }
+    catch (const TreeReader::ReadError&)
+    {
+        return false;
+    }
+}
+
+//-----------------------------------------------------------------------------
diff --git a/libpentobi_thumbnail/CreateThumbnail.h b/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/opening_books/book_callisto.blksgf b/opening_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/opening_books/book_callisto_2.blksgf b/opening_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/opening_books/book_callisto_2_4.blksgf b/opening_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/opening_books/book_callisto_3.blksgf b/opening_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/opening_books/book_classic.blksgf b/opening_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/opening_books/book_classic_2.blksgf b/opening_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/opening_books/book_classic_3.blksgf b/opening_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/opening_books/book_duo.blksgf b/opening_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/opening_books/book_gembloq.blksgf b/opening_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/opening_books/book_gembloq_2.blksgf b/opening_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/opening_books/book_gembloq_2_4.blksgf b/opening_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/opening_books/book_gembloq_3.blksgf b/opening_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/opening_books/book_junior.blksgf b/opening_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/opening_books/book_nexos.blksgf b/opening_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/opening_books/book_nexos_2.blksgf b/opening_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/opening_books/book_trigon.blksgf b/opening_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/opening_books/book_trigon_2.blksgf b/opening_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/opening_books/book_trigon_3.blksgf b/opening_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/opening_books/pentobi_books.qrc b/opening_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/pentobi/AnalyzeGameModel.cpp b/pentobi/AnalyzeGameModel.cpp
new file mode 100644 (file)
index 0000000..edddd02
--- /dev/null
@@ -0,0 +1,274 @@
+//-----------------------------------------------------------------------------
+/** @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_base/SgfUtil.h"
+
+using libboardgame_base::ArrayList;
+using libboardgame_base::is_main_variation;
+using libboardgame_base::find_root;
+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 GameViewDesktop
+        // 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)
+{
+    auto progressCallback =
+        [&]([[maybe_unused]] unsigned movesAnalyzed,
+            [[maybe_unused]] unsigned totalMoves)
+        {
+            // Use invokeMethod() because callback runs in different thread
+            QMetaObject::invokeMethod(this, "updateElements",
+                                      Qt::BlockingQueuedConnection);
+        };
+    m_analyzeGame.run(*game, *m_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;
+    m_search->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()
+{
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
+    return {this, &m_elements};
+#else
+    return {this, m_elements};
+#endif
+}
+
+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();
+    m_search = &playerModel->getSearch();
+    auto future = QtConcurrent::run(this, &AnalyzeGameModel::asyncRun,
+                                    &gameModel->getGame());
+    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/pentobi/AnalyzeGameModel.h b/pentobi/AnalyzeGameModel.h
new file mode 100644 (file)
index 0000000..1d51105
--- /dev/null
@@ -0,0 +1,118 @@
+//-----------------------------------------------------------------------------
+/** @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;
+
+    Search* m_search;
+
+
+    Q_INVOKABLE void updateElements();
+
+
+    void asyncRun(const Game* game);
+
+    void setIsRunning(bool isRunning);
+
+    void setMarkMoveNumber(int markMoveNumber);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_ANALYZE_GAME_MODEL_H
diff --git a/pentobi/AndroidUtils.cpp b/pentobi/AndroidUtils.cpp
new file mode 100644 (file)
index 0000000..30296b6
--- /dev/null
@@ -0,0 +1,152 @@
+//-----------------------------------------------------------------------------
+/** @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 <QAndroidJniObject>
+#include <QDir>
+#include <QDirIterator>
+#include <QHash>
+#include <QtAndroid>
+#endif
+
+//-----------------------------------------------------------------------------
+
+bool AndroidUtils::checkPermission([[maybe_unused]] const QString& permission)
+{
+#ifdef Q_OS_ANDROID
+    using QtAndroid::PermissionResult;
+    if (QtAndroid::checkPermission(permission) == PermissionResult::Denied)
+    {
+        QStringList permissions;
+        permissions << permission;
+        auto result = QtAndroid::requestPermissionsSync(permissions);
+        if (result[permission] == PermissionResult::Denied)
+            return false;
+    }
+#endif
+    return true;
+}
+
+QUrl AndroidUtils::extractHelp([[maybe_unused]] 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
+    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([[maybe_unused]] const QString& pathname)
+{
+#ifdef Q_OS_ANDROID
+    // Corresponding Java code:
+    //   sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
+    //                         Uri.fromFile(File(pathname).getAbsoluteFile())))
+    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());
+#endif
+}
+
+//-----------------------------------------------------------------------------
diff --git a/pentobi/AndroidUtils.h b/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/pentobi/CMakeLists.txt b/pentobi/CMakeLists.txt
new file mode 100644 (file)
index 0000000..8a7d232
--- /dev/null
@@ -0,0 +1,90 @@
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+
+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(Qt5WebView 5.11 QUIET)
+
+qt5_add_translation(pentobi_QM
+    qml/i18n/qml_de.ts
+    qml/i18n/qml_es.ts
+    qml/i18n/qml_fr.ts
+    qml/i18n/qml_nb_NO.ts
+    qml/i18n/qml_ru.ts
+    qml/i18n/qml_zh_CN.ts
+    OPTIONS -removeidentical -nounfinished
+    )
+add_custom_command(
+    OUTPUT "translations.qrc"
+    COMMAND ${CMAKE_COMMAND} -E copy
+    "${CMAKE_CURRENT_SOURCE_DIR}/qml/i18n/translations.qrc" .
+    DEPENDS qml/i18n/translations.qrc ${pentobi_QM}
+    )
+
+add_executable(pentobi WIN32
+    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
+    "${CMAKE_CURRENT_BINARY_DIR}/translations.qrc"
+    ../opening_books/pentobi_books.qrc
+    icon/pentobi_icon.qrc
+    resources.qrc
+    resources_desktop.qrc
+    qml/themes/themes.qrc
+    )
+
+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=0x060000
+    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()
+    if(NOT Qt5WebView_FOUND AND NOT PENTOBI_OPEN_HELP_EXTERNALLY)
+        message(FATAL_ERROR
+            "Qt5WebView not found. Install Qt5WebView or set"
+            " PENTOBI_OPEN_HELP_EXTERNALLY to make Pentobi use an external"
+            " browser for displaying help.")
+    endif()
+    target_compile_definitions(pentobi PRIVATE PENTOBI_OPEN_HELP_EXTERNALLY)
+endif()
+
+install(TARGETS pentobi DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+add_subdirectory(docbook)
+if(UNIX)
+    add_subdirectory(unix)
+endif()
diff --git a/pentobi/GameModel.cpp b/pentobi/GameModel.cpp
new file mode 100644 (file)
index 0000000..6949430
--- /dev/null
@@ -0,0 +1,1901 @@
+//-----------------------------------------------------------------------------
+/** @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_base/SgfUtil.h"
+#include "libboardgame_base/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_base::back_to_main_variation;
+using libboardgame_base::beginning_of_branch;
+using libboardgame_base::find_next_comment;
+using libboardgame_base::get_last_node;
+using libboardgame_base::get_letter_coord;
+using libboardgame_base::has_comment;
+using libboardgame_base::has_earlier_variation;
+using libboardgame_base::is_main_variation;
+using libboardgame_base::ArrayList;
+using libboardgame_base::SgfError;
+using libboardgame_base::TreeReader;
+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::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({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 : as_const(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 : as_const(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)];
+}
+
+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();
+    m_game.set_charset("UTF-8");
+    m_textCodec = QTextCodec::codecForName("UTF-8");
+    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);
+    }
+    // Sanitize isModified if value from settings is inconsistent
+    if (file.isEmpty() && ! libboardgame_base::is_empty(m_game.get_tree()))
+        isModified = true;
+    setIsModified(isModified);
+    restoreAutoSaveLocation();
+    updateProperties();
+    return true;
+}
+
+void GameModel::loadRecentFiles()
+{
+    QSettings settings;
+    m_recentFiles =
+            settings.value(QStringLiteral("recentFiles")).toStringList();
+    QMutableListIterator 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 : as_const(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());
+    bool result;
+    if (openStream(in))
+    {
+        auto& root = m_game.get_root();
+        if (! has_setup(root) && root.has_children())
+            goEnd();
+        result = true;
+    }
+    else
+        result = false;
+    clearFile();
+    setIsModified(true);
+    return result;
+}
+
+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 : as_const(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 timeChanged();
+    updateIsModified();
+}
+
+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_base::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/pentobi/GameModel.h b/pentobi/GameModel.h
new file mode 100644 (file)
index 0000000..2b2bff8
--- /dev/null
@@ -0,0 +1,691 @@
+//-----------------------------------------------------------------------------
+/** @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_base::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 constexpr 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 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 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/pentobi/ImageProvider.cpp b/pentobi/ImageProvider.cpp
new file mode 100644 (file)
index 0000000..65d7e51
--- /dev/null
@@ -0,0 +1,93 @@
+//-----------------------------------------------------------------------------
+/** @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,
+    // the image might be requested temporarily with a useless sourceSize, for
+    // example 0 or negative if scaleUnplayed of a piece is 0, or width 1 but
+    // height greater 1 for a square because the width and height properties
+    // are updated in two steps. In theses cases, 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() <= 1 || requestedSize.height() <= 1)
+        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 (name == "quarter-square" && splitRef.size() == 3)
+    {
+        QColor base(splitRef[1]);
+        QColor light(splitRef[2]);
+        paintQuarterSquare(painter, 0, 0, width, height, base, light);
+    }
+    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 == "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/pentobi/ImageProvider.h b/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/pentobi/Main.cpp b/pentobi/Main.cpp
new file mode 100644 (file)
index 0000000..cb1b84a
--- /dev/null
@@ -0,0 +1,278 @@
+//-----------------------------------------------------------------------------
+/** @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_base/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("globalStyle"), QString());
+    ctx->setContextProperty(QStringLiteral("initialFile"), QString());
+    ctx->setContextProperty(QStringLiteral("isDesktop"), QVariant(false));
+#ifdef QT_DEBUG
+    ctx->setContextProperty(QStringLiteral("isDebug"), QVariant(true));
+#else
+    ctx->setContextProperty(QStringLiteral("isDebug"), QVariant(false));
+#endif
+    ctx->setContextProperty(QStringLiteral("openHelpExternally"),
+                            QVariant(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(QStringLiteral(":/pentobi_icon/pentobi-128.svg"));
+    QGuiApplication::setWindowIcon(icon);
+    QGuiApplication::setDesktopFileName(
+                QStringLiteral("io.sourceforge.pentobi"));
+    QCommandLineParser parser;
+    parser.setApplicationDescription(
+                QCoreApplication::translate(
+                    "main",
+                    "computer opponent for the board game Blokus"));
+    auto maxSupportedLevel = Player::max_supported_level;
+    QCommandLineOption optionMaxLevel(
+                QStringLiteral("maxlevel"),
+                //: Description for command line option --maxlevel
+                QCoreApplication::translate(
+                    "main", "Set maximum level to <n>."),
+                QStringLiteral("n"),
+                QString::number(PlayerModel::maxLevel));
+    parser.addOption(optionMaxLevel);
+    QCommandLineOption optionNoBook(
+                QStringLiteral("nobook"),
+                //: Description for command line option --nobook
+                QCoreApplication::translate(
+                    "main", "Do not use opening books."));
+    QCommandLineOption optionMobile(
+                QStringLiteral("mobile"),
+                //: Description for command line option --mobile
+                QCoreApplication::translate(
+                    "main", "Use layout optimized for smartphones."));
+    parser.addOption(optionMobile);
+    parser.addOption(optionNoBook);
+    QCommandLineOption optionNoDelay(
+                QStringLiteral("nodelay"),
+                //: Description for command line option --nodelay
+                QCoreApplication::translate(
+                    "main", "Do not delay fast computer moves."));
+    parser.addOption(optionNoDelay);
+    QCommandLineOption optionSeed(
+                QStringLiteral("seed"),
+                //: Description for command line option --seed
+                QCoreApplication::translate(
+                    "main", "Set random seed to <n>."),
+                QStringLiteral("n"));
+    parser.addOption(optionSeed);
+    QCommandLineOption optionThreads(
+                QStringLiteral("threads"),
+                //: Description for command line option --threads
+                QCoreApplication::translate(
+                    "main", "Use <n> threads (0=auto)."),
+                QStringLiteral("n"));
+    parser.addOption(optionThreads);
+#ifndef LIBBOARDGAME_DISABLE_LOG
+    QCommandLineOption optionVerbose(
+                QStringLiteral("verbose"),
+                //: Description for command line option --verbose
+                QCoreApplication::translate(
+                    "main",
+                    "Print logging information to standard error."));
+    parser.addOption(optionVerbose);
+#endif
+    parser.addPositionalArgument(
+                //: Name of command line argument.
+                QCoreApplication::translate("main", "file.blksgf"),
+                QCoreApplication::translate(
+                    "main",
+                    //: Description of command line argument.
+                    "Blokus SGF file to open (optional)."));
+    parser.addHelpOption();
+    parser.addVersionOption();
+    parser.process(*QCoreApplication::instance());
+    try
+    {
+#ifndef LIBBOARDGAME_DISABLE_LOG
+        if (! parser.isSet(optionVerbose))
+            libboardgame_base::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 QCoreApplication::translate(
+                    "main", "--maxlevel must be between 1 and %1")
+                .arg(maxSupportedLevel);
+        PlayerModel::maxLevel = maxLevel;
+        if (parser.isSet(optionSeed))
+        {
+            auto seed = parser.value(optionSeed).toUInt(&ok);
+            if (! ok)
+                throw QCoreApplication::translate(
+                        "main", "--seed must be a positive number");
+            libboardgame_base::RandomGenerator::set_global_seed(seed);
+        }
+        if (parser.isSet(optionThreads))
+        {
+            auto nuThreads = parser.value(optionThreads).toUInt(&ok);
+            if (! ok)
+                throw QCoreApplication::translate(
+                        "main", "--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 QCoreApplication::translate("main", "Too many arguments");
+        if (! args.empty())
+            initialFile = args.at(0);
+        auto style = QQuickStyle::name();
+        // Fusion is broken on Fedora 31 (QTBUG-77107)
+        //if (style.isEmpty() && isDesktop)
+        //{
+        //    style = QStringLiteral("Fusion");
+        //    QQuickStyle::setStyle(style);
+        //}
+        QQmlApplicationEngine engine;
+        engine.addImageProvider(QStringLiteral("pentobi"), new ImageProvider);
+        auto ctx = engine.rootContext();
+        ctx->setContextProperty(QStringLiteral("globalStyle"), style);
+        ctx->setContextProperty(QStringLiteral("initialFile"), initialFile);
+        ctx->setContextProperty(QStringLiteral("isDesktop"),
+                                QVariant(isDesktop));
+#ifdef QT_DEBUG
+        ctx->setContextProperty(QStringLiteral("isDebug"), QVariant(true));
+#else
+        ctx->setContextProperty(QStringLiteral("isDebug"), QVariant(false));
+#endif
+        // Prefer help from build directory if executable was not installed
+        auto helpDir =
+                QCoreApplication::applicationDirPath() + "/docbook/help";
+        if (! QFile::exists(helpDir + "/C/pentobi/index.html"))
+        {
+#ifdef PENTOBI_HELP_DIR
+            helpDir = QString::fromLocal8Bit(PENTOBI_HELP_DIR);
+#else
+            helpDir.clear();
+#endif
+        }
+        ctx->setContextProperty(QStringLiteral("helpDir"), helpDir);
+#ifdef PENTOBI_OPEN_HELP_EXTERNALLY
+        ctx->setContextProperty(QStringLiteral("openHelpExternally"),
+                                QVariant(true));
+#else
+        ctx->setContextProperty(QStringLiteral("openHelpExternally"),
+                                QVariant(false));
+#endif
+        engine.load(QStringLiteral("qrc:///qml/Main.qml"));
+        if (engine.rootObjects().empty())
+            return 1;
+        return QGuiApplication::exec();
+    }
+    catch (const QString& s)
+    {
+        cerr << s.toLocal8Bit().constData() << '\n';
+        return 1;
+    }
+    catch (const exception& e)
+    {
+        cerr << e.what() << '\n';
+        return 1;
+    }
+}
+
+#endif // Q_OS_ANDROID
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char *argv[])
+{
+    libboardgame_base::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");
+    qmlRegisterUncreatableType<AnalyzeGameElement>(
+                "pentobi", 1, 0, "AnalyzeGameElement", {});
+    qmlRegisterUncreatableType<GameMove>("pentobi", 1, 0, "GameMove", {});
+    qmlRegisterUncreatableType<PieceModel>("pentobi", 1, 0, "PieceModel", {});
+#ifndef Q_OS_ANDROID
+    QTranslator qtTranslator;
+    qtTranslator.load(
+                "qt_" + QLocale::system().name(),
+                QLibraryInfo::location(QLibraryInfo::TranslationsPath));
+    QCoreApplication::installTranslator(&qtTranslator);
+#endif
+    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/pentobi/Pentobi.pro b/pentobi/Pentobi.pro
new file mode 100644 (file)
index 0000000..aa40a6a
--- /dev/null
@@ -0,0 +1,276 @@
+#############################################################################
+# 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 with Qt <5.15. Note that this project
+# does not work with multi-ABI builds, select only a single ABI in the qmake
+# build settings (last tested with Qt 5.15.1, QtCreator 4.13.2).
+#############################################################################
+
+lessThan(QT_MAJOR_VERSION, 5) {
+    error("Qt >=5.12 required")
+}
+equals(QT_MAJOR_VERSION, 5):lessThan(QT_MINOR_VERSION, 12) {
+    error("Qt >=5.12 required")
+}
+
+TEMPLATE = app
+
+QT += concurrent quickcontrols2 svg webview
+android { QT += androidextras }
+
+INCLUDEPATH += ..
+CONFIG += c++17 qtquickcompiler
+DEFINES += QT_DEPRECATED_WARNINGS
+DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000
+DEFINES += QT_NO_NARROWING_CONVERSIONS_IN_CONNECT
+DEFINES += VERSION=\"\\\"18.3\\\"\"
+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/Assert.cpp \
+    ../libboardgame_base/Barrier.cpp \
+    ../libboardgame_base/CpuTime.cpp \
+    ../libboardgame_base/CpuTimeSource.cpp \
+    ../libboardgame_base/IntervalChecker.cpp \
+    ../libboardgame_base/Log.cpp \
+    ../libboardgame_base/Memory.cpp \
+    ../libboardgame_base/RandomGenerator.cpp \
+    ../libboardgame_base/Rating.cpp \
+    ../libboardgame_base/Reader.cpp \
+    ../libboardgame_base/RectTransform.cpp \
+    ../libboardgame_base/SgfError.cpp \
+    ../libboardgame_base/SgfNode.cpp \
+    ../libboardgame_base/SgfTree.cpp \
+    ../libboardgame_base/SgfUtil.cpp \
+    ../libboardgame_base/StringRep.cpp \
+    ../libboardgame_base/StringUtil.cpp \
+    ../libboardgame_base/TimeIntervalChecker.cpp \
+    ../libboardgame_base/Timer.cpp \
+    ../libboardgame_base/TimeSource.cpp \
+    ../libboardgame_base/Transform.cpp \
+    ../libboardgame_base/TreeReader.cpp \
+    ../libboardgame_base/TreeWriter.cpp \
+    ../libboardgame_base/WallTimeSource.cpp \
+    ../libboardgame_base/Writer.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 += \
+    ../opening_books/pentobi_books.qrc \
+    icon/pentobi_icon.qrc \
+    qml/themes/themes.qrc \
+    resources.qrc
+
+!android {
+    RESOURCES += resources_desktop.qrc
+}
+
+HEADERS += \
+    AnalyzeGameModel.h \
+    AndroidUtils.h \
+    GameModel.h \
+    ImageProvider.h \
+    PieceModel.h \
+    PlayerModel.h \
+    RatingModel.h \
+    SyncSettings.h \
+    ../libboardgame_base/ArrayList.h \
+    ../libboardgame_base/Assert.h \
+    ../libboardgame_base/Barrier.h \
+    ../libboardgame_base/Compiler.h \
+    ../libboardgame_base/CoordPoint.h \
+    ../libboardgame_base/CpuTime.h \
+    ../libboardgame_base/CpuTimeSource.h \
+    ../libboardgame_base/FmtSaver.h \
+    ../libboardgame_base/Geometry.h \
+    ../libboardgame_base/GeometryUtil.h \
+    ../libboardgame_base/Grid.h \
+    ../libboardgame_base/IntervalChecker.h \
+    ../libboardgame_base/Log.h \
+    ../libboardgame_base/Marker.h \
+    ../libboardgame_base/MathUtil.h \
+    ../libboardgame_base/Memory.h \
+    ../libboardgame_base/Options.h \
+    ../libboardgame_base/Point.h \
+    ../libboardgame_base/PointTransform.h \
+    ../libboardgame_base/RandomGenerator.h \
+    ../libboardgame_base/Rating.h \
+    ../libboardgame_base/RectGeometry.h \
+    ../libboardgame_base/RectTransform.h \
+    ../libboardgame_base/Statistics.h \
+    ../libboardgame_base/StringRep.h \
+    ../libboardgame_base/StringUtil.h \
+    ../libboardgame_base/TimeIntervalChecker.h \
+    ../libboardgame_base/Timer.h \
+    ../libboardgame_base/TimeSource.h \
+    ../libboardgame_base/Transform.h \
+    ../libboardgame_base/WallTimeSource.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_base/Reader.h \
+    ../libboardgame_base/SgfError.h \
+    ../libboardgame_base/SgfNode.h \
+    ../libboardgame_base/SgfTree.h \
+    ../libboardgame_base/SgfUtil.h \
+    ../libboardgame_base/TreeReader.h \
+    ../libboardgame_base/Writer.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_es.ts \
+    qml/i18n/qml_fr.ts \
+    qml/i18n/qml_nb_NO.ts \
+    qml/i18n/qml_ru.ts \
+    qml/i18n/qml_zh_CN.ts
+
+qtPrepareTool(LRELEASE, lrelease)
+update_qm.input = TRANSLATIONS
+update_qm.output = $$OUT_PWD/${QMAKE_FILE_BASE}.qm
+update_qm.commands = $$LRELEASE -removeidentical -nounfinished \
+    ${QMAKE_FILE_IN} -qm ${QMAKE_FILE_OUT}
+update_qm.CONFIG += no_link target_predeps
+QMAKE_EXTRA_COMPILERS += update_qm
+
+COPY_QRC = \
+    qml/i18n/translations.qrc \
+    docbook/help.qrc
+copy_qrc.input = COPY_QRC
+copy_qrc.output = $$OUT_PWD/${QMAKE_FILE_BASE}.qrc
+copy_qrc.commands = $$QMAKE_COPY_FILE ${QMAKE_FILE_IN} ${QMAKE_FILE_OUT}
+copy_qrc.variable_out = RESOURCES
+QMAKE_EXTRA_COMPILERS += copy_qrc
+
+GEN_HELP_INPUT = docbook/index.docbook
+# Currently ignores dependencies on figures, custom.xml, and po files
+gen_help.input = GEN_HELP_INPUT
+gen_help.output = $$OUT_PWD/help/C/pentobi/index.html
+gen_help.commands = ${QMAKE_FILE_IN_PATH}/create-html $$OUT_PWD
+gen_help.CONFIG += no_link target_predeps
+QMAKE_EXTRA_COMPILERS += gen_help
+
+OTHER_FILES += android/AndroidManifest.xml
+
+ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
+
diff --git a/pentobi/PieceModel.cpp b/pentobi/PieceModel.cpp
new file mode 100644 (file)
index 0000000..80c5133
--- /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::ArrayList;
+using libboardgame_base::CoordPoint;
+using libboardgame_base::TransfIdentity;
+using libboardgame_base::TransfRectRot90;
+using libboardgame_base::TransfRectRot180;
+using libboardgame_base::TransfRectRot270;
+using libboardgame_base::TransfRectRefl;
+using libboardgame_base::TransfRectRot90Refl;
+using libboardgame_base::TransfRectRot180Refl;
+using libboardgame_base::TransfRectRot270Refl;
+using libpentobi_base::BoardType;
+using libpentobi_base::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({p.x - 1, p. y});
+                candidates.include({p.x + 1, p. y});
+            }
+            else if (pointType == 2)
+            {
+                candidates.include({p.x, p. y - 1});
+                candidates.include({p.x, p. y + 1});
+            }
+        }
+        m_junctions.reserve(candidates.size());
+        m_junctionType.reserve(candidates.size());
+        for (auto& p : candidates)
+        {
+            bool hasLeft = points.contains({p.x - 1, p. y});
+            bool hasRight = points.contains({p.x + 1, p. y});
+            bool hasUp = points.contains({p.x, p. y - 1});
+            bool hasDown = points.contains({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);
+        }
+    }
+    else if (isCallisto)
+        for (auto& p : points)
+        {
+            bool hasRight = points.contains({p.x + 1, p. y});
+            bool hasDown = points.contains({p.x, p.y + 1});
+            bool hasRightDown = points.contains({p.x + 1, p.y + 1});
+            int junctionType = 0;
+            if (hasRight)
+                junctionType |= 1;
+            if (hasDown)
+                junctionType |= 2;
+            if (hasRightDown)
+                junctionType |= 4;
+            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("60"))
+            return transforms.find<TransfTrigonRot60>();
+        if (state == QStringLiteral("120"))
+            return transforms.find<TransfTrigonRot120>();
+        if (state == QStringLiteral("180"))
+            return transforms.find<TransfTrigonRot180>();
+        if (state == QStringLiteral("240"))
+            return transforms.find<TransfTrigonRot240>();
+        if (state == QStringLiteral("300"))
+            return transforms.find<TransfTrigonRot300>();
+        if (state == QStringLiteral("flip"))
+            return transforms.find<TransfTrigonReflRot180>();
+        if (state == QStringLiteral("60flip"))
+            return transforms.find<TransfTrigonReflRot120>();
+        if (state == QStringLiteral("120flip"))
+            return transforms.find<TransfTrigonReflRot60>();
+        if (state == QStringLiteral("180flip"))
+            return transforms.find<TransfTrigonRefl>();
+        if (state == QStringLiteral("240flip"))
+            return transforms.find<TransfTrigonReflRot300>();
+        if (state == QStringLiteral("300flip"))
+            return transforms.find<TransfTrigonReflRot240>();
+        return transforms.find<TransfTrigonIdentity>();
+    }
+    if (pieceSet == PieceSet::gembloq)
+    {
+        if (state == QStringLiteral("90"))
+            return transforms.find<TransfGembloQRot90>();
+        if (state == QStringLiteral("180"))
+            return transforms.find<TransfGembloQRot180>();
+        if (state == QStringLiteral("270"))
+            return transforms.find<TransfGembloQRot270>();
+        if (state == QStringLiteral("flip"))
+            return transforms.find<TransfGembloQRot180Refl>();
+        if (state == QStringLiteral("90flip"))
+            return transforms.find<TransfGembloQRot90Refl>();
+        if (state == QStringLiteral("180flip"))
+            return transforms.find<TransfGembloQRefl>();
+        if (state == QStringLiteral("270flip"))
+            return transforms.find<TransfGembloQRot270Refl>();
+        return transforms.find<TransfGembloQIdentity>();
+    }
+    if (state == QStringLiteral("90"))
+        return transforms.find<TransfRectRot90>();
+    if (state == QStringLiteral("180"))
+        return transforms.find<TransfRectRot180>();
+    if (state == QStringLiteral("270"))
+        return transforms.find<TransfRectRot270>();
+    if (state == QStringLiteral("flip"))
+        return transforms.find<TransfRectRot180Refl>();
+    if (state == QStringLiteral("90flip"))
+        return transforms.find<TransfRectRot90Refl>();
+    if (state == QStringLiteral("180flip"))
+        return transforms.find<TransfRectRefl>();
+    if (state == QStringLiteral("270flip"))
+        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;
+        qreal 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("60");
+        else if (dynamic_cast<const TransfTrigonRot120*>(transform))
+            state = QStringLiteral("120");
+        else if (dynamic_cast<const TransfTrigonRot180*>(transform))
+            state = QStringLiteral("180");
+        else if (dynamic_cast<const TransfTrigonRot240*>(transform))
+            state = QStringLiteral("240");
+        else if (dynamic_cast<const TransfTrigonRot300*>(transform))
+            state = QStringLiteral("300");
+        else if (dynamic_cast<const TransfTrigonReflRot180*>(transform))
+            state = QStringLiteral("flip");
+        else if (dynamic_cast<const TransfTrigonReflRot120*>(transform))
+            state = QStringLiteral("60flip");
+        else if (dynamic_cast<const TransfTrigonReflRot60*>(transform))
+            state = QStringLiteral("120flip");
+        else if (dynamic_cast<const TransfTrigonRefl*>(transform))
+            state = QStringLiteral("180flip");
+        else if (dynamic_cast<const TransfTrigonReflRot300*>(transform))
+            state = QStringLiteral("240flip");
+        else if (dynamic_cast<const TransfTrigonReflRot240*>(transform))
+            state = QStringLiteral("300flip");
+    }
+    else if (pieceSet == PieceSet::gembloq)
+    {
+        if (dynamic_cast<const TransfGembloQRot90*>(transform))
+            state = QStringLiteral("90");
+        else if (dynamic_cast<const TransfGembloQRot180*>(transform))
+            state = QStringLiteral("180");
+        else if (dynamic_cast<const TransfGembloQRot270*>(transform))
+            state = QStringLiteral("270");
+        else if (dynamic_cast<const TransfGembloQRot180Refl*>(transform))
+            state = QStringLiteral("flip");
+        else if (dynamic_cast<const TransfGembloQRot90Refl*>(transform))
+            state = QStringLiteral("90flip");
+        else if (dynamic_cast<const TransfGembloQRefl*>(transform))
+            state = QStringLiteral("180flip");
+        else if (dynamic_cast<const TransfGembloQRot270Refl*>(transform))
+            state = QStringLiteral("270flip");
+    }
+    else
+    {
+        if (dynamic_cast<const TransfRectRot90*>(transform))
+            state = QStringLiteral("90");
+        else if (dynamic_cast<const TransfRectRot180*>(transform))
+            state = QStringLiteral("180");
+        else if (dynamic_cast<const TransfRectRot270*>(transform))
+            state = QStringLiteral("270");
+        else if (dynamic_cast<const TransfRectRot180Refl*>(transform))
+            state = QStringLiteral("flip");
+        else if (dynamic_cast<const TransfRectRot90Refl*>(transform))
+            state = QStringLiteral("90flip");
+        else if (dynamic_cast<const TransfRectRefl*>(transform))
+            state = QStringLiteral("180flip");
+        else if (dynamic_cast<const TransfRectRot270Refl*>(transform))
+            state = QStringLiteral("270flip");
+    }
+    if (m_state == state)
+        return;
+    m_state = state;
+    emit stateChanged();
+}
+
+//-----------------------------------------------------------------------------
diff --git a/pentobi/PieceModel.h b/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/pentobi/PlayerModel.cpp b/pentobi/PlayerModel.cpp
new file mode 100644 (file)
index 0000000..332289e
--- /dev/null
@@ -0,0 +1,154 @@
+//-----------------------------------------------------------------------------
+/** @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;
+
+//-----------------------------------------------------------------------------
+
+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)
+{
+    QElapsedTimer timer;
+    timer.start();
+    auto& bd = gm->getBoard();
+    GenMoveResult result;
+    result.color = c;
+    result.gameModel = gm;
+    result.move = m_player->genmove(bd, c);
+    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;
+    m_player->get_search().abort();
+    m_watcher.waitForFinished();
+    setIsGenMoveRunning(false);
+}
+
+void PlayerModel::genMoveFinished()
+{
+    auto result = m_watcher.future().result();
+    if (m_player->was_aborted())
+        return;
+    setIsGenMoveRunning(false);
+    auto& bd = result.gameModel->getBoard();
+    ColorMove mv(result.color, result.move);
+    if (! mv.is_null() && ! bd.is_legal(mv.color, mv.move))
+    {
+        qWarning("PlayerModel: player generated illegal move");
+        mv = ColorMove::null();
+    }
+    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);
+    QFuture<GenMoveResult> future =
+            QtConcurrent::run(this, &PlayerModel::asyncGenMove, gameModel,
+                              bd.get_effective_to_play());
+    m_watcher.setFuture(future);
+    setIsGenMoveRunning(true);
+}
+
+//-----------------------------------------------------------------------------
diff --git a/pentobi/PlayerModel.h b/pentobi/PlayerModel.h
new file mode 100644 (file)
index 0000000..0823de3
--- /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 inline bool noBook = false;
+
+    /** Global variable to disable the minimum thinking time.
+        Must be set before creating any instances of PlayerModel and not be
+        changed afterwards. */
+    static inline bool noDelay = false;
+
+    /** 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 inline unsigned nuThreads = 0;
+
+    /** Global variable to set the maximum level.
+        Must be set before creating any instances of PlayerModel and not be
+        changed afterwards. */
+#ifdef Q_OS_ANDROID
+    static inline unsigned maxLevel = 7;
+#else
+    static inline unsigned maxLevel = 9;
+#endif
+
+
+    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;
+
+        GameModel* gameModel;
+    };
+
+
+    bool m_notEnoughMemory;
+
+    bool m_isGenMoveRunning = false;
+
+    QString m_gameVariant;
+
+    unsigned m_level = 1;
+
+    unique_ptr<Player> m_player;
+
+    QFutureWatcher<GenMoveResult> m_watcher;
+
+
+    GenMoveResult asyncGenMove(GameModel* gm, Color c);
+
+    void genMoveFinished();
+
+    void loadBook(Variant variant);
+
+    void setIsGenMoveRunning(bool isGenMoveRunning);
+
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_PLAYER_MODEL_H
diff --git a/pentobi/RatingModel.cpp b/pentobi/RatingModel.cpp
new file mode 100644 (file)
index 0000000..13a1082
--- /dev/null
@@ -0,0 +1,375 @@
+//-----------------------------------------------------------------------------
+/** @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
+
+//-----------------------------------------------------------------------------
+
+TableModel::TableModel(QObject* parent, const QVector<RatedGameInfo>& history)
+    : QAbstractTableModel(parent),
+      m_history(history)
+{
+}
+
+int TableModel::rowCount([[maybe_unused]] const QModelIndex& parent) const
+{
+    return m_history.length() + 1;
+}
+
+int TableModel::columnCount([[maybe_unused]] const QModelIndex& parent) const
+{
+    return 5;
+}
+
+QVariant TableModel::data(const QModelIndex& index, int role) const
+{
+    auto row = index.row();
+    if (role != Qt::DisplayRole || row < 0 || row >= m_history.length() + 1)
+        return {};
+    if (row == 0)
+    {
+        // We currently put the table headers in row 0 because Qt 5.12 does
+        // not support table headers.
+        switch (index.column())
+        {
+        case 0:
+            //: Table header for game number in rating dialog
+            return tr("Game");
+        case 1:
+            //: Table header for game result in rating dialog
+            return tr("Result");
+        case 2:
+            //: Table header for level in rating dialog
+            return tr("Level");
+        case 3:
+            //: Table header for player color(s) in rating dialog
+            return tr("Your Color");
+        case 4:
+            //: Table header for game date in rating dialog
+            return tr("Date");
+        default:
+            return {};
+        }
+    }
+    auto& info = m_history[row - 1];
+    switch (index.column())
+    {
+    case 0:
+        return info.number;
+    case 1:
+        if (info.result == 1)
+            //: Result of rated game is a win
+            return tr("Win");
+        else if (info.result == 0)
+            //: Result of rated game is a loss
+            return tr("Loss");
+        else
+            //: 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 tr("Tie");
+    case 2:
+        return info.level;
+    case 3:
+        return info.color;
+    case 4:
+        return info.date;
+    default:
+        return {};
+    }
+}
+
+//-----------------------------------------------------------------------------
+
+RatingModel::RatingModel(QObject* parent)
+    : QObject(parent)
+{
+    m_tableModel = new TableModel(this, m_history);
+}
+
+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)
+        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({numberGames, color, level, gameResult, m_rating.get(),
+                       date});
+    auto file = getFile(numberGames);
+    QFileInfo(file).dir().mkpath(QStringLiteral("."));
+    gameModel->save(file);
+    emit ratingHistoryChanged();
+    emit tableModelChanged();
+    setNumberGames(numberGames);
+    saveSettings();
+}
+
+void RatingModel::clearRating()
+{
+    if (! m_history.isEmpty())
+    {
+        m_history.clear();
+        emit ratingHistoryChanged();
+        emit tableModelChanged();
+    }
+    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::getGameNumber(int historyIndex) const
+{
+    if (historyIndex < 0 || historyIndex >= m_history.length())
+        return -1;
+    return m_history[historyIndex].number;
+}
+
+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;
+}
+
+const QVector<qreal>& RatingModel::ratingHistory()
+{
+    m_ratingHistory.clear();
+    m_ratingHistory.reserve(m_history.length());
+    for (auto i = m_history.rbegin(); i != m_history.rend(); ++i)
+        m_ratingHistory.push_back(i->rating);
+    return m_ratingHistory;
+}
+
+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()));
+    }
+    QVector<RatedGameInfo> newHistory;
+    newHistory.reserve(m_history.size());
+    for (auto& info : m_history)
+    {
+        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 ratingHistoryChanged();
+        emit tableModelChanged();
+    }
+    settings.remove(QStringLiteral("rated_game_info"));
+    if (m_numberGames > 0)
+    {
+        settings.beginWriteArray(QStringLiteral("rated_game_info"));
+        int n = 0;
+        for (auto& info : m_history)
+        {
+            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({number, color, level, result, rating, date});
+    }
+    settings.endArray();
+    emit ratingHistoryChanged();
+    emit tableModelChanged();
+    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/pentobi/RatingModel.h b/pentobi/RatingModel.h
new file mode 100644 (file)
index 0000000..443c5bb
--- /dev/null
@@ -0,0 +1,146 @@
+//-----------------------------------------------------------------------------
+/** @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 <QAbstractTableModel>
+#include "libboardgame_base/Rating.h"
+
+class GameModel;
+
+using libboardgame_base::Rating;
+
+//-----------------------------------------------------------------------------
+
+struct RatedGameInfo
+{
+    int number;
+
+    int color;
+
+    int level;
+
+    double result;
+
+    double rating;
+
+    QString date;
+};
+
+//-----------------------------------------------------------------------------
+
+class TableModel
+    : public QAbstractTableModel
+{
+    Q_OBJECT
+
+public:
+    TableModel(QObject* parent, const QVector<RatedGameInfo>& history);
+
+    int rowCount(const QModelIndex& parent) const override;
+
+    int columnCount(const QModelIndex& parent) const override;
+
+    QVariant data(const QModelIndex& index, int role) const override;
+
+private:
+    const QVector<RatedGameInfo>& m_history;
+};
+
+//-----------------------------------------------------------------------------
+
+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(TableModel* tableModel READ tableModel NOTIFY tableModelChanged)
+    Q_PROPERTY(QVector<qreal> ratingHistory READ ratingHistory NOTIFY ratingHistoryChanged)
+    Q_PROPERTY(int numberGames READ numberGames NOTIFY numberGamesChanged)
+    Q_PROPERTY(double rating READ rating NOTIFY ratingChanged)
+
+public:
+    RatingModel(QObject* parent = nullptr);
+
+    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;
+
+    Q_INVOKABLE int getGameNumber(int historyIndex) 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 QVector<qreal>& ratingHistory();
+
+    TableModel* tableModel() { return m_tableModel; }
+
+    int numberGames() const { return m_numberGames; }
+
+    double rating() const { return m_rating.get(); }
+
+    void setGameVariant(const QString& gameVariant);
+
+signals:
+    void bestRatingChanged();
+
+    void gameVariantChanged();
+
+    void ratingHistoryChanged();
+
+    void tableModelChanged();
+
+    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;
+
+    QVector<RatedGameInfo> m_history;
+
+    QVector<qreal> m_ratingHistory;
+
+    TableModel* m_tableModel;
+
+
+    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/pentobi/SyncSettings.h b/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/pentobi/android/AndroidManifest.xml b/pentobi/android/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..10749c5
--- /dev/null
@@ -0,0 +1,73 @@
+<?xml version="1.0"?>
+<manifest package="net.sf.pentobi" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="18.3" android:versionCode="18003000" android:installLocation="auto">
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+    <!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
+         Remove the comment if you do not require these default features. -->
+    <!-- %%INSERT_FEATURES -->
+
+    <supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
+
+    <application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="Pentobi" android:extractNativeLibs="true" android:theme="@style/AppTheme" android:icon="@drawable/icon" android:requestLegacyExternalStorage="true">
+        <activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="net.sf.pentobi.Activity" android:label="Pentobi" android:screenOrientation="portrait" android:launchMode="singleTop">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <!-- Application arguments -->
+            <!-- meta-data android:name="android.app.arguments" android:value="arg1 arg2 arg3"/ -->
+            <!-- Application arguments -->
+            <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%% --"/>
+            <!-- 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_resource_id" android:resource="@array/load_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%% --"/>
+            <!-- Used to specify custom system library path to run with local system libs -->
+            <!-- <meta-data android:name="android.app.system_libs_prefix" android:value="/system/lib/"/> -->
+            <!--  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"/>
+            <meta-data android:value="@string/unsupported_android_version" android:name="android.app.unsupported_android_version"/>
+            <!--  Messages maps -->
+            <!-- Splash screen -->
+            <!-- Orientation-specific (portrait/landscape) data is checked first. If not available for current orientation,
+                 then android.app.splash_screen_drawable. For best results, use together with splash_screen_sticky and
+                 use hideSplashScreen() with a fade-out animation from Qt Android Extras to hide the splash screen when you
+                 are done populating your window with content. -->
+            <meta-data android:name="android.app.splash_screen_drawable_portrait" android:resource="@drawable/splash" />
+            <meta-data android:name="android.app.splash_screen_drawable_landscape" android:resource="@drawable/splash" />
+            <!-- meta-data android:name="android.app.splash_screen_sticky" android:value="true"/ -->
+            <!-- Splash screen -->
+            <!-- Background running -->
+            <!-- Warning: changing this value to true may cause unexpected crashes if the
+                          application still try to draw after
+                          "applicationStateChanged(Qt::ApplicationSuspended)"
+                          signal is sent! -->
+            <meta-data android:name="android.app.background_running" android:value="false"/>
+            <!-- Background running -->
+            <!-- auto screen scale factor -->
+            <meta-data android:name="android.app.auto_screen_scale_factor" android:value="false"/>
+            <!-- auto screen scale factor -->
+            <!-- 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"/>
+            <!-- extract android style -->
+        </activity>
+        <!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
+    </application>
+
+</manifest>
diff --git a/pentobi/android/res/drawable-hdpi/icon.png b/pentobi/android/res/drawable-hdpi/icon.png
new file mode 100644 (file)
index 0000000..51c96af
Binary files /dev/null and b/pentobi/android/res/drawable-hdpi/icon.png differ
diff --git a/pentobi/android/res/drawable-mdpi/icon.png b/pentobi/android/res/drawable-mdpi/icon.png
new file mode 100644 (file)
index 0000000..251f567
Binary files /dev/null and b/pentobi/android/res/drawable-mdpi/icon.png differ
diff --git a/pentobi/android/res/drawable-xhdpi/icon.png b/pentobi/android/res/drawable-xhdpi/icon.png
new file mode 100644 (file)
index 0000000..51affce
Binary files /dev/null and b/pentobi/android/res/drawable-xhdpi/icon.png differ
diff --git a/pentobi/android/res/drawable-xxhdpi/icon.png b/pentobi/android/res/drawable-xxhdpi/icon.png
new file mode 100644 (file)
index 0000000..5155434
Binary files /dev/null and b/pentobi/android/res/drawable-xxhdpi/icon.png differ
diff --git a/pentobi/android/res/drawable-xxxhdpi/icon.png b/pentobi/android/res/drawable-xxxhdpi/icon.png
new file mode 100644 (file)
index 0000000..f46bfc2
Binary files /dev/null and b/pentobi/android/res/drawable-xxxhdpi/icon.png differ
diff --git a/pentobi/android/res/drawable/splash.xml b/pentobi/android/res/drawable/splash.xml
new file mode 100644 (file)
index 0000000..42755cb
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+<item android:drawable="@color/background"/>
+<item><bitmap android:src="@drawable/icon" android:gravity="center"/></item>
+</layer-list>
diff --git a/pentobi/android/res/values/colors.xml b/pentobi/android/res/values/colors.xml
new file mode 100644 (file)
index 0000000..058fd06
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+<color name="background">#131313</color>
+</resources>
diff --git a/pentobi/android/res/values/styles.xml b/pentobi/android/res/values/styles.xml
new file mode 100644 (file)
index 0000000..2711fbf
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+<style name="AppTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
+<item name="android:colorPrimaryDark">@android:color/black</item>
+<item name="android:windowBackground">@color/background</item>
+</style>
+</resources>
diff --git a/pentobi/android/src/net/sf/pentobi/Activity.java b/pentobi/android/src/net/sf/pentobi/Activity.java
new file mode 100644 (file)
index 0000000..673bf2f
--- /dev/null
@@ -0,0 +1,14 @@
+package net.sf.pentobi;
+
+public class Activity
+    extends org.qtproject.qt5.android.bindings.QtActivity
+{
+    public Activity()
+    {
+        super();
+        // Workaround for QTBUG-55600, which causes a white border on
+        // some devices when switching orientation/fullscreen
+        QT_ANDROID_THEMES = new String[] {"Theme_Black_NoTitleBar"};
+        QT_ANDROID_DEFAULT_THEME = "Theme_Black_NoTitleBar";
+    }
+}
diff --git a/pentobi/android_icons_svg/icon48.svg b/pentobi/android_icons_svg/icon48.svg
new file mode 100644 (file)
index 0000000..6d94d9a
--- /dev/null
@@ -0,0 +1,77 @@
+<?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="m13 24h22v-11h-11v-11h-18.615c-1.6923 0-3.3846 1.6923-3.3846 3.3846v7.6153h11z" fill="#e01b24" fill-rule="evenodd" stroke-width=".42308"/>
+ <path id="h" d="m2 13 1.6923-1.6923h7.6154l1.6923 1.6923z" fill="#c6161f" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(11)" width="100%" height="100%" xlink:href="#h"/>
+ <use transform="translate(11 11)" width="100%" height="100%" xlink:href="#h"/>
+ <use transform="translate(22 11)" width="100%" height="100%" xlink:href="#h"/>
+ <path id="n" d="m13 2.0001 1.6923 1.6923h7.6154l1.6923-1.6923z" fill="#e73b43" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(0 11)" width="100%" height="100%" xlink:href="#n"/>
+ <use transform="translate(11 11)" width="100%" height="100%" xlink:href="#n"/>
+ <path id="m" d="m13 2.0001 1.6923 1.6923v7.6153l-1.6923 1.6923z" fill="#e6323a" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(0 11)" width="100%" height="100%" xlink:href="#m"/>
+ <use transform="translate(11 11)" width="100%" height="100%" xlink:href="#m"/>
+ <path id="c" d="m13 13-1.6923-1.6923v-7.6153l1.6923-1.6923z" fill="#cd1922" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(11 11)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="translate(11)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="translate(22 11)" width="100%" height="100%" xlink:href="#c"/>
+ <g fill-rule="evenodd" stroke-width=".42308">
+  <path d="m3.059 3.0756 0.63339 0.61671v7.6153l-1.6923 1.6923v-7.6153c0.00879-1.3712 1.0589-2.309 1.0589-2.309z" fill="#e6323a"/>
+  <path d="m3.0717 3.0692 0.62065 0.62306h7.6154l1.6923-1.6923h-7.6154c-1.4158 0.019119-2.313 1.0692-2.313 1.0692z" fill="#e73b42"/>
+  <path d="m24 2.0001h18.615c1.6765 0.018027 3.3666 1.7261 3.3846 3.3846v29.615h-11v-22h-11z" fill="#2ec27e"/>
+  <path id="g" d="m24 13 1.6923-1.6923h7.6154l1.6923 1.6923z" fill="#2ab073" fill-rule="evenodd" stroke-width=".42308"/>
+ </g>
+ <use transform="translate(11)" width="100%" height="100%" xlink:href="#g"/>
+ <use transform="translate(11 11)" width="100%" height="100%" xlink:href="#g"/>
+ <use transform="translate(11 22)" width="100%" height="100%" xlink:href="#g"/>
+ <path id="f" d="m24 2.0001 1.6923 1.6923v7.6153l-1.6923 1.6923z" fill="#30ca85" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(11)" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(11 22)" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(11 11)" width="100%" height="100%" xlink:href="#f"/>
+ <path id="p" d="m24 2.0001 1.6923 1.6923h7.6154l1.6923-1.6923z" fill="#4fd398" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(11 11)" width="100%" height="100%" xlink:href="#p"/>
+ <use transform="translate(11 22)" width="100%" height="100%" xlink:href="#p"/>
+ <path id="o" d="m35 2.0001-1.6923 1.6923v7.6153l1.6923 1.6923z" fill="#2fbb7c" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(11 11)" width="100%" height="100%" xlink:href="#o"/>
+ <use transform="translate(11 22)" width="100%" height="100%" xlink:href="#o"/>
+ <g transform="matrix(.42308 0 0 .42307 -3.0769 -3.0768)" fill-rule="evenodd">
+  <path d="m90 12 4 4h18l1.4274-1.4594s-2.4274-2.5406-5.4274-2.5406z" fill="#4fd398"/>
+  <path d="m116 20c-0.0151-2.9828-2.5466-5.4534-2.5466-5.4534l-1.4534 1.4534v18l4 4z" fill="#2fbb7c"/>
+  <path d="m12 38v70c0.0213 4.0053 4 7.9997 7.984 7.9997l44.015-2e-3 9.99e-4 -25.998h-26v-52z" fill="#f6d32d"/>
+  <path id="j" d="m38 64h-26l4-4h18z" fill="#e1bf09" fill-rule="evenodd"/>
+ </g>
+ <use transform="matrix(.42308 0 0 .42307 -3.0769 7.9232)" width="100%" height="100%" xlink:href="#j"/>
+ <use transform="matrix(.42308 0 0 .42307 7.9231 18.923)" width="100%" height="100%" xlink:href="#j"/>
+ <path id="a" d="m13 13h-11l1.6923 1.6923h7.6154z" fill="#f9e263" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(0 11)" width="100%" height="100%" xlink:href="#a"/>
+ <use transform="translate(11 22)" width="100%" height="100%" xlink:href="#a"/>
+ <path id="i" d="m2 24v-11l1.6923 1.6923v7.6153z" fill="#f7da51" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(0 11)" width="100%" height="100%" xlink:href="#i"/>
+ <use transform="translate(11 22)" width="100%" height="100%" xlink:href="#i"/>
+ <path id="b" d="m13 13v11l-1.6923-1.6923v-7.6153z" fill="#efc70b" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(0 11)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(11 22)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(0 22)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(0 22)" width="100%" height="100%" xlink:href="#a"/>
+ <g transform="matrix(.42308 0 0 .42307 -3.0769 -3.0768)" fill-rule="evenodd">
+  <path d="m12 108v-18l4 4v18l-1.4487 1.4487s-2.5087-1.9547-2.5513-5.4487z" fill="#f7da51"/>
+  <path d="m38 116h-18c-3.1531 0.0426-5.4487-2.5513-5.4487-2.5513l1.4487-1.4487h18z" fill="#e0bb0a"/>
+  <path d="m38 64h52v26h26v17.931c0 4.1332-3.9521 8.1118-8 8.0692h-44v-26h-26z" fill="#1c71d8"/>
+  <path id="l" d="m38 90 4-4h18l4 4z" fill="#1a68c7" fill-rule="evenodd"/>
+ </g>
+ <use transform="matrix(.42308 0 0 .42307 7.9231 -3.0768)" width="100%" height="100%" xlink:href="#l"/>
+ <use transform="matrix(.42308 0 0 .42307 7.9227 7.9223)" width="100%" height="100%" xlink:href="#l"/>
+ <path d="m35 46h7.6154c1.334 0.01802 2.3052-1.0794 2.3052-1.0794l-0.61291-0.61291h-7.6154z" fill="#1a68c7" fill-rule="evenodd" stroke-width=".42308"/>
+ <path id="e" d="m13 24 1.6923 1.6923h7.6154l1.6923-1.6923z" fill="#3585e5" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(11)" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(11 11)" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(22 11)" width="100%" height="100%" xlink:href="#e"/>
+ <path id="d" d="m13 35 1.6923-1.6923v-7.6153l-1.6923-1.6923z" fill="#2078e2" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(11)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(11 11)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(22 10.999)" width="100%" height="100%" xlink:href="#d"/>
+ <path id="k" d="m24 35-1.6923-1.6923v-7.6153l1.6923-1.6923z" fill="#1b6bce" fill-rule="evenodd" stroke-width=".42308"/>
+ <use transform="translate(11)" width="100%" height="100%" xlink:href="#k"/>
+ <use transform="translate(11 11)" width="100%" height="100%" xlink:href="#k"/>
+ <path d="m46 42.615v-7.6153l-1.6923 1.6923v7.6153l0.61291 0.61291s1.0614-0.82698 1.0794-2.3052z" fill="#1b6bce" fill-rule="evenodd" stroke-width=".42308"/>
+</svg>
diff --git a/pentobi/android_icons_svg/icon72.svg b/pentobi/android_icons_svg/icon72.svg
new file mode 100644 (file)
index 0000000..bca86c9
--- /dev/null
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="72" height="72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m19 36h34v-17h-17v-17h-28.769c-2.6154 0-5.2308 2.6154-5.2308 5.2307v11.769h17z" fill="#e01b24" fill-rule="evenodd" stroke-width=".65384"/>
+ <path id="h" d="m2 19 2.6154-2.6154h11.769l2.6154 2.6154z" fill="#c6161f" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(17)" width="100%" height="100%" xlink:href="#h"/>
+ <use transform="translate(17 17)" width="100%" height="100%" xlink:href="#h"/>
+ <use transform="translate(34 17)" width="100%" height="100%" xlink:href="#h"/>
+ <path id="n" d="m19 2.0002 2.6154 2.6154h11.769l2.6154-2.6154z" fill="#e73b43" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(4.4329e-8 17)" width="100%" height="100%" xlink:href="#n"/>
+ <use transform="translate(17 17)" width="100%" height="100%" xlink:href="#n"/>
+ <path id="m" d="m19 2.0002 2.6154 2.6154v11.769l-2.6154 2.6154z" fill="#e6323a" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(4.4329e-8 17)" width="100%" height="100%" xlink:href="#m"/>
+ <use transform="translate(17 17)" width="100%" height="100%" xlink:href="#m"/>
+ <path id="c" d="m19 19-2.6154-2.6154v-11.769l2.6154-2.6154z" fill="#cd1922" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(17 17)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="translate(17)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="translate(34 17)" width="100%" height="100%" xlink:href="#c"/>
+ <g fill-rule="evenodd" stroke-width=".65384">
+  <path d="m3.6366 3.6623 0.97887 0.9531v11.769l-2.6154 2.6154v-11.769c0.013587-2.1191 1.6365-3.5685 1.6365-3.5685z" fill="#e6323a"/>
+  <path d="m3.6562 3.6525 0.95919 0.96291h11.769l2.6154-2.6154h-11.769c-2.1881 0.029547-3.5746 1.6525-3.5746 1.6525z" fill="#e73b42"/>
+  <path d="m36 2.0002h28.769c2.591 0.02786 5.2029 2.6676 5.2308 5.2307v45.769h-17v-34h-17z" fill="#2ec27e"/>
+  <path id="g" d="m36 19 2.6154-2.6154h11.769l2.6154 2.6154z" fill="#2ab073" fill-rule="evenodd" stroke-width=".65384"/>
+ </g>
+ <use transform="translate(17)" width="100%" height="100%" xlink:href="#g"/>
+ <use transform="translate(17 17)" width="100%" height="100%" xlink:href="#g"/>
+ <use transform="translate(17 34)" width="100%" height="100%" xlink:href="#g"/>
+ <path id="f" d="m36 2.0002 2.6154 2.6154v11.769l-2.6154 2.6154z" fill="#30ca85" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(17)" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(17 34)" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(17 17)" width="100%" height="100%" xlink:href="#f"/>
+ <path id="p" d="m36 2.0002 2.6154 2.6154h11.769l2.6154-2.6154z" fill="#4fd398" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(17 17)" width="100%" height="100%" xlink:href="#p"/>
+ <use transform="translate(17 34)" width="100%" height="100%" xlink:href="#p"/>
+ <path id="o" d="m53 2.0002-2.6154 2.6154v11.769l2.6154 2.6154z" fill="#2fbb7c" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(17 17)" width="100%" height="100%" xlink:href="#o"/>
+ <use transform="translate(17 34)" width="100%" height="100%" xlink:href="#o"/>
+ <g transform="matrix(.65385 0 0 .65384 -5.8462 -5.8459)" fill-rule="evenodd">
+  <path d="m90 12 4 4h18l1.4274-1.4594s-2.4274-2.5406-5.4274-2.5406z" fill="#4fd398"/>
+  <path d="m116 20c-0.0151-2.9828-2.5466-5.4534-2.5466-5.4534l-1.4534 1.4534v18l4 4z" fill="#2fbb7c"/>
+  <path d="m12 38v70c0.0213 4.0053 4 7.9997 7.984 7.9997l44.015-2e-3 9.99e-4 -25.998h-26v-52z" fill="#f6d32d"/>
+  <path id="j" d="m38 64h-26l4-4h18z" fill="#e1bf09" fill-rule="evenodd"/>
+ </g>
+ <use transform="matrix(.65385 0 0 .65384 -5.8462 11.154)" width="100%" height="100%" xlink:href="#j"/>
+ <use transform="matrix(.65385 0 0 .65384 11.154 28.154)" width="100%" height="100%" xlink:href="#j"/>
+ <path id="a" d="m19 19h-17l2.6154 2.6154h11.769z" fill="#f9e263" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(4.4329e-8 17)" width="100%" height="100%" xlink:href="#a"/>
+ <use transform="translate(17 34)" width="100%" height="100%" xlink:href="#a"/>
+ <path id="i" d="m2 36v-17l2.6154 2.6154v11.769z" fill="#f7da51" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(4.4329e-8 17)" width="100%" height="100%" xlink:href="#i"/>
+ <use transform="translate(17 34)" width="100%" height="100%" xlink:href="#i"/>
+ <path id="b" d="m19 19v17l-2.6154-2.6154v-11.769z" fill="#efc70b" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(4.4329e-8 17)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(17 34)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(4.4329e-8 34)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(4.4329e-8 34)" width="100%" height="100%" xlink:href="#a"/>
+ <g transform="matrix(.65385 0 0 .65384 -5.8462 -5.8459)" fill-rule="evenodd">
+  <path d="m12 108v-18l4 4v18l-1.4487 1.4487s-2.5087-1.9547-2.5513-5.4487z" fill="#f7da51"/>
+  <path d="m38 116h-18c-3.1531 0.0426-5.4487-2.5513-5.4487-2.5513l1.4487-1.4487h18z" fill="#e0bb0a"/>
+  <path d="m38 64h52v26h26v17.931c0 4.1332-3.9521 8.1118-8 8.0692h-44v-26h-26z" fill="#1c71d8"/>
+  <path id="l" d="m38 90 4-4h18l4 4z" fill="#1a68c7" fill-rule="evenodd"/>
+ </g>
+ <use transform="matrix(.65385 0 0 .65384 11.154 -5.8459)" width="100%" height="100%" xlink:href="#l"/>
+ <use transform="matrix(.65385 0 0 .65384 11.153 11.153)" width="100%" height="100%" xlink:href="#l"/>
+ <path d="m52.999 70h11.769c2.0616 0.02785 3.5626-1.6681 3.5626-1.6681l-0.94723-0.94722h-11.769z" fill="#1a68c7" fill-rule="evenodd" stroke-width=".65384"/>
+ <path id="e" d="m19 36 2.6154 2.6154h11.769l2.6154-2.6154z" fill="#3585e5" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(17)" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(17 17)" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(34 17)" width="100%" height="100%" xlink:href="#e"/>
+ <path id="d" d="m19 53 2.6154-2.6154v-11.769l-2.6154-2.6154z" fill="#2078e2" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(17)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(17 17)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(33.999 16.999)" width="100%" height="100%" xlink:href="#d"/>
+ <path id="k" d="m36 53-2.6154-2.6154v-11.769l2.6154-2.6154z" fill="#1b6bce" fill-rule="evenodd" stroke-width=".65384"/>
+ <use transform="translate(17)" width="100%" height="100%" xlink:href="#k"/>
+ <use transform="translate(17 17)" width="100%" height="100%" xlink:href="#k"/>
+ <path d="m70 64.769v-11.769l-2.6154 2.6154v11.769l0.94723 0.94722s1.6403-1.2781 1.6682-3.5626z" fill="#1b6bce" fill-rule="evenodd" stroke-width=".65384"/>
+</svg>
diff --git a/pentobi/docbook/CMakeLists.txt b/pentobi/docbook/CMakeLists.txt
new file mode 100644 (file)
index 0000000..a3f736d
--- /dev/null
@@ -0,0 +1,81 @@
+find_package(Gettext 0.18 REQUIRED)
+find_package(DocBookXSL REQUIRED)
+find_program(ITSTOOL itstool)
+if(NOT ITSTOOL)
+    message(FATAL_ERROR "itstool not found")
+endif()
+find_program(XSLTPROC xsltproc)
+if(NOT XSLTPROC)
+    message(FATAL_ERROR "xsltproc not found")
+endif()
+
+file(READ "${CMAKE_CURRENT_SOURCE_DIR}/po/LINGUAS" linguas)
+string(REGEX REPLACE "\n" ";" linguas "${linguas}")
+
+set(DOCBOOK_SRC
+    custom.xsl
+    index.docbook
+    figures/analysis.jpg
+    figures/board_callisto.png
+    figures/board_classic.png
+    figures/board_duo.png
+    figures/board_gembloq.png
+    figures/board_nexos.png
+    figures/board_trigon.jpg
+    figures/pieces_callisto.png
+    figures/pieces_gembloq.jpg
+    figures/pieces_junior.png
+    figures/pieces_nexos.png
+    figures/pieces.png
+    figures/pieces_trigon.jpg
+    figures/position_callisto.png
+    figures/position_classic.png
+    figures/position_duo.png
+    figures/position_gembloq.png
+    figures/position_nexos.png
+    figures/position_trigon.jpg
+    figures/rating.jpg
+    figures/stylesheet.css
+    )
+
+foreach(lang ${linguas})
+    add_custom_command(OUTPUT ${lang}.mo
+        COMMAND "${GETTEXT_MSGFMT_EXECUTABLE}" -o ${lang}.mo
+        "${CMAKE_CURRENT_SOURCE_DIR}/po/${lang}.po"
+        DEPENDS po/${lang}.po
+        )
+    list(APPEND index_files "help/${lang}/pentobi/index.html")
+    add_custom_command(OUTPUT "index_${lang}.docbook"
+        COMMAND "${ITSTOOL}" -m ${lang}.mo -o "index_${lang}.docbook"
+        "${CMAKE_CURRENT_SOURCE_DIR}/index.docbook"
+        DEPENDS index.docbook ${lang}.mo
+        )
+    add_custom_command(OUTPUT "help/${lang}/pentobi/index.html"
+        COMMAND "${XSLTPROC}" --nonet --novalid --path "${DOCBOOKXSL_DIR}/html"
+        --stringparam l10n.gentext.language ${lang}
+        -o "help/${lang}/pentobi/" "${CMAKE_CURRENT_SOURCE_DIR}/custom.xsl"
+        index_${lang}.docbook
+        DEPENDS "index_${lang}.docbook" ${DOCBOOK_SRC}
+        )
+endforeach()
+
+add_custom_command(OUTPUT "help/C/pentobi/index.html"
+    COMMAND "${XSLTPROC}" --nonet --novalid --path "${DOCBOOKXSL_DIR}/html"
+    -o "help/C/pentobi/" "${CMAKE_CURRENT_SOURCE_DIR}/custom.xsl"
+    "${CMAKE_CURRENT_SOURCE_DIR}/index.docbook"
+    COMMAND ${CMAKE_COMMAND} -E copy
+    "${CMAKE_CURRENT_SOURCE_DIR}/figures/*.png" "help/C/pentobi/"
+    COMMAND ${CMAKE_COMMAND} -E copy
+    "${CMAKE_CURRENT_SOURCE_DIR}/figures/*.jpg" "help/C/pentobi/"
+    COMMAND ${CMAKE_COMMAND} -E copy
+    "${CMAKE_CURRENT_SOURCE_DIR}/figures/stylesheet.css" "help/C/pentobi/"
+    DEPENDS ${DOCBOOK_SRC}
+    )
+
+add_custom_target(pentobi-help ALL DEPENDS
+    "help/C/pentobi/index.html" ${index_files}
+    )
+
+install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/help"
+    DESTINATION ${CMAKE_INSTALL_DATAROOTDIR} FILES_MATCHING PATTERN "*.css"
+    PATTERN "*.html" PATTERN "*.png" PATTERN "*.jpg")
diff --git a/pentobi/docbook/create-html b/pentobi/docbook/create-html
new file mode 100755 (executable)
index 0000000..b91e638
--- /dev/null
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+# Invoked by the Android qmake project. This duplicates the code in the cmake
+# file and using a bash script is not very portable. But once QtCreator has
+# better support for using cmake for Android projects, the qmake project and
+# this script will no longer be needed anyway.
+
+if [ $# -ne 1 ]; then
+    echo "Need output directory as argument"
+    exit 1
+fi
+OUTDIR=$1
+
+cd `dirname "$0"`
+for lang in C de es; do
+  case "$lang" in
+  C)
+    xsltproc --nonet \
+      --path "/usr/share/xml/docbook/stylesheet/docbook-xsl/html" \
+      -o "$OUTDIR/help/C/pentobi/" custom.xsl index.docbook
+    cp figures/*.{png,jpg,css} "$OUTDIR/help/C/pentobi/"
+    ;;
+  *)
+    msgfmt -o "$OUTDIR/$lang.mo" po/$lang.po
+    itstool -m "$OUTDIR/$lang.mo" -o "$OUTDIR/index_$lang.docbook" index.docbook
+    xsltproc --nonet \
+      --path "/usr/share/xml/docbook/stylesheet/docbook-xsl/html" \
+      --stringparam l10n.gentext.language $lang \
+      -o "$OUTDIR/help/$lang/pentobi/" custom.xsl "$OUTDIR/index_$lang.docbook"
+    ;;
+  esac
+done
diff --git a/pentobi/docbook/create-pot b/pentobi/docbook/create-pot
new file mode 100755 (executable)
index 0000000..3035f70
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+cd $(dirname "$0")
+itstool -i docbook.its index.docbook > pentobi-manual.pot
diff --git a/pentobi/docbook/custom.xsl b/pentobi/docbook/custom.xsl
new file mode 100644 (file)
index 0000000..aba10c2
--- /dev/null
@@ -0,0 +1,154 @@
+<?xml version='1.0'?>
+<xsl:stylesheet
+xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  version="1.0">
+<xsl:import href="chunk.xsl"/>
+<xsl:param name="admon.graphics" select="0"/>
+<xsl:param name="chapter.autolabel" select="0"/>
+<xsl:param name="chunk.quietly" select="1"/>
+<xsl:param name="chunk.section.depth" select="0"/>
+<xsl:param name="chunker.output.encoding" select="'UTF-8'"/>
+<xsl:param name="chunker.output.indent" select="'yes'"/>
+<xsl:param name="footer.rule" select="1"/>
+<xsl:param name="generate.index" select="1"/>
+<xsl:param name="generate.toc" select="'book toc'"/>
+<xsl:param name="header.rule" select="1"/>
+<xsl:param name="html.cleanup" select="1"/>
+<xsl:param name="html.stylesheet" select="'../../C/pentobi/stylesheet.css'"/>
+<xsl:param name="make.valid.html" select="1"/>
+<xsl:param name="refentry.generate.name" select="0"/>
+<xsl:param name="refentry.generate.title" select="1"/>
+<xsl:param name="spacing.paras" select="0"/>
+<xsl:param name="toc.section.depth" select="0"/>
+<xsl:param name="use.id.as.filename" select="1"/>
+
+<xsl:template name="user.head.content">
+<meta name="viewport">
+<xsl:attribute name="content">width=device-width,initial-scale=1</xsl:attribute>
+</meta>
+</xsl:template>
+
+<xsl:template name="body.attributes"/>
+
+<xsl:template name="header.navigation">
+<xsl:param name="prev" select="/foo"/>
+<xsl:param name="next" select="/foo"/>
+<xsl:param name="nav.context"/>
+<xsl:variable name="home" select="/*[1]"/>
+<div class="navheader">
+<table width="100%">
+<tr>
+<td width="34%" align="left">
+<xsl:if test="count($prev)>0">
+<a>
+<xsl:attribute name="href">
+<xsl:call-template name="href.target">
+<xsl:with-param name="object" select="$prev"/>
+</xsl:call-template>
+</xsl:attribute>
+<xsl:call-template name="navig.content">
+<xsl:with-param name="direction" select="'prev'"/>
+</xsl:call-template>
+</a>
+</xsl:if>
+<xsl:text>&#160;</xsl:text>
+</td>
+<td width="32%" align="center">
+<xsl:choose>
+<xsl:when test="$home != . or $nav.context = 'toc'">
+<a>
+<xsl:attribute name="href">
+<xsl:call-template name="href.target">
+<xsl:with-param name="object" select="$home"/>
+</xsl:call-template>
+</xsl:attribute>
+<xsl:call-template name="navig.content">
+<xsl:with-param name="direction" select="'home'"/>
+</xsl:call-template>
+</a>
+</xsl:when>
+<xsl:otherwise>&#160;</xsl:otherwise>
+</xsl:choose>
+</td>
+<td width="34%" align="right">
+<xsl:text>&#160;</xsl:text>
+<xsl:if test="count($next)>0">
+<a>
+<xsl:attribute name="href">
+<xsl:call-template name="href.target">
+<xsl:with-param name="object" select="$next"/>
+</xsl:call-template>
+</xsl:attribute>
+<xsl:call-template name="navig.content">
+<xsl:with-param name="direction" select="'next'"/>
+</xsl:call-template>
+</a>
+</xsl:if>
+</td>
+</tr>
+</table>
+<hr/>
+</div>
+</xsl:template>
+
+<xsl:template name="footer.navigation">
+<xsl:param name="prev" select="/foo"/>
+<xsl:param name="next" select="/foo"/>
+<xsl:param name="nav.context"/>
+<xsl:variable name="home" select="/*[1]"/>
+<div class="navheader">
+<hr/>
+<table width="100%">
+<tr>
+<td width="34%" align="left">
+<xsl:if test="count($prev)>0">
+<a>
+<xsl:attribute name="href">
+<xsl:call-template name="href.target">
+<xsl:with-param name="object" select="$prev"/>
+</xsl:call-template>
+</xsl:attribute>
+<xsl:call-template name="navig.content">
+<xsl:with-param name="direction" select="'prev'"/>
+</xsl:call-template>
+</a>
+</xsl:if>
+<xsl:text>&#160;</xsl:text>
+</td>
+<td width="32%" align="center">
+<xsl:choose>
+<xsl:when test="$home != . or $nav.context = 'toc'">
+<a>
+<xsl:attribute name="href">
+<xsl:call-template name="href.target">
+<xsl:with-param name="object" select="$home"/>
+</xsl:call-template>
+</xsl:attribute>
+<xsl:call-template name="navig.content">
+<xsl:with-param name="direction" select="'home'"/>
+</xsl:call-template>
+</a>
+</xsl:when>
+<xsl:otherwise>&#160;</xsl:otherwise>
+</xsl:choose>
+</td>
+<td width="34%" align="right">
+<xsl:text>&#160;</xsl:text>
+<xsl:if test="count($next)>0">
+<a>
+<xsl:attribute name="href">
+<xsl:call-template name="href.target">
+<xsl:with-param name="object" select="$next"/>
+</xsl:call-template>
+</xsl:attribute>
+<xsl:call-template name="navig.content">
+<xsl:with-param name="direction" select="'next'"/>
+</xsl:call-template>
+</a>
+</xsl:if>
+</td>
+</tr>
+</table>
+</div>
+</xsl:template>
+
+</xsl:stylesheet>
diff --git a/pentobi/docbook/docbook.its b/pentobi/docbook/docbook.its
new file mode 100644 (file)
index 0000000..9345300
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<its:rules xmlns:its="http://www.w3.org/2005/11/its" version="2.0">
+<its:translateRule selector="//imageobject/imagedata" translate="no"/>
+</its:rules>
diff --git a/pentobi/docbook/figures/analysis.jpg b/pentobi/docbook/figures/analysis.jpg
new file mode 100644 (file)
index 0000000..3abdcd9
Binary files /dev/null and b/pentobi/docbook/figures/analysis.jpg differ
diff --git a/pentobi/docbook/figures/board_callisto.png b/pentobi/docbook/figures/board_callisto.png
new file mode 100644 (file)
index 0000000..89a2fc7
Binary files /dev/null and b/pentobi/docbook/figures/board_callisto.png differ
diff --git a/pentobi/docbook/figures/board_classic.png b/pentobi/docbook/figures/board_classic.png
new file mode 100644 (file)
index 0000000..9d9a543
Binary files /dev/null and b/pentobi/docbook/figures/board_classic.png differ
diff --git a/pentobi/docbook/figures/board_duo.png b/pentobi/docbook/figures/board_duo.png
new file mode 100644 (file)
index 0000000..b1f5259
Binary files /dev/null and b/pentobi/docbook/figures/board_duo.png differ
diff --git a/pentobi/docbook/figures/board_gembloq.png b/pentobi/docbook/figures/board_gembloq.png
new file mode 100644 (file)
index 0000000..a79ea02
Binary files /dev/null and b/pentobi/docbook/figures/board_gembloq.png differ
diff --git a/pentobi/docbook/figures/board_nexos.png b/pentobi/docbook/figures/board_nexos.png
new file mode 100644 (file)
index 0000000..4fd32c8
Binary files /dev/null and b/pentobi/docbook/figures/board_nexos.png differ
diff --git a/pentobi/docbook/figures/board_trigon.jpg b/pentobi/docbook/figures/board_trigon.jpg
new file mode 100644 (file)
index 0000000..886514d
Binary files /dev/null and b/pentobi/docbook/figures/board_trigon.jpg differ
diff --git a/pentobi/docbook/figures/pieces.png b/pentobi/docbook/figures/pieces.png
new file mode 100644 (file)
index 0000000..0bce3bf
Binary files /dev/null and b/pentobi/docbook/figures/pieces.png differ
diff --git a/pentobi/docbook/figures/pieces_callisto.png b/pentobi/docbook/figures/pieces_callisto.png
new file mode 100644 (file)
index 0000000..2d5d01b
Binary files /dev/null and b/pentobi/docbook/figures/pieces_callisto.png differ
diff --git a/pentobi/docbook/figures/pieces_gembloq.jpg b/pentobi/docbook/figures/pieces_gembloq.jpg
new file mode 100644 (file)
index 0000000..522d440
Binary files /dev/null and b/pentobi/docbook/figures/pieces_gembloq.jpg differ
diff --git a/pentobi/docbook/figures/pieces_junior.png b/pentobi/docbook/figures/pieces_junior.png
new file mode 100644 (file)
index 0000000..724bd0e
Binary files /dev/null and b/pentobi/docbook/figures/pieces_junior.png differ
diff --git a/pentobi/docbook/figures/pieces_nexos.png b/pentobi/docbook/figures/pieces_nexos.png
new file mode 100644 (file)
index 0000000..228495f
Binary files /dev/null and b/pentobi/docbook/figures/pieces_nexos.png differ
diff --git a/pentobi/docbook/figures/pieces_trigon.jpg b/pentobi/docbook/figures/pieces_trigon.jpg
new file mode 100644 (file)
index 0000000..a22a5ef
Binary files /dev/null and b/pentobi/docbook/figures/pieces_trigon.jpg differ
diff --git a/pentobi/docbook/figures/position_callisto.png b/pentobi/docbook/figures/position_callisto.png
new file mode 100644 (file)
index 0000000..c4097ce
Binary files /dev/null and b/pentobi/docbook/figures/position_callisto.png differ
diff --git a/pentobi/docbook/figures/position_classic.png b/pentobi/docbook/figures/position_classic.png
new file mode 100644 (file)
index 0000000..4bdf32b
Binary files /dev/null and b/pentobi/docbook/figures/position_classic.png differ
diff --git a/pentobi/docbook/figures/position_duo.png b/pentobi/docbook/figures/position_duo.png
new file mode 100644 (file)
index 0000000..96e021f
Binary files /dev/null and b/pentobi/docbook/figures/position_duo.png differ
diff --git a/pentobi/docbook/figures/position_gembloq.png b/pentobi/docbook/figures/position_gembloq.png
new file mode 100644 (file)
index 0000000..768ed05
Binary files /dev/null and b/pentobi/docbook/figures/position_gembloq.png differ
diff --git a/pentobi/docbook/figures/position_nexos.png b/pentobi/docbook/figures/position_nexos.png
new file mode 100644 (file)
index 0000000..be1c5bc
Binary files /dev/null and b/pentobi/docbook/figures/position_nexos.png differ
diff --git a/pentobi/docbook/figures/position_trigon.jpg b/pentobi/docbook/figures/position_trigon.jpg
new file mode 100644 (file)
index 0000000..fa1dd82
Binary files /dev/null and b/pentobi/docbook/figures/position_trigon.jpg differ
diff --git a/pentobi/docbook/figures/rating.jpg b/pentobi/docbook/figures/rating.jpg
new file mode 100644 (file)
index 0000000..2bb05d4
Binary files /dev/null and b/pentobi/docbook/figures/rating.jpg differ
diff --git a/pentobi/docbook/figures/stylesheet.css b/pentobi/docbook/figures/stylesheet.css
new file mode 100644 (file)
index 0000000..2a0e411
--- /dev/null
@@ -0,0 +1,65 @@
+html {
+background-color: #eee;
+}
+
+body {
+color: black;
+background-color: white;
+max-width: 60em;
+margin: auto;
+padding: 0.7em;
+min-height: 100vh;
+}
+
+hr {
+color: #eee;
+}
+
+:link {
+text-decoration: none;
+color: blue;
+}
+
+:visited {
+text-decoration: none;
+color: purple;
+}
+
+:focus {
+outline-color: darkorange;
+}
+
+.caption {
+font-size: 90%;
+text-align: center;
+margin-left: 15%;
+margin-right: 15%;
+}
+
+.guibutton {
+font-style: italic;
+}
+
+.guilabel {
+font-style: italic;
+}
+
+.guimenu {
+font-style: italic;
+}
+
+.guimenuitem {
+font-style: italic;
+}
+
+.informalfigure {
+text-align: center;
+}
+
+.informalfigure img {
+width: auto;
+height: auto;
+max-width: 90%;
+max-height: 90%;
+margin: 0.5em;
+}
diff --git a/pentobi/docbook/help.qrc b/pentobi/docbook/help.qrc
new file mode 100644 (file)
index 0000000..f22cc42
--- /dev/null
@@ -0,0 +1,71 @@
+<!-- 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/overview.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/overview.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>
+        <file>help/es/pentobi/become_stronger.html</file>
+        <file>help/es/pentobi/callisto_rules.html</file>
+        <file>help/es/pentobi/classic_rules.html</file>
+        <file>help/es/pentobi/duo_rules.html</file>
+        <file>help/es/pentobi/gembloq_rules.html</file>
+        <file>help/es/pentobi/index.html</file>
+        <file>help/es/pentobi/junior_rules.html</file>
+        <file>help/es/pentobi/license.html</file>
+        <file>help/es/pentobi/nexos_rules.html</file>
+        <file>help/es/pentobi/overview.html</file>
+        <file>help/es/pentobi/shortcuts.html</file>
+        <file>help/es/pentobi/system.html</file>
+        <file>help/es/pentobi/trigon_rules.html</file>
+        <file>help/es/pentobi/user_interface.html</file>
+        <file>help/es/pentobi/window_menu.html</file>
+    </qresource>
+</RCC>
diff --git a/pentobi/docbook/index.docbook b/pentobi/docbook/index.docbook
new file mode 100644 (file)
index 0000000..e2a0d21
--- /dev/null
@@ -0,0 +1,1330 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
+<book>
+<title>Pentobi</title>
+
+<chapter id="overview">
+<title>Overview</title>
+<para>
+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.
+</para>
+</chapter>
+
+<chapter id="classic_rules">
+<title>Classic Rules</title>
+<para>
+There are four players, Blue, Yellow, Red and Green, and a board consisting of
+20×20 squares. Each player has a set of 21 pieces of his color shaped like the
+polyominoes up to size five. A polyomino is a shape built from a number of
+squares connected along the edges.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/pieces.png"/>
+</imageobject>
+<caption><para>The 21 pieces</para></caption>
+</mediaobject>
+</informalfigure>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/board_classic.png"/>
+</imageobject>
+<caption>
+<para>
+The 20×20 board with the starting squares marked with colored dots
+</para>
+</caption>
+</mediaobject>
+</informalfigure>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/position_classic.png"/>
+</imageobject>
+<caption><para>An example position after a few moves</para></caption>
+</mediaobject>
+</informalfigure>
+<para>
+When the player of a color cannot place any more pieces, the player passes and
+the next color continues. When none of the players can place any more pieces,
+the player with the highest score wins. The score of a color is the number of
+squares on the board occupied by the color, plus a bonus of 15 points if the
+color could place all of its pieces, plus an additional bonus of 5 points if
+the color could place all pieces and the last piece played was the one-square
+piece.
+</para>
+<sect1>
+<title>Rules for Two Players</title>
+<para>
+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.
+</para>
+</sect1>
+<sect1>
+<title>Rules for Three Players</title>
+<para>
+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.
+</para>
+</sect1>
+<sect1>
+<title>Colorless Starting Points</title>
+<para>
+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 at
+the first Blokus online servers and Blokus tournaments.
+</para>
+</sect1>
+</chapter>
+
+<chapter id="duo_rules">
+<title>Duo Rules</title>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject><imagedata fileref="../../C/pentobi/board_duo.png"/></imageobject>
+<caption>
+<para>
+The 14×14 board used in game variant Duo with the starting squares marked with
+colored dots
+</para>
+</caption>
+</mediaobject>
+</informalfigure>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/position_duo.png"/>
+</imageobject>
+<caption><para>An example position in game variant Duo</para></caption>
+</mediaobject>
+</informalfigure>
+</chapter>
+
+<chapter id="trigon_rules">
+<title>Trigon Rules</title>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/pieces_trigon.jpg"/>
+</imageobject>
+<caption><para>The 22 Trigon pieces</para></caption>
+</mediaobject>
+</informalfigure>
+<para>
+The board also consists of triangles and is shaped like a hexagon with an edge
+size of nine triangles.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/board_trigon.jpg"/>
+</imageobject>
+<caption>
+<para>The board with the starting fields marked with gray dots</para>
+</caption>
+</mediaobject>
+</informalfigure>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/position_trigon.jpg"/>
+</imageobject>
+<caption><para>
+An example position in game variant Trigon
+</para></caption>
+</mediaobject>
+</informalfigure>
+<sect1>
+<title>Rules for Two Players</title>
+<para>
+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.
+</para>
+</sect1>
+<sect1>
+<title>Rules for Three Players</title>
+<para>
+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.
+</para>
+</sect1>
+</chapter>
+
+<chapter id="junior_rules">
+<title>Junior Rules</title>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/pieces_junior.png"/>
+</imageobject>
+<caption><para>The 24 pieces used in Junior</para></caption>
+</mediaobject>
+</informalfigure>
+<para>
+Bonus points are not used in Junior.
+</para>
+</chapter>
+
+<chapter id="nexos_rules">
+<title>Nexos Rules</title>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/pieces_nexos.png"/>
+</imageobject>
+<caption><para>The 24 pieces</para></caption>
+</mediaobject>
+</informalfigure>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/board_nexos.png"/>
+</imageobject>
+<caption>
+<para>
+The board for Nexos with the starting intersections marked with colored dots
+</para>
+</caption>
+</mediaobject>
+</informalfigure>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/position_nexos.png"/>
+</imageobject>
+<caption><para>An example position after a few moves</para></caption>
+</mediaobject>
+</informalfigure>
+<para>
+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.
+</para>
+<sect1>
+<title>Rules for Two Players</title>
+<para>
+Like Blokus, Nexos can be played with two players by having one player play
+Blue and Red and the other player Yellow and Green.
+</para>
+</sect1>
+</chapter>
+
+<chapter id="gembloq_rules">
+<title>GembloQ Rules</title>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/pieces_gembloq.jpg"/>
+</imageobject>
+<caption><para>The 21 pieces</para></caption>
+</mediaobject>
+</informalfigure>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/board_gembloq.png"/>
+</imageobject>
+<caption>
+<para>
+The board for GembloQ with the starting points marked with colored dots
+</para>
+</caption>
+</mediaobject>
+</informalfigure>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/position_gembloq.png"/>
+</imageobject>
+<caption><para>An example position after a few moves</para></caption>
+</mediaobject>
+</informalfigure>
+<para>
+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.
+</para>
+<sect1>
+<title>Rules for Two and Three Players</title>
+<para>
+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.
+</para>
+</sect1>
+</chapter>
+
+<chapter id="callisto_rules">
+<title>Callisto Rules</title>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/pieces_callisto.png"/>
+</imageobject>
+<caption><para>The 21 pieces</para></caption>
+</mediaobject>
+</informalfigure>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/board_callisto.png"/>
+</imageobject>
+<caption>
+<para>
+The board with the center having a darker color
+</para>
+</caption>
+</mediaobject>
+</informalfigure>
+<para>
+All larger pieces may be placed anywhere on the board but must touch an
+existing piece of the same color edge-to-edge.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/position_callisto.png"/>
+</imageobject>
+<caption><para>An example position after a few moves</para></caption>
+</mediaobject>
+</informalfigure>
+<para>
+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.
+</para>
+<sect1>
+<title>Rules for Two and Three Players</title>
+<para>
+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.
+</para>
+</sect1>
+</chapter>
+
+<chapter id="user_interface">
+<title>How to Use Pentobi</title>
+<sect1>
+<title>Board</title>
+<para>
+Pieces can be selected by clicking on one of the unplayed pieces or by using
+<link linkend="shortcuts">shortcut keys</link>. 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.
+</para>
+<para>
+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).
+</para>
+<para>
+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.
+</para>
+</sect1>
+<sect1>
+<title>Playing Against the Computer</title>
+<para>
+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.
+</para>
+<para>
+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
+<guimenuitem>Settings</guimenuitem> from the <guimenu>Computer</guimenu> menu
+or toolbar and select the colors the computer should play.
+</para>
+<para>
+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.
+</para>
+<para>
+Selecting <guimenuitem>Play</guimenuitem> from the
+<guimenu>Computer</guimenu> 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.
+</para>
+</sect1>
+<sect1>
+<title>Move Variations and the Game Tree</title>
+<para>
+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 <guimenu>Go</guimenu> menu and the navigation buttons.
+</para>
+<para>
+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
+forward button). The main variation is supposed to represent the real game
+played. If you want a side variation to become the main variation, select
+<guimenuitem>Make Main Variation</guimenuitem> from the <guimenu>Edit</guimenu>
+menu.
+</para>
+</sect1>
+</chapter>
+
+<chapter id="become_stronger">
+<title>Become a Stronger Player</title>
+<para>
+Pentobi has functionality that can help you to become a stronger Blokus player.
+</para>
+<sect1 id="analysis">
+<title>Game Analysis</title>
+<para>
+A game can be analyzed by selecting <guimenuitem>Analyze Game</guimenuitem>
+from the <guimenu>Tools</guimenu> 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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/analysis.jpg"/>
+</imageobject>
+<caption>
+<para>Analysis of a game of variant Classic (2 players)</para>
+</caption>
+</mediaobject>
+</informalfigure>
+<para>
+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.
+</para>
+<para>
+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 <guimenuitem>Play Single Move</guimenuitem> from the
+<guimenu>Computer</guimenu> menu.
+</para>
+</sect1>
+<sect1 id="rating">
+<title>Determine Your Rating</title>
+<para>
+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.
+</para>
+<para>
+A rated game is started with <guimenuitem>Rated Game</guimenuitem> from the
+<guimenu>Game</guimenu> 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.
+</para>
+<para>
+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.
+</para>
+<para>
+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.
+</para>
+<para>
+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.
+</para>
+<informalfigure>
+<mediaobject>
+<imageobject>
+<imagedata fileref="../../C/pentobi/rating.jpg"/>
+</imageobject>
+<caption>
+<para>Window with rating graph</para>
+</caption>
+</mediaobject>
+</informalfigure>
+<para>
+You can always see your current rating by selecting
+<guimenuitem>Rating</guimenuitem> from the <guimenu>Tools</guimenu> 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.
+</para>
+</sect1>
+</chapter>
+
+<chapter id="window_menu">
+<title>Window Menu and Toolbar</title>
+<sect1>
+<title>Navigation Buttons in Toolbar</title>
+<variablelist>
+<varlistentry>
+<term><guibutton>Beginning</guibutton></term>
+<listitem>
+<para>
+Go to the beginning of the game.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guibutton>Backward 10</guibutton></term>
+<listitem>
+<para>
+Go ten moves backward in the current variation. The button supports autorepeat
+if pressed and held.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guibutton>Backward</guibutton></term>
+<listitem>
+<para>
+Go one move backward in the current variation. The button supports autorepeat
+if pressed and held.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guibutton>Forward</guibutton></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guibutton>Forward 10</guibutton></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guibutton>End</guibutton></term>
+<listitem>
+<para>
+Go to the end of the current variation. Like <guibutton>Forward</guibutton>,
+this also uses the first variation in positions with several follow-up
+variations.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guibutton>Next Variation</guibutton></term>
+<listitem>
+<para>
+Go to the next variation to the last move played (i.e. the next sibling
+node of the current node in the game tree).
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guibutton>Previous Variation</guibutton></term>
+<listitem>
+<para>
+Go to the previous variation to the last move played (i.e. the previous
+sibling node of the current node in the game tree).
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</sect1>
+<sect1>
+<title><guimenu>Game</guimenu> Menu</title>
+<variablelist>
+<varlistentry>
+<term><guimenuitem>New</guimenuitem>
+</term>
+<listitem>
+<para>
+Start a new game.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Rated Game</guimenuitem></term>
+<listitem>
+<para>
+Start a new <link linkend="rating">rated game</link> against the computer.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Game Variant</guimenuitem></term>
+<listitem>
+<para>
+Select a game variant and start a new game of this game variant.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry><term><guimenuitem>Game Info</guimenuitem></term>
+<listitem>
+<para>
+Display or edit additional information about the game like the name of the
+players or the date when the game was played.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry><term><guimenuitem>Undo Move</guimenuitem></term>
+<listitem>
+<para>
+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
+<guimenu>Edit</guimenu>/<guimenuitem>Truncate</guimenuitem> to remove inner
+nodes of the game tree).
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Find Move</guimenuitem></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Open</guimenuitem></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenu>Open Recent</guimenu></term>
+<listitem>
+<para>
+Load a recently used game.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Open Clipboard</guimenuitem></term>
+<listitem>
+<para>
+Open a game from a text copied to the clipboard. The text must be a valid game
+in Pentobi SGF file format.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Save</guimenuitem></term>
+<listitem>
+<para>
+Save the current game.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Save As</guimenuitem></term>
+<listitem>
+<para>
+Save the current game under a new file name.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenu>Export</guimenu>/<guimenuitem>Image</guimenuitem></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenu>Export</guimenu>/<guimenuitem>ASCII Art</guimenuitem></term>
+<listitem>
+<para>
+Save the current position as a text diagram. The text diagram should be viewed
+using a monospace font.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Quit</guimenuitem></term>
+<listitem>
+<para>
+Quit Pentobi.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</sect1>
+<sect1>
+<title><guimenu>Go</guimenu> Menu</title>
+<variablelist>
+<varlistentry>
+<term><guimenuitem>Move Number</guimenuitem></term>
+<listitem>
+<para>
+Go to the move with a given move number in the current variation.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Main Variation</guimenuitem></term>
+<listitem>
+<para>
+Go back to the last position in the current variation that belonged to the main
+variation.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Beginning of Branch</guimenuitem></term>
+<listitem>
+<para>
+Go back to the last position in the current variation that had an alternative
+move.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Next Comment</guimenuitem></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</sect1>
+<sect1>
+<title><guimenu>Edit</guimenu> Menu</title>
+<variablelist>
+<varlistentry>
+<term><guimenuitem>Annotation</guimenuitem></term>
+<listitem>
+<para>
+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 <guilabel>Move Marking</guilabel>, on the board.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Make Main Variation</guimenuitem></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Variation Up</guimenuitem></term>
+<listitem>
+<para>
+Changes the order of variations such that the current position will appear
+earlier when iterating over the variations with
+<guibutton>Next Variation</guibutton>
+/<guibutton>Previous Variation</guibutton>.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Variation Down</guimenuitem></term>
+<listitem>
+<para>
+Changes the order of variations such that the current position will appear
+later when iterating over the variations with
+<guibutton>Next Variation</guibutton>
+/<guibutton>Previous Variation</guibutton>.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Delete Variations</guimenuitem></term>
+<listitem>
+<para>
+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
+<guimenuitem>Back to Main Variation</guimenuitem>.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Truncate</guimenuitem></term>
+<listitem>
+<para>
+Remove the node with the current position, including any subtree, from the game
+tree.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Truncate Children</guimenuitem></term>
+<listitem>
+<para>
+Remove all child nodes of the node with the current position from the game
+tree.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Keep Position</guimenuitem></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Keep Subtree</guimenuitem></term>
+<listitem>
+<para>
+Like <guimenuitem>Keep Position</guimenuitem> but does not delete the moves
+after the current position.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Setup Mode</guimenuitem></term>
+<listitem>
+<para>
+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
+<guimenuitem>Next Color</guimenuitem> 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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Next Color</guimenuitem></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</sect1>
+<sect1>
+<title><guimenu>View</guimenu> Menu</title>
+<variablelist>
+<varlistentry><term><guimenuitem>Appearance</guimenuitem></term>
+<listitem>
+<para>
+<variablelist>
+<varlistentry>
+<term><guilabel>Coordinates</guilabel></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guilabel>Show variations</guilabel></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guilabel>Move number</guilabel></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guilabel>Animations</guilabel></term>
+<listitem>
+<para>
+Enables or disables the piece rotation, flipping and placement animations.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guilabel>Color theme</guilabel></term>
+<listitem>
+<para>
+Changes the colors of the window, board and pieces.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guilabel>Move marking</guilabel></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guilabel>Show comment</guilabel></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Comment</guimenuitem></term>
+<listitem>
+<para>
+Toggle the visibility of the comment area in the current position.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Fullscreen</guimenuitem></term>
+<listitem>
+<para>
+Make the main window full screen or leave full screen mode. To leave fullscreen
+mode without using the window menu, press the F11 key.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</sect1>
+<sect1>
+<title><guimenu>Computer</guimenu> Menu</title>
+<variablelist>
+<varlistentry>
+<term><guimenuitem>Settings</guimenuitem></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Play</guimenuitem></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Play Move</guimenuitem></term>
+<listitem>
+<para>
+Make the computer play a single move for the current color without changing the
+colors played by the computer.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Stop</guimenuitem></term>
+<listitem>
+<para>
+Abort the current move generation. You can make the computer continue to play
+by selecting <guimenuitem>Play</guimenuitem>.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</sect1>
+<sect1>
+<title><guimenu>Tools</guimenu> Menu</title>
+<variablelist>
+<varlistentry>
+<term><guimenuitem>Rating</guimenuitem></term>
+<listitem>
+<para>
+Show a dialog window with the <link linkend="rating">rating</link> of the user
+in the current game variant.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Clear Rating</guimenuitem></term>
+<listitem>
+<para>
+Deletes the rating information and history for the current game variant.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Analyze Game</guimenuitem></term>
+<listitem>
+<para>
+Perform a <link linkend="analysis">game analysis</link>.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>Clear Analysis</guimenuitem></term>
+<listitem>
+<para>
+Deletes the analysis for the current game.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</sect1>
+<sect1>
+<title><guimenu>Help</guimenu> Menu</title>
+<variablelist>
+<varlistentry>
+<term><guimenuitem>Pentobi Help</guimenuitem></term>
+<listitem>
+<para>
+Show a window to browse the Pentobi user manual.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><guimenuitem>About Pentobi</guimenuitem></term>
+<listitem>
+<para>
+Show an info dialog with information about this version of Pentobi.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</sect1>
+</chapter>
+
+<chapter id="shortcuts">
+<title>Keyboard Shortcuts</title>
+<para>
+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.
+</para>
+<variablelist>
+<varlistentry>
+<term><keysym>Plus</keysym></term>
+<listitem>
+<para>
+Select next piece
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><keysym>Minus</keysym></term>
+<listitem>
+<para>
+Select previous piece
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><keysym>Escape</keysym></term>
+<listitem>
+<para>
+Clear selected piece
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><keysym>Left</keysym></term>
+<term><keysym>Right</keysym></term>
+<term><keysym>Up</keysym></term>
+<term><keysym>Down</keysym></term>
+<term><keysym>Shift+Left</keysym></term>
+<term><keysym>Shift+Right</keysym></term>
+<term><keysym>Shift+Up</keysym></term>
+<term><keysym>Shift+Down</keysym></term>
+<listitem>
+<para>
+Move the selected piece. The Shift key makes the piece move faster.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><keysym>Space</keysym></term>
+<listitem>
+<para>
+Next orientation of the selected piece
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><keysym>Shift+Space</keysym></term>
+<listitem>
+<para>
+Previous orientation of the selected piece
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><keysym>Enter</keysym></term>
+<listitem>
+<para>
+Play the selected piece
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><keysym>1</keysym></term>
+<term><keysym>2</keysym></term>
+<term><keysym>A</keysym></term>
+<term><keysym>C</keysym></term>
+<term><keysym>E</keysym></term>
+<term><keysym>F</keysym></term>
+<term><keysym>G</keysym></term>
+<term><keysym>H</keysym></term>
+<term><keysym>I</keysym></term>
+<term><keysym>J</keysym></term>
+<term><keysym>L</keysym></term>
+<term><keysym>N</keysym></term>
+<term><keysym>O</keysym></term>
+<term><keysym>P</keysym></term>
+<term><keysym>S</keysym></term>
+<term><keysym>T</keysym></term>
+<term><keysym>U</keysym></term>
+<term><keysym>V</keysym></term>
+<term><keysym>W</keysym></term>
+<term><keysym>X</keysym></term>
+<term><keysym>Y</keysym></term>
+<term><keysym>Z</keysym></term>
+<listitem>
+<para>
+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").
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><keysym>Ctrl+Home</keysym></term>
+<term><keysym>Ctrl+Shift+Left</keysym></term>
+<term><keysym>Ctrl+Left</keysym></term>
+<term><keysym>Ctrl+Right</keysym></term>
+<term><keysym>Ctrl+Shift+Right</keysym></term>
+<term><keysym>Ctrl+End</keysym></term>
+<term><keysym>Ctrl+Up</keysym></term>
+<term><keysym>Ctrl+Down</keysym></term>
+<listitem>
+<para>
+Navigate in the game: beginning, ten moves backward, backward, forward, ten
+moves forward, end, previous variation, next variation.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><keysym>Ctrl+Shift+H</keysym></term>
+<listitem>
+<para>
+Like <guimenuitem>Find Move</guimenuitem> (Ctrl+H) but iterates backwards
+through the list of legal moves.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><keysym>Ctrl+T</keysym></term>
+<listitem>
+<para>
+Switch view between comment and game analysis.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><keysym>Alt+M</keysym></term>
+<listitem>
+<para>
+Open menu.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</chapter>
+
+<chapter id="system">
+<title>System Requirements</title>
+<para>
+Minimum: 1&#160;GB RAM, 1&#160;GHz CPU
+</para>
+<para>
+Recommended for playing level 9: 4&#160;GB RAM, 2.5&#160;GHz dual-core or
+faster CPU
+</para>
+<para>
+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).
+</para>
+</chapter>
+
+<chapter id="license">
+<title>License</title>
+<para>
+Copyright © 2011–2020 Markus Enzenberger
+</para>
+<para>
+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.
+</para>
+<para>
+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.
+</para>
+<sect1>
+<title>Trademark Disclaimer</title>
+<para>
+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.
+</para>
+</sect1>
+</chapter>
+
+</book>
diff --git a/pentobi/docbook/pentobi-manual.pot b/pentobi/docbook/pentobi-manual.pot
new file mode 100644 (file)
index 0000000..49d1d7b
--- /dev/null
@@ -0,0 +1,1410 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2019-02-11 17:37+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. (itstool) path: book/title
+#: index.docbook:5
+msgid "Pentobi"
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:8
+msgid "Overview"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:9
+msgid "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."
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:18
+msgid "Classic Rules"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:19
+msgid "There are four players, Blue, Yellow, Red and Green, and a board consisting of 20×20 squares. Each player has a set of 21 pieces of his color shaped like the polyominoes up to size five. A polyomino is a shape built from a number of squares connected along the edges."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:30
+#: index.docbook:289
+#: index.docbook:353
+msgid "The 21 pieces"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:33
+msgid "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."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:44
+msgid "The 20×20 board with the starting squares marked with colored dots"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:50
+msgid "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."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:61
+#: index.docbook:260
+#: index.docbook:320
+#: index.docbook:383
+msgid "An example position after a few moves"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:64
+msgid "When the player of a color cannot place any more pieces, the player passes and the next color continues. When none of the players can place any more pieces, the player with the highest score wins. The score of a color is the number of squares on the board occupied by the color, plus a bonus of 15 points if the color could place all of its pieces, plus an additional bonus of 5 points if the color could place all pieces and the last piece played was the one-square piece."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:74
+#: index.docbook:178
+#: index.docbook:268
+msgid "Rules for Two Players"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:75
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:82
+#: index.docbook:185
+msgid "Rules for Three Players"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:83
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:90
+msgid "Colorless Starting Points"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:91
+msgid "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 at the first Blokus online servers and Blokus tournaments."
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:102
+msgid "Duo Rules"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:103
+msgid "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."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:114
+msgid "The 14×14 board used in game variant Duo with the starting squares marked with colored dots"
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:126
+msgid "An example position in game variant Duo"
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:132
+msgid "Trigon Rules"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:133
+msgid "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."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:145
+msgid "The 22 Trigon pieces"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:148
+msgid "The board also consists of triangles and is shaped like a hexagon with an edge size of nine triangles."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:158
+msgid "The board with the starting fields marked with gray dots"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:162
+msgid "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."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:172
+msgid "An example position in game variant Trigon"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:179
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:186
+msgid "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."
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:196
+msgid "Junior Rules"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:197
+msgid "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."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:207
+msgid "The 24 pieces used in Junior"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:210
+msgid "Bonus points are not used in Junior."
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:216
+msgid "Nexos Rules"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:217
+msgid "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."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:227
+msgid "The 24 pieces"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:230
+msgid "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."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:240
+msgid "The board for Nexos with the starting intersections marked with colored dots"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:246
+msgid "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."
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:263
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:269
+msgid "Like Blokus, Nexos can be played with two players by having one player play Blue and Red and the other player Yellow and Green."
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:277
+msgid "GembloQ Rules"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:278
+msgid "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."
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:292
+msgid "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."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:302
+msgid "The board for GembloQ with the starting points marked with colored dots"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:308
+msgid "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."
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:323
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:329
+#: index.docbook:392
+msgid "Rules for Two and Three Players"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:330
+msgid "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."
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:340
+msgid "Callisto Rules"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:341
+msgid "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."
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:356
+msgid "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."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:368
+msgid "The board with the center having a darker color"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:374
+msgid "All larger pieces may be placed anywhere on the board but must touch an existing piece of the same color edge-to-edge."
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:386
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:393
+msgid "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."
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:405
+msgid "How to Use Pentobi"
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:407
+msgid "Board"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:408
+msgid "Pieces can be selected by clicking on one of the unplayed pieces or by using <link linkend=\"shortcuts\">shortcut keys</link>. 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."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:414
+msgid "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)."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:419
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:427
+msgid "Playing Against the Computer"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:428
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:433
+msgid "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 <guimenuitem>Settings</guimenuitem> from the <guimenu>Computer</guimenu> menu or toolbar and select the colors the computer should play."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:439
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:446
+msgid "Selecting <guimenuitem>Play</guimenuitem> from the <guimenu>Computer</guimenu> 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."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:455
+msgid "Move Variations and the Game Tree"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:456
+msgid "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 <guimenu>Go</guimenu> menu and the navigation buttons."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:466
+msgid "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 forward button). The main variation is supposed to represent the real game played. If you want a side variation to become the main variation, select <guimenuitem>Make Main Variation</guimenuitem> from the <guimenu>Edit</guimenu> menu."
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:478
+msgid "Become a Stronger Player"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:479
+msgid "Pentobi has functionality that can help you to become a stronger Blokus player."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:483
+msgid "Game Analysis"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:484
+msgid "A game can be analyzed by selecting <guimenuitem>Analyze Game</guimenuitem> from the <guimenu>Tools</guimenu> 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."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:496
+msgid "Analysis of a game of variant Classic (2 players)"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:500
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:506
+msgid "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 <guimenuitem>Play Single Move</guimenuitem> from the <guimenu>Computer</guimenu> menu."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:516
+msgid "Determine Your Rating"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:517
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:522
+msgid "A rated game is started with <guimenuitem>Rated Game</guimenuitem> from the <guimenu>Game</guimenu> 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."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:529
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:534
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:540
+msgid "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."
+msgstr ""
+
+#. (itstool) path: caption/para
+#: index.docbook:552
+msgid "Window with rating graph"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:556
+msgid "You can always see your current rating by selecting <guimenuitem>Rating</guimenuitem> from the <guimenu>Tools</guimenu> 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."
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:567
+msgid "Window Menu and Toolbar"
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:569
+msgid "Navigation Buttons in Toolbar"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:572
+msgid "<guibutton>Beginning</guibutton>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:574
+msgid "Go to the beginning of the game."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:580
+msgid "<guibutton>Backward 10</guibutton>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:582
+msgid "Go ten moves backward in the current variation. The button supports autorepeat if pressed and held."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:589
+msgid "<guibutton>Backward</guibutton>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:591
+msgid "Go one move backward in the current variation. The button supports autorepeat if pressed and held."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:598
+msgid "<guibutton>Forward</guibutton>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:600
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:609
+msgid "<guibutton>Forward 10</guibutton>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:611
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:620
+msgid "<guibutton>End</guibutton>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:622
+msgid "Go to the end of the current variation. Like <guibutton>Forward</guibutton>, this also uses the first variation in positions with several follow-up variations."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:630
+msgid "<guibutton>Next Variation</guibutton>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:632
+msgid "Go to the next variation to the last move played (i.e. the next sibling node of the current node in the game tree)."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:639
+msgid "<guibutton>Previous Variation</guibutton>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:641
+msgid "Go to the previous variation to the last move played (i.e. the previous sibling node of the current node in the game tree)."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:650
+msgid "<guimenu>Game</guimenu> Menu"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:653
+msgid "<guimenuitem>New</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:656
+msgid "Start a new game."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:662
+msgid "<guimenuitem>Rated Game</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:664
+msgid "Start a new <link linkend=\"rating\">rated game</link> against the computer."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:670
+msgid "<guimenuitem>Game Variant</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:672
+msgid "Select a game variant and start a new game of this game variant."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:677
+msgid "<guimenuitem>Game Info</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:679
+msgid "Display or edit additional information about the game like the name of the players or the date when the game was played."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:685
+msgid "<guimenuitem>Undo Move</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:687
+msgid "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 <guimenu>Edit</guimenu>/<guimenuitem>Truncate</guimenuitem> to remove inner nodes of the game tree)."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:697
+msgid "<guimenuitem>Find Move</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:699
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:706
+msgid "<guimenuitem>Open</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:708
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:718
+msgid "<guimenu>Open Recent</guimenu>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:720
+msgid "Load a recently used game."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:726
+msgid "<guimenuitem>Open Clipboard</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:728
+msgid "Open a game from a text copied to the clipboard. The text must be a valid game in Pentobi SGF file format."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:735
+msgid "<guimenuitem>Save</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:737
+msgid "Save the current game."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:743
+msgid "<guimenuitem>Save As</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:745
+msgid "Save the current game under a new file name."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:751
+msgid "<guimenu>Export</guimenu>/<guimenuitem>Image</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:753
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:762
+msgid "<guimenu>Export</guimenu>/<guimenuitem>ASCII Art</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:764
+msgid "Save the current position as a text diagram. The text diagram should be viewed using a monospace font."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:771
+msgid "<guimenuitem>Quit</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:773
+msgid "Quit Pentobi."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:781
+msgid "<guimenu>Go</guimenu> Menu"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:784
+msgid "<guimenuitem>Move Number</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:786
+msgid "Go to the move with a given move number in the current variation."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:792
+msgid "<guimenuitem>Main Variation</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:794
+msgid "Go back to the last position in the current variation that belonged to the main variation."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:801
+msgid "<guimenuitem>Beginning of Branch</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:803
+msgid "Go back to the last position in the current variation that had an alternative move."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:810
+msgid "<guimenuitem>Next Comment</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:812
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:822
+msgid "<guimenu>Edit</guimenu> Menu"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:825
+msgid "<guimenuitem>Annotation</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:827
+msgid "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 <guilabel>Move Marking</guilabel>, on the board."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:835
+msgid "<guimenuitem>Make Main Variation</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:837
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:845
+msgid "<guimenuitem>Variation Up</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:847
+msgid "Changes the order of variations such that the current position will appear earlier when iterating over the variations with <guibutton>Next Variation</guibutton> /<guibutton>Previous Variation</guibutton>."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:856
+msgid "<guimenuitem>Variation Down</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:858
+msgid "Changes the order of variations such that the current position will appear later when iterating over the variations with <guibutton>Next Variation</guibutton> /<guibutton>Previous Variation</guibutton>."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:867
+msgid "<guimenuitem>Delete Variations</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:869
+msgid "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 <guimenuitem>Back to Main Variation</guimenuitem>."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:877
+msgid "<guimenuitem>Truncate</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:879
+msgid "Remove the node with the current position, including any subtree, from the game tree."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:886
+msgid "<guimenuitem>Truncate Children</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:888
+msgid "Remove all child nodes of the node with the current position from the game tree."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:895
+msgid "<guimenuitem>Keep Position</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:897
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:904
+msgid "<guimenuitem>Keep Subtree</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:906
+msgid "Like <guimenuitem>Keep Position</guimenuitem> but does not delete the moves after the current position."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:913
+msgid "<guimenuitem>Setup Mode</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:915
+msgid "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 <guimenuitem>Next Color</guimenuitem> 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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:927
+msgid "<guimenuitem>Next Color</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:929
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:939
+msgid "<guimenu>View</guimenu> Menu"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:941
+msgid "<guimenuitem>Appearance</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:946
+msgid "<guilabel>Coordinates</guilabel>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:948
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:956
+msgid "<guilabel>Show variations</guilabel>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:958
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:966
+msgid "<guilabel>Move number</guilabel>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:968
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:975
+msgid "<guilabel>Animations</guilabel>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:977
+msgid "Enables or disables the piece rotation, flipping and placement animations."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:983
+msgid "<guilabel>Color theme</guilabel>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:985
+msgid "Changes the colors of the window, board and pieces."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:991
+msgid "<guilabel>Move marking</guilabel>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:993
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1001
+msgid "<guilabel>Show comment</guilabel>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1003
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1015
+msgid "<guimenuitem>Comment</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1017
+msgid "Toggle the visibility of the comment area in the current position."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1023
+msgid "<guimenuitem>Fullscreen</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1025
+msgid "Make the main window full screen or leave full screen mode. To leave fullscreen mode without using the window menu, press the F11 key."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:1034
+msgid "<guimenu>Computer</guimenu> Menu"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1037
+msgid "<guimenuitem>Settings</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1039
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1047
+msgid "<guimenuitem>Play</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1049
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1058
+msgid "<guimenuitem>Play Move</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1060
+msgid "Make the computer play a single move for the current color without changing the colors played by the computer."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1067
+msgid "<guimenuitem>Stop</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1069
+msgid "Abort the current move generation. You can make the computer continue to play by selecting <guimenuitem>Play</guimenuitem>."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:1078
+msgid "<guimenu>Tools</guimenu> Menu"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1081
+msgid "<guimenuitem>Rating</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1083
+msgid "Show a dialog window with the <link linkend=\"rating\">rating</link> of the user in the current game variant."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1090
+msgid "<guimenuitem>Clear Rating</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1092
+msgid "Deletes the rating information and history for the current game variant."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1098
+msgid "<guimenuitem>Analyze Game</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1100
+msgid "Perform a <link linkend=\"analysis\">game analysis</link>."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1106
+msgid "<guimenuitem>Clear Analysis</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1108
+msgid "Deletes the analysis for the current game."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:1116
+msgid "<guimenu>Help</guimenu> Menu"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1119
+msgid "<guimenuitem>Pentobi Help</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1121
+msgid "Show a window to browse the Pentobi user manual."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1127
+msgid "<guimenuitem>About Pentobi</guimenuitem>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1129
+msgid "Show an info dialog with information about this version of Pentobi."
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:1139
+msgid "Keyboard Shortcuts"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:1140
+msgid "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."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1148
+msgid "<keysym>Plus</keysym>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1150
+msgid "Select next piece"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1156
+msgid "<keysym>Minus</keysym>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1158
+msgid "Select previous piece"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1164
+msgid "<keysym>Escape</keysym>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1166
+msgid "Clear selected piece"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1172
+msgid "<keysym>Left</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1173
+msgid "<keysym>Right</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1174
+msgid "<keysym>Up</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1175
+msgid "<keysym>Down</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1176
+msgid "<keysym>Shift+Left</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1177
+msgid "<keysym>Shift+Right</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1178
+msgid "<keysym>Shift+Up</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1179
+msgid "<keysym>Shift+Down</keysym>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1181
+msgid "Move the selected piece. The Shift key makes the piece move faster."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1187
+msgid "<keysym>Space</keysym>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1189
+msgid "Next orientation of the selected piece"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1195
+msgid "<keysym>Shift+Space</keysym>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1197
+msgid "Previous orientation of the selected piece"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1203
+msgid "<keysym>Enter</keysym>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1205
+msgid "Play the selected piece"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1211
+msgid "<keysym>1</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1212
+msgid "<keysym>2</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1213
+msgid "<keysym>A</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1214
+msgid "<keysym>C</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1215
+msgid "<keysym>E</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1216
+msgid "<keysym>F</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1217
+msgid "<keysym>G</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1218
+msgid "<keysym>H</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1219
+msgid "<keysym>I</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1220
+msgid "<keysym>J</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1221
+msgid "<keysym>L</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1222
+msgid "<keysym>N</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1223
+msgid "<keysym>O</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1224
+msgid "<keysym>P</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1225
+msgid "<keysym>S</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1226
+msgid "<keysym>T</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1227
+msgid "<keysym>U</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1228
+msgid "<keysym>V</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1229
+msgid "<keysym>W</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1230
+msgid "<keysym>X</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1231
+msgid "<keysym>Y</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1232
+msgid "<keysym>Z</keysym>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1234
+msgid "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\")."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1244
+msgid "<keysym>Ctrl+Home</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1245
+msgid "<keysym>Ctrl+Shift+Left</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1246
+msgid "<keysym>Ctrl+Left</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1247
+msgid "<keysym>Ctrl+Right</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1248
+msgid "<keysym>Ctrl+Shift+Right</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1249
+msgid "<keysym>Ctrl+End</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1250
+msgid "<keysym>Ctrl+Up</keysym>"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1251
+msgid "<keysym>Ctrl+Down</keysym>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1253
+msgid "Navigate in the game: beginning, ten moves backward, backward, forward, ten moves forward, end, previous variation, next variation."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1260
+msgid "<keysym>Ctrl+Shift+H</keysym>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1262
+msgid "Like <guimenuitem>Find Move</guimenuitem> (Ctrl+H) but iterates backwards through the list of legal moves."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1269
+msgid "<keysym>Ctrl+T</keysym>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1271
+msgid "Switch view between comment and game analysis."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1277
+msgid "<keysym>Alt+M</keysym>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: index.docbook:1279
+msgid "Open menu."
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:1288
+msgid "System Requirements"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:1289
+msgid "Minimum: 1 GB RAM, 1 GHz CPU"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:1292
+msgid "Recommended for playing level 9: 4 GB RAM, 2.5 GHz dual-core or faster CPU"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:1296
+msgid "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)."
+msgstr ""
+
+#. (itstool) path: chapter/title
+#: index.docbook:1304
+msgid "License"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:1305
+msgid "Copyright © 2011–2020 Markus Enzenberger"
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:1308
+msgid "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."
+msgstr ""
+
+#. (itstool) path: chapter/para
+#: index.docbook:1314
+msgid "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."
+msgstr ""
+
+#. (itstool) path: sect1/title
+#: index.docbook:1321
+msgid "Trademark Disclaimer"
+msgstr ""
+
+#. (itstool) path: sect1/para
+#: index.docbook:1322
+msgid "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."
+msgstr ""
+
diff --git a/pentobi/docbook/po/LINGUAS b/pentobi/docbook/po/LINGUAS
new file mode 100644 (file)
index 0000000..173f978
--- /dev/null
@@ -0,0 +1,2 @@
+de
+es
diff --git a/pentobi/docbook/po/de.po b/pentobi/docbook/po/de.po
new file mode 100644 (file)
index 0000000..84124c8
--- /dev/null
@@ -0,0 +1,2162 @@
+# 
+# Translators:
+# Markus Enzenberger <markus.enzenberger@gmail.com>, 2020
+# 
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2019-02-11 17:37+0100\n"
+"PO-Revision-Date: 2019-02-25 15:30+0000\n"
+"Last-Translator: Markus Enzenberger <markus.enzenberger@gmail.com>, 2020\n"
+"Language-Team: German (https://www.transifex.com/markus-enzenberger/teams/89074/de/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. (itstool) path: book/title
+#: index.docbook:5
+msgid "Pentobi"
+msgstr "Pentobi"
+
+#. (itstool) path: chapter/title
+#: index.docbook:8
+msgid "Overview"
+msgstr "Übersicht"
+
+#. (itstool) path: chapter/para
+#: index.docbook:9
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: chapter/title
+#: index.docbook:18
+msgid "Classic Rules"
+msgstr "Klassische Regeln"
+
+#. (itstool) path: chapter/para
+#: index.docbook:19
+msgid ""
+"There are four players, Blue, Yellow, Red and Green, and a board consisting "
+"of 20×20 squares. Each player has a set of 21 pieces of his color shaped "
+"like the polyominoes up to size five. A polyomino is a shape built from a "
+"number of squares connected along the edges."
+msgstr ""
+"Es gibt vier Spieler, Blau, Gelb, Rot und Grün, und ein Brett, das aus 20×20"
+" Quadraten besteht. Jeder Spieler besitzt 21 Spielsteine seiner Farbe, die "
+"die Form von Polyominos bis zur Größe fünf haben. Ein Polyomino ist eine "
+"Figur, die aus einer Anzahl von Quadraten besteht, die entlang der Kanten "
+"verbunden sind."
+
+#. (itstool) path: caption/para
+#: index.docbook:30 index.docbook:289 index.docbook:353
+msgid "The 21 pieces"
+msgstr "Die 21 Spielsteine"
+
+#. (itstool) path: chapter/para
+#: index.docbook:33
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: caption/para
+#: index.docbook:44
+msgid "The 20×20 board with the starting squares marked with colored dots"
+msgstr "Das 20×20-Brett mit den durch farbige Punkte markierten Startfeldern"
+
+#. (itstool) path: chapter/para
+#: index.docbook:50
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: caption/para
+#: index.docbook:61 index.docbook:260 index.docbook:320 index.docbook:383
+msgid "An example position after a few moves"
+msgstr "Eine Beispielstellung nach ein paar Zügen"
+
+#. (itstool) path: chapter/para
+#: index.docbook:64
+msgid ""
+"When the player of a color cannot place any more pieces, the player passes "
+"and the next color continues. When none of the players can place any more "
+"pieces, the player with the highest score wins. The score of a color is the "
+"number of squares on the board occupied by the color, plus a bonus of 15 "
+"points if the color could place all of its pieces, plus an additional bonus "
+"of 5 points if the color could place all pieces and the last piece played "
+"was the one-square piece."
+msgstr ""
+"Wenn der Spieler einer Farbe keine Spielsteine mehr setzen kann, muss der "
+"Spieler aussetzen und die nächste Farbe ist am Zug. Wenn keiner der Spieler "
+"mehr einen Spielstein setzen kann, gewinnt der Spieler mit der höchsten "
+"Punktzahl. Die Punktzahl einer Farbe ist die Anzahl der Quadrate auf dem "
+"Brett, die von der Farbe besetzt sind, plus ein Bonus von 15 Punkten, wenn "
+"die Farbe alle ihre Spielsteine setzen konnte, plus ein zusätzlicher Bonus "
+"von 5 Punkten, wenn die Farbe alle Spielsteine setzen konnte und der zuletzt"
+" gespielte Spielstein der Spielstein war, der aus einem Quadrat besteht."
+
+#. (itstool) path: sect1/title
+#: index.docbook:74 index.docbook:178 index.docbook:268
+msgid "Rules for Two Players"
+msgstr "Regeln für zwei Spieler"
+
+#. (itstool) path: sect1/para
+#: index.docbook:75
+msgid ""
+"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."
+msgstr ""
+"Das Spiel kann mit zwei Spielern gespielt werden. Der erste Spieler spielt "
+"Blau und Rot, der zweite Spieler Gelb und Grün. Die Punkte beider Farben "
+"eines Spielers werden addiert."
+
+#. (itstool) path: sect1/title
+#: index.docbook:82 index.docbook:185
+msgid "Rules for Three Players"
+msgstr "Regeln für drei Spieler"
+
+#. (itstool) path: sect1/para
+#: index.docbook:83
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/title
+#: index.docbook:90
+msgid "Colorless Starting Points"
+msgstr "Farblose Startfelder"
+
+#. (itstool) path: sect1/para
+#: index.docbook:91
+msgid ""
+"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 at the first Blokus online servers and Blokus tournaments."
+msgstr ""
+"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 den ersten Blokus-Online-Servern und "
+"Blokus-Turnieren verwendet wurde."
+
+#. (itstool) path: chapter/title
+#: index.docbook:102
+msgid "Duo Rules"
+msgstr "Duo-Regeln"
+
+#. (itstool) path: chapter/para
+#: index.docbook:103
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: caption/para
+#: index.docbook:114
+msgid ""
+"The 14×14 board used in game variant Duo with the starting squares marked "
+"with colored dots"
+msgstr ""
+"Das 14×14-Brett, das in der Spielvariante Duo benutzt wird, mit den durch "
+"farbige Punkte markierten Startfeldern"
+
+#. (itstool) path: caption/para
+#: index.docbook:126
+msgid "An example position in game variant Duo"
+msgstr "Eine Beispielstellung in der Spielvariante Duo"
+
+#. (itstool) path: chapter/title
+#: index.docbook:132
+msgid "Trigon Rules"
+msgstr "Trigon-Regeln"
+
+#. (itstool) path: chapter/para
+#: index.docbook:133
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: caption/para
+#: index.docbook:145
+msgid "The 22 Trigon pieces"
+msgstr "Die 22 Trigon-Spielsteine"
+
+#. (itstool) path: chapter/para
+#: index.docbook:148
+msgid ""
+"The board also consists of triangles and is shaped like a hexagon with an "
+"edge size of nine triangles."
+msgstr ""
+"Das Spielbrett besteht ebenfalls aus Dreiecken und hat die Form eines "
+"Sechsecks mit jeweils neun Dreiecken pro Kante."
+
+#. (itstool) path: caption/para
+#: index.docbook:158
+msgid "The board with the starting fields marked with gray dots"
+msgstr "Das Brett mit den durch graue Punkte markierten Startfeldern"
+
+#. (itstool) path: chapter/para
+#: index.docbook:162
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: caption/para
+#: index.docbook:172
+msgid "An example position in game variant Trigon"
+msgstr "Eine Beispielstellung in der Spielvariante Trigon"
+
+#. (itstool) path: sect1/para
+#: index.docbook:179
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/para
+#: index.docbook:186
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: chapter/title
+#: index.docbook:196
+msgid "Junior Rules"
+msgstr "Junior-Regeln"
+
+#. (itstool) path: chapter/para
+#: index.docbook:197
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: caption/para
+#: index.docbook:207
+msgid "The 24 pieces used in Junior"
+msgstr "Die 24 Spielsteine, die in Junior benutzt werden"
+
+#. (itstool) path: chapter/para
+#: index.docbook:210
+msgid "Bonus points are not used in Junior."
+msgstr "Bonuspunkte werden in Junior nicht benutzt."
+
+#. (itstool) path: chapter/title
+#: index.docbook:216
+msgid "Nexos Rules"
+msgstr "Nexos-Regeln"
+
+#. (itstool) path: chapter/para
+#: index.docbook:217
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: caption/para
+#: index.docbook:227
+msgid "The 24 pieces"
+msgstr "Die 24 Spielsteine"
+
+#. (itstool) path: chapter/para
+#: index.docbook:230
+msgid ""
+"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."
+msgstr ""
+"Jede Farbe hat einen Startkreuzungspunkt auf der Kreuzung der dritten Linien"
+" nahe einer Ecke. Der erste Spielstein muss den Startkreuzungspunkt "
+"berühren."
+
+#. (itstool) path: caption/para
+#: index.docbook:240
+msgid ""
+"The board for Nexos with the starting intersections marked with colored dots"
+msgstr ""
+"Das Brett für Nexos mit den durch farbige Punkte markierten "
+"Startkreuzungspunkten"
+
+#. (itstool) path: chapter/para
+#: index.docbook:246
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: chapter/para
+#: index.docbook:263
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/para
+#: index.docbook:269
+msgid ""
+"Like Blokus, Nexos can be played with two players by having one player play "
+"Blue and Red and the other player Yellow and Green."
+msgstr ""
+"Wie Blokus kann Nexos von zwei Spielern gespielt werden, indem ein Spieler "
+"Rot und Blau, und der andere Spieler Gelb und Grün spielt."
+
+#. (itstool) path: chapter/title
+#: index.docbook:277
+msgid "GembloQ Rules"
+msgstr "GembloQ-Regeln"
+
+#. (itstool) path: chapter/para
+#: index.docbook:278
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: chapter/para
+#: index.docbook:292
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: caption/para
+#: index.docbook:302
+msgid ""
+"The board for GembloQ with the starting points marked with colored dots"
+msgstr ""
+"Das Brett für GembloQ mit den durch farbige Punkte markierten Startfeldern"
+
+#. (itstool) path: chapter/para
+#: index.docbook:308
+msgid ""
+"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."
+msgstr ""
+"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 nicht entlang der"
+" Kanten. Züge sind auch legal, wenn eine Spitze eines halben Quadrats die "
+"Kante eines Spielsteins der selben Farbe berührt."
+
+#. (itstool) path: chapter/para
+#: index.docbook:323
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/title
+#: index.docbook:329 index.docbook:392
+msgid "Rules for Two and Three Players"
+msgstr "Regeln für zwei und drei Spieler"
+
+#. (itstool) path: sect1/para
+#: index.docbook:330
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: chapter/title
+#: index.docbook:340
+msgid "Callisto Rules"
+msgstr "Callisto-Regeln"
+
+#. (itstool) path: chapter/para
+#: index.docbook:341
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: chapter/para
+#: index.docbook:356
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: caption/para
+#: index.docbook:368
+msgid "The board with the center having a darker color"
+msgstr "Das Brett mit einer dunkleren Farbe im Zentrum"
+
+#. (itstool) path: chapter/para
+#: index.docbook:374
+msgid ""
+"All larger pieces may be placed anywhere on the board but must touch an "
+"existing piece of the same color edge-to-edge."
+msgstr ""
+"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."
+
+#. (itstool) path: chapter/para
+#: index.docbook:386
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/para
+#: index.docbook:393
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: chapter/title
+#: index.docbook:405
+msgid "How to Use Pentobi"
+msgstr "Wie Sie Pentobi benutzen"
+
+#. (itstool) path: sect1/title
+#: index.docbook:407
+msgid "Board"
+msgstr "Spielbrett"
+
+#. (itstool) path: sect1/para
+#: index.docbook:408
+msgid ""
+"Pieces can be selected by clicking on one of the unplayed pieces or by using"
+" <link linkend=\"shortcuts\">shortcut keys</link>. 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."
+msgstr ""
+"Spielsteine können durch Klicken auf einen ungespielten Spielstein "
+"ausgewählt werden oder durch Benutzen von <link "
+"linkend=\"shortcuts\">Tastenkürzeln</link>. 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."
+
+#. (itstool) path: sect1/para
+#: index.docbook:414
+msgid ""
+"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)."
+msgstr ""
+"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)."
+
+#. (itstool) path: sect1/para
+#: index.docbook:419
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/title
+#: index.docbook:427
+msgid "Playing Against the Computer"
+msgstr "Gegen den Computer spielen"
+
+#. (itstool) path: sect1/para
+#: index.docbook:428
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/para
+#: index.docbook:433
+msgid ""
+"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 "
+"<guimenuitem>Settings</guimenuitem> from the <guimenu>Computer</guimenu> "
+"menu or toolbar and select the colors the computer should play."
+msgstr ""
+"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 <guimenuitem>Einstellungen</guimenuitem> aus "
+"dem Menü <guimenu>Computer</guimenu> oder der Werkzeugleiste und wählen Sie "
+"die Farben, die der Computer spielen soll."
+
+#. (itstool) path: sect1/para
+#: index.docbook:439
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/para
+#: index.docbook:446
+msgid ""
+"Selecting <guimenuitem>Play</guimenuitem> from the "
+"<guimenu>Computer</guimenu> 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."
+msgstr ""
+"Die Auswahl von <guimenuitem>Spielen</guimenuitem> aus dem Menü "
+"<guimenu>Computer</guimenu> 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."
+
+#. (itstool) path: sect1/title
+#: index.docbook:455
+msgid "Move Variations and the Game Tree"
+msgstr "Zugvarianten und der Spielbaum"
+
+#. (itstool) path: sect1/para
+#: index.docbook:456
+msgid ""
+"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 <guimenu>Go</guimenu> menu and the navigation "
+"buttons."
+msgstr ""
+"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 <guimenu>Gehe zu</guimenu> und den "
+"Navigations-Buttons navigieren."
+
+#. (itstool) path: sect1/para
+#: index.docbook:466
+msgid ""
+"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 forward button). The main variation is supposed to represent "
+"the real game played. If you want a side variation to become the main "
+"variation, select <guimenuitem>Make Main Variation</guimenuitem> from the "
+"<guimenu>Edit</guimenu> menu."
+msgstr ""
+"Die Hauptvariante ist die Zugfolge, die in der Startstellung beginnt und "
+"immer den ersten Kindknoten in jeder Brettstellung wählt (z. B. indem Sie "
+"den Vorwärts-Button drücken). Die Hauptvariante sollte das wirklich "
+"gespielte Spiel darstellen. Wenn Sie eine Nebenvariante zur Hauptvariante "
+"machen wollen, wählen Sie <guimenuitem>Zu Hauptvariante machen</guimenuitem>"
+" aus dem Menü <guimenu>Bearbeiten</guimenu>."
+
+#. (itstool) path: chapter/title
+#: index.docbook:478
+msgid "Become a Stronger Player"
+msgstr "Ein stärkerer Spieler werden"
+
+#. (itstool) path: chapter/para
+#: index.docbook:479
+msgid ""
+"Pentobi has functionality that can help you to become a stronger Blokus "
+"player."
+msgstr ""
+"Pentobi besitzt Funktionen, die Ihnen helfen können, ein stärkerer Blokus-"
+"Spieler zu werden."
+
+#. (itstool) path: sect1/title
+#: index.docbook:483
+msgid "Game Analysis"
+msgstr "Spielanalyse"
+
+#. (itstool) path: sect1/para
+#: index.docbook:484
+msgid ""
+"A game can be analyzed by selecting <guimenuitem>Analyze Game</guimenuitem> "
+"from the <guimenu>Tools</guimenu> 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."
+msgstr ""
+"Sie können ein Spiel analysieren, indem Sie <guimenuitem>Spiel "
+"analysieren</guimenuitem> aus dem Menü <guimenu>Extras</guimenu> 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."
+
+#. (itstool) path: caption/para
+#: index.docbook:496
+msgid "Analysis of a game of variant Classic (2 players)"
+msgstr "Analyse eines Spiels der Spielvariante Klassisch (2 Spieler)"
+
+#. (itstool) path: sect1/para
+#: index.docbook:500
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/para
+#: index.docbook:506
+msgid ""
+"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 <guimenuitem>Play Single Move</guimenuitem> from "
+"the <guimenu>Computer</guimenu> menu."
+msgstr ""
+"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 <guimenuitem>Einzelnen Zug spielen</guimenuitem> aus dem Menü "
+"<guimenu>Computer</guimenu> auswählen."
+
+#. (itstool) path: sect1/title
+#: index.docbook:516
+msgid "Determine Your Rating"
+msgstr "Ihre Wertung ermitteln"
+
+#. (itstool) path: sect1/para
+#: index.docbook:517
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/para
+#: index.docbook:522
+msgid ""
+"A rated game is started with <guimenuitem>Rated Game</guimenuitem> from the "
+"<guimenu>Game</guimenu> 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."
+msgstr ""
+"Ein gewertetes Spiel wird mit <guimenuitem>Gewertetes Spiel</guimenuitem> "
+"aus dem Menü <guimenu>Spiel</guimenu> 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."
+
+#. (itstool) path: sect1/para
+#: index.docbook:529
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/para
+#: index.docbook:534
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/para
+#: index.docbook:540
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: caption/para
+#: index.docbook:552
+msgid "Window with rating graph"
+msgstr "Fenster mit Wertungsgraph"
+
+#. (itstool) path: sect1/para
+#: index.docbook:556
+msgid ""
+"You can always see your current rating by selecting "
+"<guimenuitem>Rating</guimenuitem> from the <guimenu>Tools</guimenu> 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."
+msgstr ""
+"Sie können Ihre aktuelle Wertung jederzeit mit "
+"<guimenuitem>Wertung</guimenuitem> aus dem Menü <guimenu>Extras</guimenu> "
+"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."
+
+#. (itstool) path: chapter/title
+#: index.docbook:567
+msgid "Window Menu and Toolbar"
+msgstr "Fenstermenü und Werkzeugleiste"
+
+#. (itstool) path: sect1/title
+#: index.docbook:569
+msgid "Navigation Buttons in Toolbar"
+msgstr "Navigations-Buttons in der Werkzeugleiste"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:572
+msgid "<guibutton>Beginning</guibutton>"
+msgstr "<guibutton>Anfang</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:574
+msgid "Go to the beginning of the game."
+msgstr "Geht zum Anfang des Spiels."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:580
+msgid "<guibutton>Backward 10</guibutton>"
+msgstr "<guibutton>Zurück 10</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:582
+msgid ""
+"Go ten moves backward in the current variation. The button supports "
+"autorepeat if pressed and held."
+msgstr ""
+"Geht zehn Züge in der gegenwärtigen Variante zurück. Der Button unterstützt "
+"automatische Wiederholung, wenn er gedrückt gehalten wird."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:589
+msgid "<guibutton>Backward</guibutton>"
+msgstr "<guibutton>Zurück</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:591
+msgid ""
+"Go one move backward in the current variation. The button supports "
+"autorepeat if pressed and held."
+msgstr ""
+"Geht einen Zug in der gegenwärtigen Variante zurück. Der Button unterstützt "
+"automatische Wiederholung, wenn er gedrückt gehalten wird."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:598
+msgid "<guibutton>Forward</guibutton>"
+msgstr "<guibutton>Vorwärts</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:600
+msgid ""
+"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."
+msgstr ""
+"Geht einen Zug in der gegenwärtigen Variante vorwärts. Wenn die gegenwärtige"
+" Brettstellung mehrerer nachfolgende Varianten hat (d. h. der gegenwärtige "
+"Knoten im Spielbaum mehrere Kindknoten hat), wird die erste Variante "
+"benutzt. Der Button unterstützt automatische Wiederholung, wenn er gedrückt "
+"gehalten wird."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:609
+msgid "<guibutton>Forward 10</guibutton>"
+msgstr "<guibutton>Vorwärts 10</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:611
+msgid ""
+"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."
+msgstr ""
+"Geht zehn Züge in der gegenwärtigen Variante vorwärts. Wenn eine "
+"Brettstellung mehrerer nachfolgende Varianten hat (d. 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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:620
+msgid "<guibutton>End</guibutton>"
+msgstr "<guibutton>Ende</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:622
+msgid ""
+"Go to the end of the current variation. Like <guibutton>Forward</guibutton>,"
+" this also uses the first variation in positions with several follow-up "
+"variations."
+msgstr ""
+"Geht zum Ende der gegenwärtigen Variante. Wie bei "
+"<guibutton>Vorwärts</guibutton> wird auch hier jeweils die erste Variante "
+"benutzt, wenn die Brettstellung mehrere nachfolgende Varianten hat."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:630
+msgid "<guibutton>Next Variation</guibutton>"
+msgstr "<guibutton>Nächste Variante</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:632
+msgid ""
+"Go to the next variation to the last move played (i.e. the next sibling node"
+" of the current node in the game tree)."
+msgstr ""
+"Geht zur nächsten Variante zum zuletzt gespielten Zug (d. h. zum nächsten "
+"Geschwisterknoten des gegenwärtigen Knotens im Spielbaum)."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:639
+msgid "<guibutton>Previous Variation</guibutton>"
+msgstr "<guibutton>Vorherige Variante</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:641
+msgid ""
+"Go to the previous variation to the last move played (i.e. the previous "
+"sibling node of the current node in the game tree)."
+msgstr ""
+"Geht zur vorherigen Variante zum zuletzt gespielten Zug (d. h. zum "
+"vorherigen Geschwisterknoten des gegenwärtigen Knotens im Spielbaum)."
+
+#. (itstool) path: sect1/title
+#: index.docbook:650
+msgid "<guimenu>Game</guimenu> Menu"
+msgstr "<guimenu>Spiel</guimenu>-Menü"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:653
+msgid "<guimenuitem>New</guimenuitem>"
+msgstr "<guimenuitem>Neu</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:656
+msgid "Start a new game."
+msgstr "Beginnt ein neues Spiel."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:662
+msgid "<guimenuitem>Rated Game</guimenuitem>"
+msgstr "<guimenuitem>Gewertetes Spiel</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:664
+msgid ""
+"Start a new <link linkend=\"rating\">rated game</link> against the computer."
+msgstr ""
+"Beginnt ein neues <link linkend=\"rating\">gewertetes Spiel</link> gegen den"
+" Computer."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:670
+msgid "<guimenuitem>Game Variant</guimenuitem>"
+msgstr "<guimenuitem>Spielvariante</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:672
+msgid "Select a game variant and start a new game of this game variant."
+msgstr ""
+"Wählt eine Spielvariante und beginnt ein neues Spiel dieser Spielvariante."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:677
+msgid "<guimenuitem>Game Info</guimenuitem>"
+msgstr "<guimenuitem>Spielinformation</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:679
+msgid ""
+"Display or edit additional information about the game like the name of the "
+"players or the date when the game was played."
+msgstr ""
+"Ö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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:685
+msgid "<guimenuitem>Undo Move</guimenuitem>"
+msgstr "<guimenuitem>Zug rückgängig</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:687
+msgid ""
+"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 "
+"<guimenu>Edit</guimenu>/<guimenuitem>Truncate</guimenuitem> to remove inner "
+"nodes of the game tree)."
+msgstr ""
+"Nimmt den zuletzt gespielten Zug zurück und entfernt ihn aus dem Spielbaum. "
+"Das Zurücknehmen eines Zugs ist nur möglich, wenn er der letzte Zug der "
+"gegenwärtigen Variante ist (d. h. ein Endknoten im Spielbaum; benutzen Sie "
+"<guimenu>Bearbeiten</guimenu>/<guimenuitem>Abschneiden</guimenuitem> zum "
+"Entfernen innerer Knoten aus dem Spielbaum)."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:697
+msgid "<guimenuitem>Find Move</guimenuitem>"
+msgstr "<guimenuitem>Zug finden</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:699
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:706
+msgid "<guimenuitem>Open</guimenuitem>"
+msgstr "<guimenuitem>Öffnen</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:708
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:718
+msgid "<guimenu>Open Recent</guimenu>"
+msgstr "<guimenu>Zuletzt benutzt</guimenu>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:720
+msgid "Load a recently used game."
+msgstr "Lädt ein kürzlich benutztes Spiel."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:726
+msgid "<guimenuitem>Open Clipboard</guimenuitem>"
+msgstr "<guimenuitem>Zwischenablage öffnen</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:728
+msgid ""
+"Open a game from a text copied to the clipboard. The text must be a valid "
+"game in Pentobi SGF file format."
+msgstr ""
+"Öffnet ein Spiel von einem Text, der in die Zwischenablage kopiert wurde. "
+"Der Text muss ein gültiges Spiel im Pentobi-SGF-Dateiformat sein."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:735
+msgid "<guimenuitem>Save</guimenuitem>"
+msgstr "<guimenuitem>Speichern</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:737
+msgid "Save the current game."
+msgstr "Speichert das gegenwärtige Spiel."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:743
+msgid "<guimenuitem>Save As</guimenuitem>"
+msgstr "<guimenuitem>Speichern unter</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:745
+msgid "Save the current game under a new file name."
+msgstr "Speichert das gegenwärtige Spiel unter einem neuen Dateinamen."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:751
+msgid "<guimenu>Export</guimenu>/<guimenuitem>Image</guimenuitem>"
+msgstr "<guimenu>Exportieren</guimenu>/<guimenuitem>Grafik</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:753
+msgid ""
+"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."
+msgstr ""
+"Speichert die gegenwärtige Brettstellung als eine Grafikdatei. Mehrere "
+"Grafikdateiformate werden unterstützt, das Dateiformat wird von der "
+"Dateiendung abgeleitet (z. B. „.png“ für das PNG-Format). Für ein gestochen "
+"scharfes Bild sollte die Bildbreite ein ganzzahliges Vielfaches der "
+"Spaltenanzahl des Bretts in der gegenwärtigen Spielvariante sein."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:762
+msgid "<guimenu>Export</guimenu>/<guimenuitem>ASCII Art</guimenuitem>"
+msgstr "<guimenu>Exportieren</guimenu>/<guimenuitem>ASCII-Art</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:764
+msgid ""
+"Save the current position as a text diagram. The text diagram should be "
+"viewed using a monospace font."
+msgstr ""
+"Speichert die gegenwärtige Brettstellung als Textdiagramm. Das Textdiagramm "
+"sollte mit einer Schriftart fester Breite betrachtet werden."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:771
+msgid "<guimenuitem>Quit</guimenuitem>"
+msgstr "<guimenuitem>Beenden</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:773
+msgid "Quit Pentobi."
+msgstr "Beendet Pentobi."
+
+#. (itstool) path: sect1/title
+#: index.docbook:781
+msgid "<guimenu>Go</guimenu> Menu"
+msgstr "<guimenu>Gehe-zu</guimenu>-Menü"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:784
+msgid "<guimenuitem>Move Number</guimenuitem>"
+msgstr "<guimenuitem>Zugnummer</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:786
+msgid "Go to the move with a given move number in the current variation."
+msgstr ""
+"Geht zum Zug mit einer bestimmten Nummer in der gegenwärtigen Variante."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:792
+msgid "<guimenuitem>Main Variation</guimenuitem>"
+msgstr "<guimenuitem>Hauptvariante</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:794
+msgid ""
+"Go back to the last position in the current variation that belonged to the "
+"main variation."
+msgstr ""
+"Kehrt zur letzten Brettstellung in der gegenwärtigen Variante zurück, die "
+"zur Hauptvariante gehörte."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:801
+msgid "<guimenuitem>Beginning of Branch</guimenuitem>"
+msgstr "<guimenuitem>Verzweigungsanfang</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:803
+msgid ""
+"Go back to the last position in the current variation that had an "
+"alternative move."
+msgstr ""
+"Kehrt zur letzten Brettstellung in der gegenwärtigen Variante zurück, die "
+"einen alternativen Zug hatte."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:810
+msgid "<guimenuitem>Next Comment</guimenuitem>"
+msgstr "<guimenuitem>Nächster Kommentar</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:812
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/title
+#: index.docbook:822
+msgid "<guimenu>Edit</guimenu> Menu"
+msgstr "<guimenu>Bearbeiten</guimenu>-Menü"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:825
+msgid "<guimenuitem>Annotation</guimenuitem>"
+msgstr "<guimenuitem>Annotierung</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:827
+msgid ""
+"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 <guilabel>Move Marking</guilabel>, on the board."
+msgstr ""
+"Fügt ein wie in der Schachnotation benutztes Symbol (z. B. ‼) zum "
+"gegenwärtigen Zug hinzu. Die Symbole werden an die Zugnummern in der "
+"Statusleiste angehängt und, abhängig von der Einstellung von "
+"<guilabel>Zugmarkierung</guilabel>, an die auf dem Spielbrett."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:835
+msgid "<guimenuitem>Make Main Variation</guimenuitem>"
+msgstr "<guimenuitem>Zu Hauptvariante machen</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:837
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:845
+msgid "<guimenuitem>Variation Up</guimenuitem>"
+msgstr "<guimenuitem>Variante nach oben</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:847
+msgid ""
+"Changes the order of variations such that the current position will appear "
+"earlier when iterating over the variations with <guibutton>Next "
+"Variation</guibutton> /<guibutton>Previous Variation</guibutton>."
+msgstr ""
+"Ändert die Reihenfolge der Varianten so, dass die gegenwärtige Brettstellung"
+" beim Durchlaufen der Varianten mit <guibutton>Nächste Variante</guibutton> "
+"/<guibutton>Vorherige Variante</guibutton> früher erscheint."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:856
+msgid "<guimenuitem>Variation Down</guimenuitem>"
+msgstr "<guimenuitem>Variante nach unten</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:858
+msgid ""
+"Changes the order of variations such that the current position will appear "
+"later when iterating over the variations with <guibutton>Next "
+"Variation</guibutton> /<guibutton>Previous Variation</guibutton>."
+msgstr ""
+"Ändert die Reihenfolge der Varianten so, dass die gegenwärtige Brettstellung"
+" beim Durchlaufen der Varianten mit <guibutton>Nächste Variante</guibutton> "
+"/<guibutton>Vorherige Variante</guibutton> später erscheint."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:867
+msgid "<guimenuitem>Delete Variations</guimenuitem>"
+msgstr "<guimenuitem>Varianten löschen</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:869
+msgid ""
+"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 "
+"<guimenuitem>Back to Main Variation</guimenuitem>."
+msgstr ""
+"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 <guimenuitem>Zurück zu "
+"Hauptvariante</guimenuitem>."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:877
+msgid "<guimenuitem>Truncate</guimenuitem>"
+msgstr "<guimenuitem>Abschneiden</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:879
+msgid ""
+"Remove the node with the current position, including any subtree, from the "
+"game tree."
+msgstr ""
+"Entfernt den Knoten mit der gegenwärtigen Brettstellung zusammen mit dem auf"
+" ihn folgenden Teilbaum aus dem Spielbaum."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:886
+msgid "<guimenuitem>Truncate Children</guimenuitem>"
+msgstr "<guimenuitem>Kindknoten abschneiden</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:888
+msgid ""
+"Remove all child nodes of the node with the current position from the game "
+"tree."
+msgstr ""
+"Entfernt alle Kindknoten des Knotens mit der gegenwärtigen Brettstellung aus"
+" dem Spielbaum."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:895
+msgid "<guimenuitem>Keep Position</guimenuitem>"
+msgstr "<guimenuitem>Brettstellung behalten</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:897
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:904
+msgid "<guimenuitem>Keep Subtree</guimenuitem>"
+msgstr "<guimenuitem>Teilbaum behalten</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:906
+msgid ""
+"Like <guimenuitem>Keep Position</guimenuitem> but does not delete the moves "
+"after the current position."
+msgstr ""
+"Wie <guimenuitem>Brettstellung behalten</guimenuitem>, aber die Züge nach "
+"der gegenwärtigen Brettstellung werden nicht gelöscht."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:913
+msgid "<guimenuitem>Setup Mode</guimenuitem>"
+msgstr "<guimenuitem>Stellungsaufbau</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:915
+msgid ""
+"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 <guimenuitem>Next Color</guimenuitem> 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."
+msgstr ""
+"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 "
+"<guimenuitem>Nächste Farbe</guimenuitem> 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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:927
+msgid "<guimenuitem>Next Color</guimenuitem>"
+msgstr "<guimenuitem>Nächste Farbe</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:929
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/title
+#: index.docbook:939
+msgid "<guimenu>View</guimenu> Menu"
+msgstr "<guimenu>Ansicht</guimenu>-Menü"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:941
+msgid "<guimenuitem>Appearance</guimenuitem>"
+msgstr "<guimenuitem>Erscheinungsbild</guimenuitem>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:946
+msgid "<guilabel>Coordinates</guilabel>"
+msgstr "<guilabel>Koordinaten</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:948
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:956
+msgid "<guilabel>Show variations</guilabel>"
+msgstr "<guilabel>Varianten zeigen</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:958
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:966
+msgid "<guilabel>Move number</guilabel>"
+msgstr "<guilabel>Zugnummer</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:968
+msgid ""
+"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."
+msgstr ""
+"Diese Option existiert nur im Desktop-Modus und zeigt die Zugnummer, "
+"Zugannotierung und Varianteninformation auf der rechten Seite der "
+"Statusleiste an."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:975
+msgid "<guilabel>Animations</guilabel>"
+msgstr "<guilabel>Animationen</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:977
+msgid ""
+"Enables or disables the piece rotation, flipping and placement animations."
+msgstr ""
+"Aktiviert oder deaktiviert die Animationen für Drehen, Umdrehen und "
+"Platzieren der Spielsteine."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:983
+msgid "<guilabel>Color theme</guilabel>"
+msgstr "<guilabel>Farbthema</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:985
+msgid "Changes the colors of the window, board and pieces."
+msgstr "Ändert die Farben des Fensters, des Spielbretts und der Spielsteine."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:991
+msgid "<guilabel>Move marking</guilabel>"
+msgstr "<guilabel>Zugmarkierung</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:993
+msgid ""
+"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."
+msgstr ""
+"Ä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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1001
+msgid "<guilabel>Show comment</guilabel>"
+msgstr "<guilabel>Kommentar zeigen</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1003
+msgid ""
+"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."
+msgstr ""
+"Diese Option existiert nur im Desktop-Modus und konfiguriert die "
+"Sichtbarkeit des Kommentarbereichs, wenn sich die Stellung ändert. Die "
+"Standardeinstellung ist, dass der Kommentarbereich nur sichtbar ist, wenn "
+"ein Kommentar zur aktuellen Stellung existiert."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1015
+msgid "<guimenuitem>Comment</guimenuitem>"
+msgstr "<guimenuitem>Kommentar</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1017
+msgid "Toggle the visibility of the comment area in the current position."
+msgstr ""
+"Mach den Kommentarbereich in der aktuellen Stellung sichtbar oder nicht "
+"sichtbar."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1023
+msgid "<guimenuitem>Fullscreen</guimenuitem>"
+msgstr "<guimenuitem>Vollbild</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1025
+msgid ""
+"Make the main window full screen or leave full screen mode. To leave "
+"fullscreen mode without using the window menu, press the F11 key."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/title
+#: index.docbook:1034
+msgid "<guimenu>Computer</guimenu> Menu"
+msgstr "<guimenu>Computer</guimenu>-Menü"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1037
+msgid "<guimenuitem>Settings</guimenuitem>"
+msgstr "<guimenuitem>Einstellungen</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1039
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1047
+msgid "<guimenuitem>Play</guimenuitem>"
+msgstr "<guimenuitem>Spielen</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1049
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1058
+msgid "<guimenuitem>Play Move</guimenuitem>"
+msgstr "<guimenuitem>Zug spielen</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1060
+msgid ""
+"Make the computer play a single move for the current color without changing "
+"the colors played by the computer."
+msgstr ""
+"Lässt den Computer einen einzelnen Zug für die gegenwärtige Farbe spielen "
+"ohne die vom Computer gespielten Farben zu ändern."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1067
+msgid "<guimenuitem>Stop</guimenuitem>"
+msgstr "<guimenuitem>Stopp</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1069
+msgid ""
+"Abort the current move generation. You can make the computer continue to "
+"play by selecting <guimenuitem>Play</guimenuitem>."
+msgstr ""
+"Bricht die gegenwärtige Zuggenerierung ab. Sie können den Computer "
+"weiterspielen lassen, indem Sie <guimenuitem>Spielen</guimenuitem> "
+"auswählen."
+
+#. (itstool) path: sect1/title
+#: index.docbook:1078
+msgid "<guimenu>Tools</guimenu> Menu"
+msgstr "<guimenu>Extras</guimenu>-Menü"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1081
+msgid "<guimenuitem>Rating</guimenuitem>"
+msgstr "<guimenuitem>Wertung</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1083
+msgid ""
+"Show a dialog window with the <link linkend=\"rating\">rating</link> of the "
+"user in the current game variant."
+msgstr ""
+"Zeigt ein Dialogfenster mit der <link linkend=\"rating\">Wertung</link> des "
+"Benutzers in der gegenwärtigen Spielvariante."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1090
+msgid "<guimenuitem>Clear Rating</guimenuitem>"
+msgstr "<guimenuitem>Wertung löschen</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1092
+msgid ""
+"Deletes the rating information and history for the current game variant."
+msgstr ""
+"Löscht die Wertungsinformationen und -entwicklung zur gegenwärtigen "
+"Spielvariante."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1098
+msgid "<guimenuitem>Analyze Game</guimenuitem>"
+msgstr "<guimenuitem>Spiel analysieren</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1100
+msgid "Perform a <link linkend=\"analysis\">game analysis</link>."
+msgstr "Führt eine <link linkend=\"analysis\">Spielanalyse</link> durch."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1106
+msgid "<guimenuitem>Clear Analysis</guimenuitem>"
+msgstr "<guimenuitem>Analyse löschen</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1108
+msgid "Deletes the analysis for the current game."
+msgstr "Löscht die Analyse des gegenwärtigen Spiels."
+
+#. (itstool) path: sect1/title
+#: index.docbook:1116
+msgid "<guimenu>Help</guimenu> Menu"
+msgstr "<guimenu>Hilfe</guimenu>-Menü"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1119
+msgid "<guimenuitem>Pentobi Help</guimenuitem>"
+msgstr "<guimenuitem>Pentobi-Hilfe</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1121
+msgid "Show a window to browse the Pentobi user manual."
+msgstr "Zeigt ein Fenster mit dem Pentobi-Benutzerhandbuch."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1127
+msgid "<guimenuitem>About Pentobi</guimenuitem>"
+msgstr "<guimenuitem>Über Pentobi</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1129
+msgid "Show an info dialog with information about this version of Pentobi."
+msgstr ""
+"Zeigt eine Dialogfenster mit Informationen über diese Version von Pentobi."
+
+#. (itstool) path: chapter/title
+#: index.docbook:1139
+msgid "Keyboard Shortcuts"
+msgstr "Tastenkürzel"
+
+#. (itstool) path: chapter/para
+#: index.docbook:1140
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1148
+msgid "<keysym>Plus</keysym>"
+msgstr "<keysym>Plus</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1150
+msgid "Select next piece"
+msgstr "Nächsten Spielstein auswählen"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1156
+msgid "<keysym>Minus</keysym>"
+msgstr "<keysym>Minus</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1158
+msgid "Select previous piece"
+msgstr "Vorherigen Spielstein auswählen"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1164
+msgid "<keysym>Escape</keysym>"
+msgstr "<keysym>Escape</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1166
+msgid "Clear selected piece"
+msgstr "Spielsteinauswahl löschen"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1172
+msgid "<keysym>Left</keysym>"
+msgstr "<keysym>Links</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1173
+msgid "<keysym>Right</keysym>"
+msgstr "<keysym>Rechts</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1174
+msgid "<keysym>Up</keysym>"
+msgstr "<keysym>Oben</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1175
+msgid "<keysym>Down</keysym>"
+msgstr "<keysym>Unten</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1176
+msgid "<keysym>Shift+Left</keysym>"
+msgstr "<keysym>Umschalt+Links</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1177
+msgid "<keysym>Shift+Right</keysym>"
+msgstr "<keysym>Umschalt+Rechts</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1178
+msgid "<keysym>Shift+Up</keysym>"
+msgstr "<keysym>Umschalt+Oben</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1179
+msgid "<keysym>Shift+Down</keysym>"
+msgstr "<keysym>Umschalt+Unten</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1181
+msgid "Move the selected piece. The Shift key makes the piece move faster."
+msgstr ""
+"Bewegen des ausgewählten Spielsteins. Mit der Umschalttaste wird der "
+"Spielstein schneller bewegt."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1187
+msgid "<keysym>Space</keysym>"
+msgstr "<keysym>Leertaste</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1189
+msgid "Next orientation of the selected piece"
+msgstr "Nächste Ausrichtung des ausgewählten Spielsteins"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1195
+msgid "<keysym>Shift+Space</keysym>"
+msgstr "<keysym>Umschalt+Leertaste</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1197
+msgid "Previous orientation of the selected piece"
+msgstr "Vorherige Ausrichtung des ausgewählten Spielsteins"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1203
+msgid "<keysym>Enter</keysym>"
+msgstr "<keysym>Enter</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1205
+msgid "Play the selected piece"
+msgstr "Spielen des ausgewählten Spielsteins"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1211
+msgid "<keysym>1</keysym>"
+msgstr "<keysym>1</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1212
+msgid "<keysym>2</keysym>"
+msgstr "<keysym>2</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1213
+msgid "<keysym>A</keysym>"
+msgstr "<keysym>A</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1214
+msgid "<keysym>C</keysym>"
+msgstr "<keysym>C</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1215
+msgid "<keysym>E</keysym>"
+msgstr "<keysym>E</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1216
+msgid "<keysym>F</keysym>"
+msgstr "<keysym>F</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1217
+msgid "<keysym>G</keysym>"
+msgstr "<keysym>G</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1218
+msgid "<keysym>H</keysym>"
+msgstr "<keysym>H</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1219
+msgid "<keysym>I</keysym>"
+msgstr "<keysym>I</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1220
+msgid "<keysym>J</keysym>"
+msgstr "<keysym>J</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1221
+msgid "<keysym>L</keysym>"
+msgstr "<keysym>L</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1222
+msgid "<keysym>N</keysym>"
+msgstr "<keysym>N</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1223
+msgid "<keysym>O</keysym>"
+msgstr "<keysym>O</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1224
+msgid "<keysym>P</keysym>"
+msgstr "<keysym>P</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1225
+msgid "<keysym>S</keysym>"
+msgstr "<keysym>S</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1226
+msgid "<keysym>T</keysym>"
+msgstr "<keysym>T</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1227
+msgid "<keysym>U</keysym>"
+msgstr "<keysym>U</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1228
+msgid "<keysym>V</keysym>"
+msgstr "<keysym>V</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1229
+msgid "<keysym>W</keysym>"
+msgstr "<keysym>W</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1230
+msgid "<keysym>X</keysym>"
+msgstr "<keysym>X</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1231
+msgid "<keysym>Y</keysym>"
+msgstr "<keysym>Y</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1232
+msgid "<keysym>Z</keysym>"
+msgstr "<keysym>Z</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1234
+msgid ""
+"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\")."
+msgstr ""
+"Einen Spielstein entsprechend den üblicherweise benutzten Spielsteinnamen "
+"auswählen. Wenn es mehrere Spielsteine mit dem Buchstaben gibt (z. B. I3, "
+"I4, I5), dann kann durch mehrmaliges Drücken der Taste zwischen ihnen "
+"gewechselt werden. Einige Buchstaben werden nur in bestimmten Spielvarianten"
+" benutzt. Zum Beispiel wird A nur in Trigon für die Spielsteine A6 und A4 "
+"benutzt (auch bekannt als „Hummer“ und „Dreieck“)."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1244
+msgid "<keysym>Ctrl+Home</keysym>"
+msgstr "<keysym>Strg+Pos1</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1245
+msgid "<keysym>Ctrl+Shift+Left</keysym>"
+msgstr "<keysym>Strg+Umschalt+Links</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1246
+msgid "<keysym>Ctrl+Left</keysym>"
+msgstr "<keysym>Strg+Links</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1247
+msgid "<keysym>Ctrl+Right</keysym>"
+msgstr "<keysym>Strg+Rechts</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1248
+msgid "<keysym>Ctrl+Shift+Right</keysym>"
+msgstr "<keysym>Strg+Umschalt+Rechts</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1249
+msgid "<keysym>Ctrl+End</keysym>"
+msgstr "<keysym>Strg+Ende</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1250
+msgid "<keysym>Ctrl+Up</keysym>"
+msgstr "<keysym>Strg+Oben</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1251
+msgid "<keysym>Ctrl+Down</keysym>"
+msgstr "<keysym>Strg+Unten</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1253
+msgid ""
+"Navigate in the game: beginning, ten moves backward, backward, forward, ten "
+"moves forward, end, previous variation, next variation."
+msgstr ""
+"Im Spiel navigieren: Anfang, zehn Züge zurück, zurück, vorwärts, zehn Züge "
+"vorwärts, Ende, vorherige Variante, nächste Variante."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1260
+msgid "<keysym>Ctrl+Shift+H</keysym>"
+msgstr "<keysym>Strg+Umschalt+H</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1262
+msgid ""
+"Like <guimenuitem>Find Move</guimenuitem> (Ctrl+H) but iterates backwards "
+"through the list of legal moves."
+msgstr ""
+"Wie <guimenuitem>Zug finden</guimenuitem> (Strg+H), jedoch wird rückwärts "
+"durch die Liste der legalen Züge iteriert."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1269
+msgid "<keysym>Ctrl+T</keysym>"
+msgstr "<keysym>Strg+T</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1271
+msgid "Switch view between comment and game analysis."
+msgstr "Ansicht zwischen Kommentar und Spielanalyse umschalten."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1277
+msgid "<keysym>Alt+M</keysym>"
+msgstr "<keysym>Alt+M</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1279
+msgid "Open menu."
+msgstr "Menü öffnen."
+
+#. (itstool) path: chapter/title
+#: index.docbook:1288
+msgid "System Requirements"
+msgstr "Systemvoraussetzungen"
+
+#. (itstool) path: chapter/para
+#: index.docbook:1289
+msgid "Minimum: 1 GB RAM, 1 GHz CPU"
+msgstr "Minimum: 1 GB RAM, 1 GHz CPU"
+
+#. (itstool) path: chapter/para
+#: index.docbook:1292
+msgid ""
+"Recommended for playing level 9: 4 GB RAM, 2.5 GHz dual-core or faster CPU"
+msgstr ""
+"Empfohlen für Spielstufe 9: 4 GB RAM, 2,5 GHz Dual-Core- oder schnellere CPU"
+
+#. (itstool) path: chapter/para
+#: index.docbook:1296
+msgid ""
+"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)."
+msgstr ""
+"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)."
+
+#. (itstool) path: chapter/title
+#: index.docbook:1304
+msgid "License"
+msgstr "Lizenz"
+
+#. (itstool) path: chapter/para
+#: index.docbook:1305
+msgid "Copyright © 2011–2020 Markus Enzenberger"
+msgstr "Copyright © 2011–2020 Markus Enzenberger"
+
+#. (itstool) path: chapter/para
+#: index.docbook:1308
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: chapter/para
+#: index.docbook:1314
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#. (itstool) path: sect1/title
+#: index.docbook:1321
+msgid "Trademark Disclaimer"
+msgstr "Hinweis zu Markennamen"
+
+#. (itstool) path: sect1/para
+#: index.docbook:1322
+msgid ""
+"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."
+msgstr ""
+"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."
diff --git a/pentobi/docbook/po/es.po b/pentobi/docbook/po/es.po
new file mode 100644 (file)
index 0000000..8a83e69
--- /dev/null
@@ -0,0 +1,2191 @@
+# 
+# Translators:
+# Francisco Zamorano <pacozamo@gmail.com>, 2019
+# Markus Enzenberger <markus.enzenberger@gmail.com>, 2020
+# 
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2019-02-11 17:37+0100\n"
+"PO-Revision-Date: 2019-02-25 15:30+0000\n"
+"Last-Translator: Markus Enzenberger <markus.enzenberger@gmail.com>, 2020\n"
+"Language-Team: Spanish (https://www.transifex.com/markus-enzenberger/teams/89074/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. (itstool) path: book/title
+#: index.docbook:5
+msgid "Pentobi"
+msgstr "Pentobi"
+
+#. (itstool) path: chapter/title
+#: index.docbook:8
+msgid "Overview"
+msgstr "Descripción"
+
+#. (itstool) path: chapter/para
+#: index.docbook:9
+msgid ""
+"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."
+msgstr ""
+"Pentobi es un rival virtual para el juego de mesa Blokus. En este juego, "
+"hasta cuatro jugadores se dedican a colocar piezas parecidas a las del "
+"videojuego Tetris en un tablero de 20x20 casillas. Además, Pentobi también "
+"cuenta con variantes de juego para dos o tres jugadores y con las variantes "
+"de juego Dúo, Trigón, Sencillo, Nexos, GembloQ y Calisto."
+
+#. (itstool) path: chapter/title
+#: index.docbook:18
+msgid "Classic Rules"
+msgstr "Reglas de Clásico"
+
+#. (itstool) path: chapter/para
+#: index.docbook:19
+msgid ""
+"There are four players, Blue, Yellow, Red and Green, and a board consisting "
+"of 20×20 squares. Each player has a set of 21 pieces of his color shaped "
+"like the polyominoes up to size five. A polyomino is a shape built from a "
+"number of squares connected along the edges."
+msgstr ""
+"Hay cuatro jugadores: azul, amarillo, rojo y verde, y un tablero compuesto "
+"de 20x20 casillas. Cada jugador tiene un conjunto de 21 piezas de su color "
+"con forma de poliominós con un tamaño de hasta cinco cuadrados. Un poliominó"
+" es una forma construida a partir de la conexión por los laterales de "
+"cuadrados."
+
+#. (itstool) path: caption/para
+#: index.docbook:30 index.docbook:289 index.docbook:353
+msgid "The 21 pieces"
+msgstr "Las 21 piezas"
+
+#. (itstool) path: chapter/para
+#: index.docbook:33
+msgid ""
+"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."
+msgstr ""
+"Los jugadores juegan por turnos tratando de colocar una de sus piezas sobre "
+"el tablero. Con la primera pieza que coloque un jugador se tiene que cubrir "
+"la casilla de inicio. Las casillas de inicio están colocadas en las esquinas"
+" del tablero."
+
+#. (itstool) path: caption/para
+#: index.docbook:44
+msgid "The 20×20 board with the starting squares marked with colored dots"
+msgstr ""
+"El tablero de 20x20 con las casillas de inicio marcadas con puntos de "
+"colores"
+
+#. (itstool) path: chapter/para
+#: index.docbook:50
+msgid ""
+"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."
+msgstr ""
+"Las siguientes piezas deben colocarse sobre casillas vacías de tal manera "
+"que la nueva pieza toque al menos por una esquina una pieza del mismo color,"
+" pero no puede tocar ninguna otra pieza del mismo color por los lados. La "
+"nueva pieza puede tocar los lados de las piezas de otros colores rivales."
+
+#. (itstool) path: caption/para
+#: index.docbook:61 index.docbook:260 index.docbook:320 index.docbook:383
+msgid "An example position after a few moves"
+msgstr "Una posición de ejemplo después de algunos movimientos"
+
+#. (itstool) path: chapter/para
+#: index.docbook:64
+msgid ""
+"When the player of a color cannot place any more pieces, the player passes "
+"and the next color continues. When none of the players can place any more "
+"pieces, the player with the highest score wins. The score of a color is the "
+"number of squares on the board occupied by the color, plus a bonus of 15 "
+"points if the color could place all of its pieces, plus an additional bonus "
+"of 5 points if the color could place all pieces and the last piece played "
+"was the one-square piece."
+msgstr ""
+"Cuando el jugador de un color ya no puede colocar ninguna pieza más, el "
+"jugador pasa su turno para que continúe el jugador del siguiente color. "
+"Cuando ninguno de los jugadores puede colocar más piezas, el jugador con la "
+"puntuación más alta es el ganador. La puntuación de un color resulta del "
+"número de cuadrados de ese color ocupados sobre el tablero, más una "
+"bonificación de 15 puntos si el color fue capaz de colocar todas sus piezas,"
+" además de una bonificación adicional de 5 puntos si el color pudo colocar "
+"todos las piezas y la última pieza que jugó fue la pieza de un solo "
+"cuadrado."
+
+#. (itstool) path: sect1/title
+#: index.docbook:74 index.docbook:178 index.docbook:268
+msgid "Rules for Two Players"
+msgstr "Reglas para dos jugadores"
+
+#. (itstool) path: sect1/para
+#: index.docbook:75
+msgid ""
+"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."
+msgstr ""
+"La partida puede jugarse a dos jugadores. El primer jugador juega con azules"
+" y rojas y el segundo jugador juega con amarillas y verdes. Al final se "
+"sumarán las puntuaciones de los dos colores con los que esté jugando el "
+"jugador."
+
+#. (itstool) path: sect1/title
+#: index.docbook:82 index.docbook:185
+msgid "Rules for Three Players"
+msgstr "Reglas para tres jugadores"
+
+#. (itstool) path: sect1/para
+#: index.docbook:83
+msgid ""
+"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."
+msgstr ""
+"Se pueden realizar también partidas de tres jugadores. Los jugadores se "
+"turnan para jugar con el cuarto color (verde). Al final de la partida, la "
+"puntuación de las verdes se ignora."
+
+#. (itstool) path: sect1/title
+#: index.docbook:90
+msgid "Colorless Starting Points"
+msgstr "Puntos de inicio sin color"
+
+#. (itstool) path: sect1/para
+#: index.docbook:91
+msgid ""
+"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 at the first Blokus online servers and Blokus tournaments."
+msgstr ""
+"Tenga en cuenta que las reglas clásicas del Blokus original usaban puntos de"
+" inicio sin color. Esto significa que cada color puede elegir libremente "
+"cuál de los puntos iniciales que no estén ocupados puede utilizar para "
+"realizar su primer movimiento. En estos momentos, Pentobi solo permite jugar"
+" con la variante de la regla con puntos de inicio con color, ya que fue esta"
+" variante de regla la que se usó en los primeros servidores en línea de "
+"Blokus y en los torneos de Blokus."
+
+#. (itstool) path: chapter/title
+#: index.docbook:102
+msgid "Duo Rules"
+msgstr "Reglas de Dúo"
+
+#. (itstool) path: chapter/para
+#: index.docbook:103
+msgid ""
+"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."
+msgstr ""
+"La variante de juego Dúo es otra variante de juego para dos jugadores. La "
+"partida se desarrolla en un tablero más pequeño de 14x14 casillas. Cada "
+"jugador juega con un único color (morado y naranja) y las casillas de inicio"
+" no se encuentran en las esquinas, sino en la casilla con las coordenadas "
+"(5,10) para el morado, y en (10,5) para el naranja."
+
+#. (itstool) path: caption/para
+#: index.docbook:114
+msgid ""
+"The 14×14 board used in game variant Duo with the starting squares marked "
+"with colored dots"
+msgstr ""
+"El tablero de 14x14 que se usa en la variante de juego Dúo con las casillas "
+"de inicio marcadas con puntos de colores"
+
+#. (itstool) path: caption/para
+#: index.docbook:126
+msgid "An example position in game variant Duo"
+msgstr "Una posición de ejemplo en la variante de juego Dúo"
+
+#. (itstool) path: chapter/title
+#: index.docbook:132
+msgid "Trigon Rules"
+msgstr "Reglas de Trigón"
+
+#. (itstool) path: chapter/para
+#: index.docbook:133
+msgid ""
+"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."
+msgstr ""
+"Trigón es otra variante de juego. Las reglas son similares a la variante de "
+"juego clásica, pero en este caso se juega sobre un tablero con una forma "
+"diferente y un conjunto de piezas distinto. Cada color utiliza 22 piezas que"
+" tienen forma de polidiamante con un tamaño máximo de seis triángulos. Un "
+"polidiamante es un forma compuesta por un determinado número de triángulos "
+"equiláteros conectados por los lados."
+
+#. (itstool) path: caption/para
+#: index.docbook:145
+msgid "The 22 Trigon pieces"
+msgstr "Las 22 piezas de Trigón"
+
+#. (itstool) path: chapter/para
+#: index.docbook:148
+msgid ""
+"The board also consists of triangles and is shaped like a hexagon with an "
+"edge size of nine triangles."
+msgstr ""
+"El tablero también puede estar formado por triángulos y tiene forma de "
+"hexágono con un tamaño del borde de nueve triángulos."
+
+#. (itstool) path: caption/para
+#: index.docbook:158
+msgid "The board with the starting fields marked with gray dots"
+msgstr "El tablero con campos de inicio marcado con puntos grises"
+
+#. (itstool) path: chapter/para
+#: index.docbook:162
+msgid ""
+"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."
+msgstr ""
+"Hay seis puntos de inicio sobre el tablero, cada uno de ellos situado en "
+"mitad de la cuarta fila contada a partir de cada uno de los laterales del "
+"tablero. Los puntos de inicio no pertenecen a un color determinado, por lo "
+"que los jugadores pueden elegir libremente en qué punto de inicio desean "
+"colocar la primera pieza de su color."
+
+#. (itstool) path: caption/para
+#: index.docbook:172
+msgid "An example position in game variant Trigon"
+msgstr "Una posición de ejemplo en la variante de juego Trigón"
+
+#. (itstool) path: sect1/para
+#: index.docbook:179
+msgid ""
+"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."
+msgstr ""
+"Como en el caso de la variante de juego Clásico, Trigón se puede jugar a dos"
+" jugadores, con un jugador a cargo de las azules y rojas y el otro jugador a"
+" cargo de amarillas y verdes."
+
+#. (itstool) path: sect1/para
+#: index.docbook:186
+msgid ""
+"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."
+msgstr ""
+"Trigón se puede jugar a tres jugadores utilizando las mismas reglas que para"
+" la variante de cuatro jugadores. La variante de tres jugadores se juega "
+"sobre un tablero más pequeño con un tamaño de los laterales de ocho "
+"triángulos. Los puntos de inicio están situados en mitad de la tercera fila "
+"contada a partir de cada uno de los laterales del tablero."
+
+#. (itstool) path: chapter/title
+#: index.docbook:196
+msgid "Junior Rules"
+msgstr "Reglas de Sencillo"
+
+#. (itstool) path: chapter/para
+#: index.docbook:197
+msgid ""
+"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."
+msgstr ""
+"Sencillo es una variante de juego simplificada para dos jugadores. Se juega "
+"sobre el mismo tablero de 14x14 de la variante Dúo pero utiliza un grupo de "
+"poliominós con un máximo de cinco cuadrados y los jugadores reciben dos de "
+"cada uno de esos poliominós."
+
+#. (itstool) path: caption/para
+#: index.docbook:207
+msgid "The 24 pieces used in Junior"
+msgstr "Las 24 piezas utilizadas en Sencillo"
+
+#. (itstool) path: chapter/para
+#: index.docbook:210
+msgid "Bonus points are not used in Junior."
+msgstr "No se utilizan puntos de bonificación en Sencillo."
+
+#. (itstool) path: chapter/title
+#: index.docbook:216
+msgid "Nexos Rules"
+msgstr "Reglas de Nexos"
+
+#. (itstool) path: chapter/para
+#: index.docbook:217
+msgid ""
+"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."
+msgstr ""
+"Nexos es un juego de mesa parecido a Blokus. El tablero es una cuadrícula "
+"rectangular de 13x13 líneas. Cada color utiliza 24 piezas compuestas de un "
+"máximo de cuatro segmentos de línea unidos entre sí."
+
+#. (itstool) path: caption/para
+#: index.docbook:227
+msgid "The 24 pieces"
+msgstr "Las 24 piezas"
+
+#. (itstool) path: chapter/para
+#: index.docbook:230
+msgid ""
+"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."
+msgstr ""
+"A cada color le corresponde una intersección de inicio cercana a cada "
+"esquina donde se entrecruzan la tercera línea vertical y horizontal. La "
+"primera pieza debe tocar la intersección de inicio."
+
+#. (itstool) path: caption/para
+#: index.docbook:240
+msgid ""
+"The board for Nexos with the starting intersections marked with colored dots"
+msgstr ""
+"El tablero para Nexos con las intersecciones de inicio marcadas con puntos "
+"de colores"
+
+#. (itstool) path: chapter/para
+#: index.docbook:246
+msgid ""
+"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."
+msgstr ""
+"Las siguientes piezas deben colocarse sobre segmentos de línea vacíos de tal"
+" manera que uno de los segmentos de la nueva pieza esté en contacto con una "
+"intersección que ya se encuentre en contacto con un segmento del mismo "
+"color. No importa si otras piezas de distinto color tocan o cubren la misma "
+"intersección. Sin embargo, las piezas no pueden solaparse. Las uniones entre"
+" segmentos de una pieza están hechas así para que las uniones rectangulares "
+"de piezas distintas puedan cubrir la misma intersección sin solaparse, pero "
+"esto no se puede hacer en el caso de las uniones rectas. "
+
+#. (itstool) path: chapter/para
+#: index.docbook:263
+msgid ""
+"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."
+msgstr ""
+"La puntuación de cada color es el número de segmentos de línea sobre el "
+"tablero cubiertos por dicho color, más una bonificación de 10 puntos si el "
+"color fue capaz de colocar todas las piezas."
+
+#. (itstool) path: sect1/para
+#: index.docbook:269
+msgid ""
+"Like Blokus, Nexos can be played with two players by having one player play "
+"Blue and Red and the other player Yellow and Green."
+msgstr ""
+"Al igual que en Blokus, Nexos puede jugarse a dos jugadores, con un jugador "
+"a cargo de azules y rojas y el otro jugador a cargo de amarillas y verdes."
+
+#. (itstool) path: chapter/title
+#: index.docbook:277
+msgid "GembloQ Rules"
+msgstr "Reglas de GembloQ"
+
+#. (itstool) path: chapter/para
+#: index.docbook:278
+msgid ""
+"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."
+msgstr ""
+"GembloQ es un juego de mesa parecido a Blokus. Las casillas del tablero "
+"están inclinadas 45 grados. El tablero tiene un tamaño en diagonal de 27 "
+"casillas. Además de las casillas completas con cuatro lados, a lo largo de "
+"los laterales hay también casillas cortadas por la mitad de tal manera que "
+"los laterales del tablero sean líneas rectas."
+
+#. (itstool) path: chapter/para
+#: index.docbook:292
+msgid ""
+"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."
+msgstr ""
+"Cada jugador tiene un conjunto de 21 piezas, que incluye las piezas usadas "
+"en Blokus, pero que además tiene algunas piezas que cuentan solo con medio "
+"cuadrado."
+
+#. (itstool) path: caption/para
+#: index.docbook:302
+msgid ""
+"The board for GembloQ with the starting points marked with colored dots"
+msgstr ""
+"El tablero para GembloQ con los puntos de inicio marcados con puntos de "
+"colores"
+
+#. (itstool) path: chapter/para
+#: index.docbook:308
+msgid ""
+"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."
+msgstr ""
+"Tal y como sucede en Blokus, las casillas de inicio se encuentran en las "
+"esquinas. El primer movimiento debe cubrir por completo la casilla de inicio"
+" del color correspondiente y todos los movimientos posteriores deben tocar "
+"una pieza del color del jugador en uno de sus vértices, pero no esquina con "
+"esquina. Hay algunos movimientos que también se consideran legales, si un "
+"vértice de un medio cuadrado toca el lateral de una pieza del mismo color."
+
+#. (itstool) path: chapter/para
+#: index.docbook:323
+msgid ""
+"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."
+msgstr ""
+"La puntuación se obtiene de la misma manera que en Blokus; los medios "
+"cuadrados cuentan como 0,5 puntos. El desempate mediante la asignación de "
+"valores adicionales a las piezas, tal y como se describe en las reglas "
+"oficiales de GembloQ, no es una opción compatible actualmente con Pentobi."
+
+#. (itstool) path: sect1/title
+#: index.docbook:329 index.docbook:392
+msgid "Rules for Two and Three Players"
+msgstr "Reglas para dos y tres jugadores"
+
+#. (itstool) path: sect1/para
+#: index.docbook:330
+msgid ""
+"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."
+msgstr ""
+"Las variantes de juego para dos y tres jugadores utilizan tableros más "
+"pequeños y diferentes colocaciones de los puntos de inicio. Además de la "
+"variante de dos jugadores habitual, Pentobi también ofrece una variante de "
+"juego para dos jugadores como en Blokus, donde cada jugador juega con dos "
+"colores. "
+
+#. (itstool) path: chapter/title
+#: index.docbook:340
+msgid "Callisto Rules"
+msgstr "Reglas de Calisto"
+
+#. (itstool) path: chapter/para
+#: index.docbook:341
+msgid ""
+"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."
+msgstr ""
+"Calisto es otro juego de mesa parecido a Blokus. El tablero es una "
+"derivación del clásico 20x20 de Blokus, solo que eliminando las esquinas "
+"para conseguir un octógono con un lateral superior de seis casillas "
+"restantes. Las piezas son un grupo de poliominós de un máximo de cinco "
+"cuadrados. Entre ellas, hay tres piezas de 1x1 para cada jugador que "
+"desempeñan una función especial."
+
+#. (itstool) path: chapter/para
+#: index.docbook:356
+msgid ""
+"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."
+msgstr ""
+"Las piezas de 1x1 pueden colocarse en cualquier lugar sobre el tablero a "
+"partir del centro del mismo. El centro consiste en un octógono con una "
+"anchura de seis casillas y un lateral superior de dos casillas. Para los dos"
+" primeros movimientos de un jugador se deben usar dos piezas de 1x1; la "
+"tercera pieza de 1x1 puede jugarse posteriormente en cualquier momento."
+
+#. (itstool) path: caption/para
+#: index.docbook:368
+msgid "The board with the center having a darker color"
+msgstr "El tablero con el centro de un color más oscuro"
+
+#. (itstool) path: chapter/para
+#: index.docbook:374
+msgid ""
+"All larger pieces may be placed anywhere on the board but must touch an "
+"existing piece of the same color edge-to-edge."
+msgstr ""
+"El resto de piezas más grandes se pueden colocar en cualquier lugar del "
+"tablero, pero deben tocar por la esquina una pieza ya existente del mismo "
+"color."
+
+#. (itstool) path: chapter/para
+#: index.docbook:386
+msgid ""
+"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."
+msgstr ""
+"La puntuación que recibe un color se corresponde con el número de cuadrados "
+"sobre el tablero que están ocupados por dicho color, sin contar las piezas "
+"de 1x1. No se utilizan puntos de bonificación. A diferencia de Blokus, los "
+"empates se deshacen a favor del jugador que comenzó último."
+
+#. (itstool) path: sect1/para
+#: index.docbook:393
+msgid ""
+"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."
+msgstr ""
+"A esta variante pueden jugar menos de cuatro jugadores utilizando un tablero"
+" más pequeño. Para tres jugadores, el tablero es un octógono con una anchura"
+" de 20 casillas y un lateral superior de 2 casillas. Para dos jugadores, el "
+"tablero es un octógono con una anchura de 17 casillas y un lateral superior "
+"de 2 casillas. Las dimensiones de la zona central siguen siendo las mismas. "
+"Además de la variante normal de dos jugadores, Pentobi también ofrece una "
+"variante de juego para dos jugadores como en Blokus, donde cada jugador "
+"juega con dos colores."
+
+#. (itstool) path: chapter/title
+#: index.docbook:405
+msgid "How to Use Pentobi"
+msgstr "Cómo usar Pentobi"
+
+#. (itstool) path: sect1/title
+#: index.docbook:407
+msgid "Board"
+msgstr "Tablero"
+
+#. (itstool) path: sect1/para
+#: index.docbook:408
+msgid ""
+"Pieces can be selected by clicking on one of the unplayed pieces or by using"
+" <link linkend=\"shortcuts\">shortcut keys</link>. 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."
+msgstr ""
+"La selección de las piezas puede realizarse haciendo clic sobre una de las "
+"que no se hayan jugado todavía o usando <link linkend=\"shortcuts\">teclas "
+"de método abreviado</link>. Se pueden jugar las piezas arrastrándolas a una "
+"posición que se corresponda con un movimiento legal, tanto con el ratón como"
+" con las teclas de dirección, y pulsar el botón izquierdo del ratón o la "
+"tecla Entrar."
+
+#. (itstool) path: sect1/para
+#: index.docbook:414
+msgid ""
+"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)."
+msgstr ""
+"Las piezas jugadas sobre el tablero puede que muestren números que sirven "
+"para indicar el número de movimiento en el que se jugó dicha pieza. Una "
+"letra a continuación del número de movimiento indica que existe una "
+"variación de este movimiento (ver a continuación)."
+
+#. (itstool) path: sect1/para
+#: index.docbook:419
+msgid ""
+"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."
+msgstr ""
+"El indicador de puntuación muestra los puntos actuales de cada color o "
+"jugador. Los puntos son la suma de los puntos sobre el tablero y los puntos "
+"de bonificación. Los puntos aparecen subrayados si se trata de los puntos "
+"finales debido a que dicho color ya no puede colocar más piezas. Una "
+"estrella pequeña indica que los puntos incluyen una bonificación."
+
+#. (itstool) path: sect1/title
+#: index.docbook:427
+msgid "Playing Against the Computer"
+msgstr "Jugar contra la máquina"
+
+#. (itstool) path: sect1/para
+#: index.docbook:428
+msgid ""
+"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."
+msgstr ""
+"El tablero se puede utilizar para crear registros de partidas jugadas por "
+"jugadores humanos o por partidas jugadas contra la máquina. En partidas "
+"contra la máquina, esta puede jugar con alguno (o la mayoría) de los "
+"colores."
+
+#. (itstool) path: sect1/para
+#: index.docbook:433
+msgid ""
+"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 "
+"<guimenuitem>Settings</guimenuitem> from the <guimenu>Computer</guimenu> "
+"menu or toolbar and select the colors the computer should play."
+msgstr ""
+"Cuando comienza una nueva partida, el jugador humano jugará por defecto con "
+"el color o los colores del primer jugador y la máquina jugará con el resto "
+"de colores. Para poder cambiar esto, puede ir a "
+"<guimenuitem>Ajustes</guimenuitem> en la opción de menú de la "
+"<guimenu>Máquina</guimenu> o desde la barra de herramientas y seleccionar "
+"los colores con los que quiere que juegue la máquina."
+
+#. (itstool) path: sect1/para
+#: index.docbook:439
+msgid ""
+"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."
+msgstr ""
+"Existe una excepción en la que no se asignará un color predeterminado a la "
+"máquina cuando esta haya jugado sin un color asignado en la partida "
+"anterior. Esto impide que la máquina pueda comenzar a jugar de forma "
+"automática si el usuario quiere básicamente utilizar el tablero para "
+"introducir secuencias de movimientos o realizar tareas de edición similares."
+" Después de cargar una partida guardada, la máquina tampoco tendrá un color "
+"predeterminado."
+
+#. (itstool) path: sect1/para
+#: index.docbook:446
+msgid ""
+"Selecting <guimenuitem>Play</guimenuitem> from the "
+"<guimenu>Computer</guimenu> 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."
+msgstr ""
+"Al seleccionar <guimenuitem>Jugar</guimenuitem> desde la opción de menú de "
+"<guimenu>Máquina</guimenu> o desde la barra de herramientas la máquina "
+"siempre realiza un movimiento para el color actual. Si la máquina no hubiera"
+" jugado con este color antes, de esta manera la máquina será capaz de jugar "
+"con este color (y solo con este color) de ahora en adelante."
+
+#. (itstool) path: sect1/title
+#: index.docbook:455
+msgid "Move Variations and the Game Tree"
+msgstr "Más variaciones y el árbol de la partida"
+
+#. (itstool) path: sect1/para
+#: index.docbook:456
+msgid ""
+"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 <guimenu>Go</guimenu> menu and the navigation "
+"buttons."
+msgstr ""
+"Cuando juegue una partida, Pentobi guardará la secuencia de movimientos de "
+"tal manera que siempre es posible regresar a una posición anterior y "
+"realizar una jugada distinta. En caso de hacerlo así, la nueva secuencia se "
+"guarda también como una secuencia alternativa (llamada variación). Los "
+"anotadores también pueden hacer uso de las variaciones para realizar "
+"comentarios sobre partidas ya existentes. Las variaciones pueden existir en "
+"cualquier posición dentro del tablero y pueden contar con sus "
+"correspondientes subvariaciones. La partida puede llegar a convertirse en un"
+" árbol de partida, en el que cada nodo representa una posición de tablero. "
+"Puede navegar a través del árbol de la partida usando las opciones del menú "
+"<guimenu>Ir a</guimenu> y los botones de navegación."
+
+#. (itstool) path: sect1/para
+#: index.docbook:466
+msgid ""
+"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 forward button). The main variation is supposed to represent "
+"the real game played. If you want a side variation to become the main "
+"variation, select <guimenuitem>Make Main Variation</guimenuitem> from the "
+"<guimenu>Edit</guimenu> menu."
+msgstr ""
+"La variación principal es la secuencia de movimientos que comienza en la "
+"posición de inicio y siempre selecciona el primer nodo descendiente en cada "
+"posición (p. ej., al pulsar el botón de adelante). Se supone que la "
+"variación principal representa la partida que se ha jugado en realidad. Si "
+"desea que una variación alternativa se convierta en la variación principal, "
+"seleccione <guimenuitem>Convertir en variación principal</guimenuitem> desde"
+" el menú <guimenu>Editar</guimenu>."
+
+#. (itstool) path: chapter/title
+#: index.docbook:478
+msgid "Become a Stronger Player"
+msgstr "Mejore su juego"
+
+#. (itstool) path: chapter/para
+#: index.docbook:479
+msgid ""
+"Pentobi has functionality that can help you to become a stronger Blokus "
+"player."
+msgstr ""
+"Pentobi dispone de una funcionalidad que le puede ayudar a mejorar como "
+"jugador de Blokus."
+
+#. (itstool) path: sect1/title
+#: index.docbook:483
+msgid "Game Analysis"
+msgstr "Análisis de la partida"
+
+#. (itstool) path: sect1/para
+#: index.docbook:484
+msgid ""
+"A game can be analyzed by selecting <guimenuitem>Analyze Game</guimenuitem> "
+"from the <guimenu>Tools</guimenu> 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."
+msgstr ""
+"Se puede analizar una partida seleccionando <guimenuitem>Analizar "
+"partida</guimenuitem> desde el menú <guimenu>Herramientas</guimenu>. Con "
+"esta acción el jugador de la máquina analizará cada posición de la variación"
+" principal. El resultado se mostrará en una ventana con un diagrama de "
+"puntos de colores."
+
+#. (itstool) path: caption/para
+#: index.docbook:496
+msgid "Analysis of a game of variant Classic (2 players)"
+msgstr "Análisis de una partida de la variante Clásico (2 jugadores)"
+
+#. (itstool) path: sect1/para
+#: index.docbook:500
+msgid ""
+"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."
+msgstr ""
+"Cada uno de los puntos representa una posición dentro de la partida en la "
+"que el color del punto se jugó. Los puntos están ordenados de manera "
+"horizontal según el número del movimiento. El eje vertical representa una "
+"estimación de la probabilidad de ganar la partida del color en juego. Al "
+"hacer clic con el ratón sobre el diagrama, iremos a la posición "
+"correspondiente."
+
+#. (itstool) path: sect1/para
+#: index.docbook:506
+msgid ""
+"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 <guimenuitem>Play Single Move</guimenuitem> from "
+"the <guimenu>Computer</guimenu> menu."
+msgstr ""
+"Los valores de posición son solo estimaciones y a veces la máquina realizará"
+" estimaciones incorrectas. En cualquier caso, las caídas repentinas del "
+"valor de un movimiento le ayudarán a detectar movimientos que fueron "
+"potencialmente malos. De esta manera, puede regresar a la posición anterior "
+"a dicho movimiento e intentar encontrar un movimiento mejor o preguntar a la"
+" máquina que movimiento hubiera realizado ella seleccionando "
+"<guimenuitem>Jugar un movimiento</guimenuitem> desde el menú "
+"<guimenu>Máquina</guimenu>."
+
+#. (itstool) path: sect1/title
+#: index.docbook:516
+msgid "Determine Your Rating"
+msgstr "Determinar su nivel"
+
+#. (itstool) path: sect1/para
+#: index.docbook:517
+msgid ""
+"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."
+msgstr ""
+"Puede realizar un seguimiento de su progreso jugando partidas contra la "
+"máquina con selección de nivel. Los resultados de la partida se utilizan "
+"para determinar su nivel actual. El nivel es una cifra que representa su "
+"habilidad con el juego."
+
+#. (itstool) path: sect1/para
+#: index.docbook:522
+msgid ""
+"A rated game is started with <guimenuitem>Rated Game</guimenuitem> from the "
+"<guimenu>Game</guimenu> 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."
+msgstr ""
+"Para comenzar una partida con selección de nivel tiene que ir a "
+"<guimenuitem>Partida con selección de nivel</guimenuitem> desde el menú o la"
+" barra de herramientas <guimenu>Partida</guimenu>. Si todavía no ha jugado "
+"ninguna partida con selección de nivel en esta variante de juego, se le "
+"pedirá que elija un valor de inicio, que puede reducir el número de partidas"
+" necesarias para determinar su nivel real. Si es principiante, deje el valor"
+" inicial en 800."
+
+#. (itstool) path: sect1/para
+#: index.docbook:529
+msgid ""
+"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."
+msgstr ""
+"Por cada partida con selección de nivel, la máquina elegirá un nivel de "
+"juego para el rival virtual en consonancia con su nivel actual. El color con"
+" el que jugará se escogerá de manera aleatoria en cada partida."
+
+#. (itstool) path: sect1/para
+#: index.docbook:534
+msgid ""
+"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."
+msgstr ""
+"Durante una partida con selección de nivel, la mayoría de las funciones que "
+"no sean necesarias para jugar, se encontrarán desactivadas: no podrá "
+"deshacer movimientos, navegar por la partida, cambiar los colores del "
+"jugador virtual o cambiar el nivel de juego. Para conseguir una puntuación "
+"precisa, siempre es mejor finalizar las partidas con selección de nivel."
+
+#. (itstool) path: sect1/para
+#: index.docbook:540
+msgid ""
+"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."
+msgstr ""
+"Una vez que la partida ha acabado, su nivel se actualizará en función del "
+"resultado de la partida y del nivel del rival virtual. En cuanto al "
+"resultado de la partida, lo único que importa es si se ha ganado la partida,"
+" perdido o ha habido un empate. El número exacto de puntos obtenidos es algo"
+" que no se tiene en cuenta."
+
+#. (itstool) path: caption/para
+#: index.docbook:552
+msgid "Window with rating graph"
+msgstr "Ventana donde se muestra el gráfico de nivel"
+
+#. (itstool) path: sect1/para
+#: index.docbook:556
+msgid ""
+"You can always see your current rating by selecting "
+"<guimenuitem>Rating</guimenuitem> from the <guimenu>Tools</guimenu> 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."
+msgstr ""
+"En cualquier momento, puede consultar el nivel que tiene seleccionado "
+"<guimenuitem>Nivel</guimenuitem> del menú de "
+"<guimenu>Herramientas</guimenu>. Esto abrirá una ventana donde se muestra la"
+" evolución mediante un gráfico de las últimas 50 partidas. Las últimas 50 "
+"partidas se guardan automáticamente y pueden cargarse abriendo un menú "
+"contextual en la tabla de la partida justo debajo del gráfico."
+
+#. (itstool) path: chapter/title
+#: index.docbook:567
+msgid "Window Menu and Toolbar"
+msgstr "Menú de ventana y barra de herramientas"
+
+#. (itstool) path: sect1/title
+#: index.docbook:569
+msgid "Navigation Buttons in Toolbar"
+msgstr "Botones de navegación en la barra de herramientas"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:572
+msgid "<guibutton>Beginning</guibutton>"
+msgstr "<guibutton>Inicio</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:574
+msgid "Go to the beginning of the game."
+msgstr "Ir al inicio de la partida."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:580
+msgid "<guibutton>Backward 10</guibutton>"
+msgstr "<guibutton>Retroceder 10</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:582
+msgid ""
+"Go ten moves backward in the current variation. The button supports "
+"autorepeat if pressed and held."
+msgstr ""
+"Retroceder diez movimientos dentro de la variación actual. El botón permite "
+"repetición automática si se pulsa y se mantiene pulsado."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:589
+msgid "<guibutton>Backward</guibutton>"
+msgstr "<guibutton>Retroceder</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:591
+msgid ""
+"Go one move backward in the current variation. The button supports "
+"autorepeat if pressed and held."
+msgstr ""
+"Retroceder un movimiento dentro de la variación actual. El botón permite "
+"repetición automática si se pulsa y se mantiene pulsado."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:598
+msgid "<guibutton>Forward</guibutton>"
+msgstr "<guibutton>Adelante</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:600
+msgid ""
+"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."
+msgstr ""
+"Avanzar un movimiento dentro de la variación actual. Si la posición actual "
+"se puede jugar de varias formas posibles (es decir, el nodo actual dentro "
+"del árbol de la partida tiene varios nodos descendientes), será la primera "
+"variación la que se use. El botón permite repetición automática si se pulsa "
+"y se mantiene pulsado."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:609
+msgid "<guibutton>Forward 10</guibutton>"
+msgstr "<guibutton>Avanzar 10</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:611
+msgid ""
+"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."
+msgstr ""
+"Avanzar diez movimientos dentro de la variación actual. Si una posición se "
+"puede jugar de varias formas posibles (es decir, el nodo actual dentro del "
+"árbol de la partida tiene varios nodos descendientes), será la primera "
+"variación la que se use. El botón permite repetición automática si se pulsa "
+"y se mantiene pulsado."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:620
+msgid "<guibutton>End</guibutton>"
+msgstr "<guibutton>Fin</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:622
+msgid ""
+"Go to the end of the current variation. Like <guibutton>Forward</guibutton>,"
+" this also uses the first variation in positions with several follow-up "
+"variations."
+msgstr ""
+"Avanzar hasta el final de la variación actual. Como en la opción "
+"<guibutton>Adelante</guibutton>, esto también usa la primera variación en "
+"posiciones con múltiples variaciones."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:630
+msgid "<guibutton>Next Variation</guibutton>"
+msgstr "<guibutton>Siguiente variación</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:632
+msgid ""
+"Go to the next variation to the last move played (i.e. the next sibling node"
+" of the current node in the game tree)."
+msgstr ""
+"Ir a la siguiente variación del último movimiento realizado (es decir, el "
+"anterior nodo del mismo nivel del nodo actual dentro del árbol de la "
+"partida)."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:639
+msgid "<guibutton>Previous Variation</guibutton>"
+msgstr "<guibutton>Variación anterior</guibutton>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:641
+msgid ""
+"Go to the previous variation to the last move played (i.e. the previous "
+"sibling node of the current node in the game tree)."
+msgstr ""
+"Ir a la variación anterior del último movimiento realizado (es decir, el "
+"anterior nodo del mismo nivel del nodo actual dentro del árbol de la "
+"partida)."
+
+#. (itstool) path: sect1/title
+#: index.docbook:650
+msgid "<guimenu>Game</guimenu> Menu"
+msgstr "Menú <guimenu>Partida</guimenu>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:653
+msgid "<guimenuitem>New</guimenuitem>"
+msgstr "<guimenuitem>Nueva</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:656
+msgid "Start a new game."
+msgstr "Comenzar una partida nueva."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:662
+msgid "<guimenuitem>Rated Game</guimenuitem>"
+msgstr "<guimenuitem>Partida con selección de nivel</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:664
+msgid ""
+"Start a new <link linkend=\"rating\">rated game</link> against the computer."
+msgstr ""
+"Comenzar una <link linkend=\"rating\">partida con selección de nivel</link> "
+"contra la máquina."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:670
+msgid "<guimenuitem>Game Variant</guimenuitem>"
+msgstr "<guimenuitem>Variante de juego</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:672
+msgid "Select a game variant and start a new game of this game variant."
+msgstr ""
+"Seleccionar una variante de juego y comenzar una partida nueva de esta "
+"variante de juego."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:677
+msgid "<guimenuitem>Game Info</guimenuitem>"
+msgstr "<guimenuitem>Información de la partida</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:679
+msgid ""
+"Display or edit additional information about the game like the name of the "
+"players or the date when the game was played."
+msgstr ""
+"Mostrar o editar información adicional de la partida como el número de "
+"jugadores o la fecha en la que se jugó la partida."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:685
+msgid "<guimenuitem>Undo Move</guimenuitem>"
+msgstr "<guimenuitem>Deshacer movimiento</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:687
+msgid ""
+"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 "
+"<guimenu>Edit</guimenu>/<guimenuitem>Truncate</guimenuitem> to remove inner "
+"nodes of the game tree)."
+msgstr ""
+"Deshacer el último movimiento realizado y eliminarlo del árbol de la "
+"partida. Deshacer un movimiento solo es posible si este es el último "
+"movimiento dentro de la variación actual (es decir, una hoja del nodo dentro"
+" del árbol de la partida; usar "
+"<guimenu>Editar</guimenu>/<guimenuitem>Eliminar</guimenuitem> para borrar "
+"todos los nodos internos del árbol de la partida)."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:697
+msgid "<guimenuitem>Find Move</guimenuitem>"
+msgstr "<guimenuitem>Buscar movimiento</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:699
+msgid ""
+"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."
+msgstr ""
+"Buscar un movimiento legal para el color actual y mostrarlo durante unos "
+"segundos sobre el tablero. Al seleccionar esta opción de forma repetida se "
+"mostrarán todos los movimientos legales."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:706
+msgid "<guimenuitem>Open</guimenuitem>"
+msgstr "<guimenuitem>Abrir</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:708
+msgid ""
+"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."
+msgstr ""
+"Cargar una partida guardada. La posición en el tablero después de cargar la "
+"partida será la última posición correspondiente a la variación principal, a "
+"menos que la partida comience con una posición predeterminada. Si la partida"
+" comienza de forma predeterminada, la posición en el tablero pasará a ser la"
+" primera posición. De este manera se evita que se muestren las soluciones de"
+" manera inmediata en caso de que el archivo contenga un rompecabezas de "
+"Blokus como predeterminado con la solución como variación principal."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:718
+msgid "<guimenu>Open Recent</guimenu>"
+msgstr "<guimenu>Abrir reciente</guimenu>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:720
+msgid "Load a recently used game."
+msgstr "Cargar una partida usada recientemente."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:726
+msgid "<guimenuitem>Open Clipboard</guimenuitem>"
+msgstr "<guimenuitem>Abrir portapapeles</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:728
+msgid ""
+"Open a game from a text copied to the clipboard. The text must be a valid "
+"game in Pentobi SGF file format."
+msgstr ""
+"Abrir una partida a partir de un texto copiado en el portapapeles. El texto "
+"debe ser una partida válida en formato de archivo SGF de Pentobi."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:735
+msgid "<guimenuitem>Save</guimenuitem>"
+msgstr "<guimenuitem>Guardar</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:737
+msgid "Save the current game."
+msgstr "Guardar la partida actual."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:743
+msgid "<guimenuitem>Save As</guimenuitem>"
+msgstr "<guimenuitem>Guardar como</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:745
+msgid "Save the current game under a new file name."
+msgstr "Guardar la partida actual con un nuevo nombre de archivo."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:751
+msgid "<guimenu>Export</guimenu>/<guimenuitem>Image</guimenuitem>"
+msgstr "<guimenu>Exportar</guimenu>/<guimenuitem>Imagen</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:753
+msgid ""
+"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."
+msgstr ""
+"Guardar la posición actual como un archivo de imagen. Existen numerosos "
+"formatos de archivo de imagen compatibles; el formato de archivo se puede "
+"averiguar por la extensión del nombre del archivo (p. ej., \".png\" en el "
+"caso del formato PNG). Para conseguir una imagen nítida, la anchura de la "
+"imagen debería ser un múltiplo entero del número de columnas del tablero de "
+"la variante de juego actual."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:762
+msgid "<guimenu>Export</guimenu>/<guimenuitem>ASCII Art</guimenuitem>"
+msgstr "<guimenu>Exportar</guimenu>/<guimenuitem>arte ASCII</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:764
+msgid ""
+"Save the current position as a text diagram. The text diagram should be "
+"viewed using a monospace font."
+msgstr ""
+"Guardar la posición actual como diagrama de texto. El diagrama de texto "
+"debería visualizarse usando una fuente monoespacio."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:771
+msgid "<guimenuitem>Quit</guimenuitem>"
+msgstr "<guimenuitem>Salir</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:773
+msgid "Quit Pentobi."
+msgstr "Salir de Pentobi."
+
+#. (itstool) path: sect1/title
+#: index.docbook:781
+msgid "<guimenu>Go</guimenu> Menu"
+msgstr "Menú <guimenu>Ir a</guimenu>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:784
+msgid "<guimenuitem>Move Number</guimenuitem>"
+msgstr "<guimenuitem>Número de movimiento</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:786
+msgid "Go to the move with a given move number in the current variation."
+msgstr ""
+"Ir al movimiento con un número de movimiento asignado en la variación "
+"actual."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:792
+msgid "<guimenuitem>Main Variation</guimenuitem>"
+msgstr "<guimenuitem>Variación principal</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:794
+msgid ""
+"Go back to the last position in the current variation that belonged to the "
+"main variation."
+msgstr ""
+"Regresar a la última posición dentro de la variación actual correspondiente "
+"a la variación principal."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:801
+msgid "<guimenuitem>Beginning of Branch</guimenuitem>"
+msgstr "<guimenuitem>Inicio de rama</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:803
+msgid ""
+"Go back to the last position in the current variation that had an "
+"alternative move."
+msgstr ""
+"Regresar a la última posición dentro de la variación actual que tenía un "
+"movimiento alternativo."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:810
+msgid "<guimenuitem>Next Comment</guimenuitem>"
+msgstr "<guimenuitem>Siguiente comentario</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:812
+msgid ""
+"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."
+msgstr ""
+"Ir a la siguiente posición que tenga un comentario. Si el campo de texto del"
+" comentario no era visible, ahora se hará visible. Al seleccionar esta "
+"opción repetidamente se mostrarán todas las posiciones con comentarios "
+"dentro del árbol de la partida."
+
+#. (itstool) path: sect1/title
+#: index.docbook:822
+msgid "<guimenu>Edit</guimenu> Menu"
+msgstr "Menú <guimenu>Editar</guimenu>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:825
+msgid "<guimenuitem>Annotation</guimenuitem>"
+msgstr "<guimenuitem>Valoración</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:827
+msgid ""
+"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 <guilabel>Move Marking</guilabel>, on the board."
+msgstr ""
+"Añadir un símbolo de valoración a la manera del ajedrez (p. ej., !!) al "
+"movimiento actual. Los símbolos se encuentran anexos a los números de los "
+"movimientos en la barra de estado y, en función de la configuración de "
+"<guilabel>Marcado de movimientos</guilabel>, en el tablero."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:835
+msgid "<guimenuitem>Make Main Variation</guimenuitem>"
+msgstr "<guimenuitem>Convertir en variación principal</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:837
+msgid ""
+"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."
+msgstr ""
+"Hacer que la variación actual sea la variación principal de la partida. Esto"
+" reordena los nodos dentro del árbol de la partida de tal manera que la "
+"variación actual se convierte en la variación principal."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:845
+msgid "<guimenuitem>Variation Up</guimenuitem>"
+msgstr "<guimenuitem>Subir variación</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:847
+msgid ""
+"Changes the order of variations such that the current position will appear "
+"earlier when iterating over the variations with <guibutton>Next "
+"Variation</guibutton> /<guibutton>Previous Variation</guibutton>."
+msgstr ""
+"Cambia el orden de las variaciones de tal manera que la posición actual "
+"aparecerá más tarde cuando se recorran las variaciones con "
+"<guibutton>Siguiente variación</guibutton>/<guibutton>Variación "
+"anterior</guibutton>."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:856
+msgid "<guimenuitem>Variation Down</guimenuitem>"
+msgstr "<guimenuitem>Bajar variación</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:858
+msgid ""
+"Changes the order of variations such that the current position will appear "
+"later when iterating over the variations with <guibutton>Next "
+"Variation</guibutton> /<guibutton>Previous Variation</guibutton>."
+msgstr ""
+"Cambia el orden de las variaciones de tal manera que la posición actual "
+"aparecerá más tarde cuando se recorran las variaciones con "
+"<guibutton>Siguiente variación</guibutton>/<guibutton>Variación "
+"anterior</guibutton>."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:867
+msgid "<guimenuitem>Delete Variations</guimenuitem>"
+msgstr "<guimenuitem>Eliminar variaciones</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:869
+msgid ""
+"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 "
+"<guimenuitem>Back to Main Variation</guimenuitem>."
+msgstr ""
+"Eliminar todas las variaciones excepto la variación principal. Si la "
+"posición actual no está en la variación principal, primero se cambiará a una"
+" posición como en <guimenuitem>Regresar a la variación "
+"principal</guimenuitem>."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:877
+msgid "<guimenuitem>Truncate</guimenuitem>"
+msgstr "<guimenuitem>Eliminar</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:879
+msgid ""
+"Remove the node with the current position, including any subtree, from the "
+"game tree."
+msgstr ""
+"Eliminar el nodo con la posición actual, incluido cualquier subárbol, del "
+"árbol de la partida."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:886
+msgid "<guimenuitem>Truncate Children</guimenuitem>"
+msgstr "<guimenuitem>Eliminar descendientes</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:888
+msgid ""
+"Remove all child nodes of the node with the current position from the game "
+"tree."
+msgstr ""
+"Eliminar todos los nodos descendientes del nodo con la posición actual del "
+"árbol de la partida."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:895
+msgid "<guimenuitem>Keep Position</guimenuitem>"
+msgstr "<guimenuitem>Conservar posición</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:897
+msgid ""
+"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."
+msgstr ""
+"Elimina todos los movimientos y conserva solo la posición actual como "
+"preestablecida. Esto se puede usar para crear archivos que comiencen con una"
+" posición fija determinada."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:904
+msgid "<guimenuitem>Keep Subtree</guimenuitem>"
+msgstr "<guimenuitem>Conservar árbol</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:906
+msgid ""
+"Like <guimenuitem>Keep Position</guimenuitem> but does not delete the moves "
+"after the current position."
+msgstr ""
+"Como <guimenuitem>Conservar posicion</guimenuitem> pero no elimina los "
+"movimientos posteriores a la posición actual."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:913
+msgid "<guimenuitem>Setup Mode</guimenuitem>"
+msgstr "<guimenuitem>Modo configuración</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:915
+msgid ""
+"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 <guimenuitem>Next Color</guimenuitem> 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."
+msgstr ""
+"Entrar o salir del modo de configuración. En el modo de configuración, las "
+"piezas pueden colocarse en cualquier lugar del tablero, incluso si esto "
+"supone una violación de las reglas del juego. Las piezas existentes se "
+"pueden eliminar del tablero haciendo clic sobre ellas. El color actual "
+"seleccionado también determina el color con el que se jugará una vez "
+"finalizada la configuración. Se puede cambiar el color con "
+"<guimenuitem>Siguiente color</guimenuitem> o haciendo clic sobre el selector"
+" de orientación cuando no haya ninguna pieza seleccionada. El modo de "
+"configuración solo puede usarse si no se ha jugado todavía ningún "
+"movimiento."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:927
+msgid "<guimenuitem>Next Color</guimenuitem>"
+msgstr "<guimenuitem>Siguiente color</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:929
+msgid ""
+"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."
+msgstr ""
+"Elija el siguiente color para seleccionar piezas. Esto puede utilizarse, por"
+" ejemplo, para introducir anotaciones de la partida, como cuántos "
+"movimientos de un color se omitieron debido a que el color se quedó sin "
+"tiempo."
+
+#. (itstool) path: sect1/title
+#: index.docbook:939
+msgid "<guimenu>View</guimenu> Menu"
+msgstr "Menú <guimenu>Vista</guimenu> "
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:941
+msgid "<guimenuitem>Appearance</guimenuitem>"
+msgstr "<guimenuitem>Apariencia</guimenuitem>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:946
+msgid "<guilabel>Coordinates</guilabel>"
+msgstr "<guilabel>Coordenadas</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:948
+msgid ""
+"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."
+msgstr ""
+"Muestra las coordenadas alrededor del tablero para cada una de los campos "
+"del tablero. La convención para utilizar las coordenadas es la misma que en "
+"el archivo con formato SGF de Blokus utilizado por Pentobi."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:956
+msgid "<guilabel>Show variations</guilabel>"
+msgstr "<guilabel>Mostrar variaciones</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:958
+msgid ""
+"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."
+msgstr ""
+"Añade una letra al número del movimiento sobre el tablero si el movimiento "
+"cuenta con variaciones. Si los movimientos están marcados con puntos en "
+"lugar de números, se utilizará un círculo en lugar de un punto para los "
+"movimientos que no estén en la variación principal."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:966
+msgid "<guilabel>Move number</guilabel>"
+msgstr "<guilabel>Número de movimiento</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:968
+msgid ""
+"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."
+msgstr ""
+"Esta opción solo existe en el modo de escritorio y muestra el número de "
+"movimiento, la valoración del movimiento e información de la variación a la "
+"derecha de la barra de estado."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:975
+msgid "<guilabel>Animations</guilabel>"
+msgstr "<guilabel>Animaciones</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:977
+msgid ""
+"Enables or disables the piece rotation, flipping and placement animations."
+msgstr ""
+"Activa o desactiva las animaciones de rotación, volteo y colocación de la "
+"pieza."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:983
+msgid "<guilabel>Color theme</guilabel>"
+msgstr "<guilabel>Color del tema</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:985
+msgid "Changes the colors of the window, board and pieces."
+msgstr "Cambia los colores de la ventana, del tablero y de las piezas."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:991
+msgid "<guilabel>Move marking</guilabel>"
+msgstr "<guilabel>Marcado de movimientos</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:993
+msgid ""
+"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."
+msgstr ""
+"Cambie la forma de marcar los movimientos en el tablero. Las opciones son "
+"marcar el último movimiento jugado con un punto o con un número, mostrar los"
+" números de todos los movimientos o no mostrar ninguna marca."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1001
+msgid "<guilabel>Show comment</guilabel>"
+msgstr "<guilabel>Mostrar comentario</guilabel>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1003
+msgid ""
+"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."
+msgstr ""
+"Esta opción solo existe en el modo de escritorio y sirve para configurar la "
+"visibilidad del área de comentarios cuando la posición cambia. Por defecto, "
+"el área de comentarios solo se mostrará si existe un comentario para la "
+"posición actual."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1015
+msgid "<guimenuitem>Comment</guimenuitem>"
+msgstr "<guimenuitem>Comentario</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1017
+msgid "Toggle the visibility of the comment area in the current position."
+msgstr ""
+"Alternar la visibilidad del área de comentarios en la posición actual."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1023
+msgid "<guimenuitem>Fullscreen</guimenuitem>"
+msgstr "<guimenuitem>Pantalla completa</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1025
+msgid ""
+"Make the main window full screen or leave full screen mode. To leave "
+"fullscreen mode without using the window menu, press the F11 key."
+msgstr ""
+"Hacer que la ventana principal se vea a pantalla completa o abandonar el "
+"modo de pantalla completa. Para abandonar el modo de pantalla completa sin "
+"usar el menú de la ventana, pulse la tecla F11."
+
+#. (itstool) path: sect1/title
+#: index.docbook:1034
+msgid "<guimenu>Computer</guimenu> Menu"
+msgstr "Menú <guimenu>Máquina</guimenu>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1037
+msgid "<guimenuitem>Settings</guimenuitem>"
+msgstr "<guimenuitem>Ajustes</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1039
+msgid ""
+"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."
+msgstr ""
+"Seleccione con qué colores jugará la máquina y la habilidad de juego que "
+"tendrá el oponente virtual. Los niveles más altos son los más difíciles de "
+"ganar pero pueden hacer que la máquina tarde bastante tiempo en hacer una "
+"jugada en ordenadores lentos."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1047
+msgid "<guimenuitem>Play</guimenuitem>"
+msgstr "<guimenuitem>Jugar</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1049
+msgid ""
+"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."
+msgstr ""
+"Hacer que la máquina realice un movimiento para el color actual. Esto puede "
+"usarse para cambiar el color con el que juega la máquina o para seguir "
+"jugando después de haber navegado por el árbol de la partida. Si la máquina "
+"no juega ya con el color actual, ahora jugará con este color (y solo con "
+"este color) durante el resto de la partida."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1058
+msgid "<guimenuitem>Play Move</guimenuitem>"
+msgstr "<guimenuitem>Realizar movimiento</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1060
+msgid ""
+"Make the computer play a single move for the current color without changing "
+"the colors played by the computer."
+msgstr ""
+"Hacer que la máquina realice un único movimiento para el color actual sin "
+"cambiar el color con el que juega la máquina."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1067
+msgid "<guimenuitem>Stop</guimenuitem>"
+msgstr "<guimenuitem>Detener</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1069
+msgid ""
+"Abort the current move generation. You can make the computer continue to "
+"play by selecting <guimenuitem>Play</guimenuitem>."
+msgstr ""
+"Anular la generación del movimiento actual. Puede hacer que la máquina siga "
+"jugando seleccionando <guimenuitem>Jugar</guimenuitem>."
+
+#. (itstool) path: sect1/title
+#: index.docbook:1078
+msgid "<guimenu>Tools</guimenu> Menu"
+msgstr "Menú <guimenu>Herramientas</guimenu>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1081
+msgid "<guimenuitem>Rating</guimenuitem>"
+msgstr "<guimenuitem>Nivel</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1083
+msgid ""
+"Show a dialog window with the <link linkend=\"rating\">rating</link> of the "
+"user in the current game variant."
+msgstr ""
+"Muestra una ventana de diálogo con el<link linkend=\"rating\">nivel</link> "
+"del usuario para la variante de juego actual."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1090
+msgid "<guimenuitem>Clear Rating</guimenuitem>"
+msgstr "<guimenuitem>Borrar nivel</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1092
+msgid ""
+"Deletes the rating information and history for the current game variant."
+msgstr ""
+"Borrar la información sobre el nivel y el historial de la variante de juego "
+"actual."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1098
+msgid "<guimenuitem>Analyze Game</guimenuitem>"
+msgstr "<guimenuitem>Analizar partida</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1100
+msgid "Perform a <link linkend=\"analysis\">game analysis</link>."
+msgstr "Realizar un <link linkend=\"analysis\">análisis de partida</link>."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1106
+msgid "<guimenuitem>Clear Analysis</guimenuitem>"
+msgstr "<guimenuitem>Borrar análisis</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1108
+msgid "Deletes the analysis for the current game."
+msgstr "Borra el análisis de la partida actual."
+
+#. (itstool) path: sect1/title
+#: index.docbook:1116
+msgid "<guimenu>Help</guimenu> Menu"
+msgstr "Menú <guimenu>Ayuda</guimenu>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1119
+msgid "<guimenuitem>Pentobi Help</guimenuitem>"
+msgstr "<guimenuitem>Ayuda para Pentobi</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1121
+msgid "Show a window to browse the Pentobi user manual."
+msgstr "Muestra una ventana para navegar por el manual de usuario de Pentobi."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1127
+msgid "<guimenuitem>About Pentobi</guimenuitem>"
+msgstr "<guimenuitem>Acerca de Pentobi</guimenuitem>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1129
+msgid "Show an info dialog with information about this version of Pentobi."
+msgstr ""
+"Muestra un diálogo de información con detalles sobre esta versión de "
+"Pentobi."
+
+#. (itstool) path: chapter/title
+#: index.docbook:1139
+msgid "Keyboard Shortcuts"
+msgstr "Atajos de teclado"
+
+#. (itstool) path: chapter/para
+#: index.docbook:1140
+msgid ""
+"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."
+msgstr ""
+"Además de las teclas de método abreviado, que se muestran en el menú de "
+"ventana, Pentobi admite los siguientes atajos de teclado. Recuerde que estos"
+" atajos de teclado no se encuentran activos cuando el campo de texto de los "
+"comentarios está habilitado. En este caso, se puede salir del campo de texto"
+" con la tecla Tab."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1148
+msgid "<keysym>Plus</keysym>"
+msgstr "<keysym>Más</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1150
+msgid "Select next piece"
+msgstr "Seleccionar pieza siguiente"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1156
+msgid "<keysym>Minus</keysym>"
+msgstr "<keysym>Menos</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1158
+msgid "Select previous piece"
+msgstr "Seleccionar pieza anterior"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1164
+msgid "<keysym>Escape</keysym>"
+msgstr "<keysym>Escape</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1166
+msgid "Clear selected piece"
+msgstr "Retirar pieza seleccionada"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1172
+msgid "<keysym>Left</keysym>"
+msgstr "<keysym>Izquierda</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1173
+msgid "<keysym>Right</keysym>"
+msgstr "<keysym>Derecha</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1174
+msgid "<keysym>Up</keysym>"
+msgstr "<keysym>Arriba</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1175
+msgid "<keysym>Down</keysym>"
+msgstr "<keysym>Abajo</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1176
+msgid "<keysym>Shift+Left</keysym>"
+msgstr "<keysym>Mayús+Izquierda</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1177
+msgid "<keysym>Shift+Right</keysym>"
+msgstr "<keysym>Mayús+Derecha</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1178
+msgid "<keysym>Shift+Up</keysym>"
+msgstr "<keysym>Mayús+Arriba</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1179
+msgid "<keysym>Shift+Down</keysym>"
+msgstr "<keysym>Mayús+Abajo</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1181
+msgid "Move the selected piece. The Shift key makes the piece move faster."
+msgstr ""
+"Mover la pieza seleccionada. La tecla Mayúsculas hace que la pieza se mueva "
+"más rápido."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1187
+msgid "<keysym>Space</keysym>"
+msgstr "<keysym>Espacio</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1189
+msgid "Next orientation of the selected piece"
+msgstr "Siguiente orientación de la pieza seleccionada"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1195
+msgid "<keysym>Shift+Space</keysym>"
+msgstr "<keysym>Mayús+Espacio</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1197
+msgid "Previous orientation of the selected piece"
+msgstr "Anterior orientación de la pieza seleccionada"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1203
+msgid "<keysym>Enter</keysym>"
+msgstr "<keysym>Entrar</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1205
+msgid "Play the selected piece"
+msgstr "Jugar la pieza seleccionada"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1211
+msgid "<keysym>1</keysym>"
+msgstr "<keysym>1</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1212
+msgid "<keysym>2</keysym>"
+msgstr "<keysym>2</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1213
+msgid "<keysym>A</keysym>"
+msgstr "<keysym>A</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1214
+msgid "<keysym>C</keysym>"
+msgstr "<keysym>C</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1215
+msgid "<keysym>E</keysym>"
+msgstr "<keysym>E</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1216
+msgid "<keysym>F</keysym>"
+msgstr "<keysym>F</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1217
+msgid "<keysym>G</keysym>"
+msgstr "<keysym>G</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1218
+msgid "<keysym>H</keysym>"
+msgstr "<keysym>H</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1219
+msgid "<keysym>I</keysym>"
+msgstr "<keysym>I</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1220
+msgid "<keysym>J</keysym>"
+msgstr "<keysym>J</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1221
+msgid "<keysym>L</keysym>"
+msgstr "<keysym>L</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1222
+msgid "<keysym>N</keysym>"
+msgstr "<keysym>N</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1223
+msgid "<keysym>O</keysym>"
+msgstr "<keysym>O</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1224
+msgid "<keysym>P</keysym>"
+msgstr "<keysym>P</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1225
+msgid "<keysym>S</keysym>"
+msgstr "<keysym>S</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1226
+msgid "<keysym>T</keysym>"
+msgstr "<keysym>T</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1227
+msgid "<keysym>U</keysym>"
+msgstr "<keysym>U</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1228
+msgid "<keysym>V</keysym>"
+msgstr "<keysym>V</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1229
+msgid "<keysym>W</keysym>"
+msgstr "<keysym>W</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1230
+msgid "<keysym>X</keysym>"
+msgstr "<keysym>X</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1231
+msgid "<keysym>Y</keysym>"
+msgstr "<keysym>Y</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1232
+msgid "<keysym>Z</keysym>"
+msgstr "<keysym>Z</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1234
+msgid ""
+"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\")."
+msgstr ""
+"Seleccione una pieza de acuerdo con nombres de pieza utilizados comúnmente. "
+"Si existen varias piezas con una letra, (p. ej., I3, I4, I5), al pulsar "
+"varias veces la tecla hace que cambie de una a otra. Algunas letras solo se "
+"usan en ciertas variantes de juego. Por ejemplo, la A solo se usa en Trigón "
+"para las piezas A6 y A4 (también conocidas como la \"langosta\" y el "
+"\"triángulo\")."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1244
+msgid "<keysym>Ctrl+Home</keysym>"
+msgstr "<keysym>Ctrl+Inicio</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1245
+msgid "<keysym>Ctrl+Shift+Left</keysym>"
+msgstr "<keysym>Ctrl+Mayús+Izquierda</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1246
+msgid "<keysym>Ctrl+Left</keysym>"
+msgstr "<keysym>Ctrl+Izquierda</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1247
+msgid "<keysym>Ctrl+Right</keysym>"
+msgstr "<keysym>Ctrl+Derecha</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1248
+msgid "<keysym>Ctrl+Shift+Right</keysym>"
+msgstr "<keysym>Ctrl+Mayús+Derecha</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1249
+msgid "<keysym>Ctrl+End</keysym>"
+msgstr "<keysym>Ctrl+Fin</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1250
+msgid "<keysym>Ctrl+Up</keysym>"
+msgstr "<keysym>Ctrl+Arriba</keysym>"
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1251
+msgid "<keysym>Ctrl+Down</keysym>"
+msgstr "<keysym>Ctrl+Abajo</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1253
+msgid ""
+"Navigate in the game: beginning, ten moves backward, backward, forward, ten "
+"moves forward, end, previous variation, next variation."
+msgstr ""
+"Navegar durante la partida: inicio, retroceder diez movimientos, retroceder,"
+" avanzar, avanzar diez movimientos, final, variación anterior, siguiente "
+"variación."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1260
+msgid "<keysym>Ctrl+Shift+H</keysym>"
+msgstr "<keysym>Ctrl+Mayús+H</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1262
+msgid ""
+"Like <guimenuitem>Find Move</guimenuitem> (Ctrl+H) but iterates backwards "
+"through the list of legal moves."
+msgstr ""
+"Como <guimenuitem>Buscar movimiento</guimenuitem> (Ctrl+H) pero recorre "
+"hacia atrás la lista de movimientos legales."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1269
+msgid "<keysym>Ctrl+T</keysym>"
+msgstr "<keysym>Ctrl+T</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1271
+msgid "Switch view between comment and game analysis."
+msgstr "Cambiar vista entre comentarios y análisis de la partida."
+
+#. (itstool) path: varlistentry/term
+#: index.docbook:1277
+msgid "<keysym>Alt+M</keysym>"
+msgstr "<keysym>Alt+M</keysym>"
+
+#. (itstool) path: listitem/para
+#: index.docbook:1279
+msgid "Open menu."
+msgstr "Abrir menú."
+
+#. (itstool) path: chapter/title
+#: index.docbook:1288
+msgid "System Requirements"
+msgstr "Requisitos de sistema"
+
+#. (itstool) path: chapter/para
+#: index.docbook:1289
+msgid "Minimum: 1 GB RAM, 1 GHz CPU"
+msgstr "Mínimo: 1 GB RAM, 1 GHz CPU"
+
+#. (itstool) path: chapter/para
+#: index.docbook:1292
+msgid ""
+"Recommended for playing level 9: 4 GB RAM, 2.5 GHz dual-core or faster CPU"
+msgstr ""
+"Recomendado para jugar nivel 9: 4 GB RAM, 2,5 GHz dual-core o CPU más rápida"
+
+#. (itstool) path: chapter/para
+#: index.docbook:1296
+msgid ""
+"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)."
+msgstr ""
+"Pentobi también funcionará en sistemas que no cumplen los requisitos mínimos"
+" pero los niveles de juego más altos se ralentizarán en estos sistemas ( si "
+"la CPU es demasiado lenta) o las opciones de habilidad para jugar se verán "
+"reducidas (si no hay suficiente memoria)."
+
+#. (itstool) path: chapter/title
+#: index.docbook:1304
+msgid "License"
+msgstr "Licencia"
+
+#. (itstool) path: chapter/para
+#: index.docbook:1305
+msgid "Copyright © 2011–2020 Markus Enzenberger"
+msgstr "Copyright © 2011–2020 Markus Enzenberger"
+
+#. (itstool) path: chapter/para
+#: index.docbook:1308
+msgid ""
+"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."
+msgstr ""
+"Este es un programa de software libre: puede distribuirlo o modificarlo de "
+"acuerdo con los términos de la Licencia Pública General de GNU tal y como ha"
+" sido publicada por la Free Software Foundation, tanto en la versión 3 de la"
+" licencia, o (a su elección) cualquier otra versión posterior."
+
+#. (itstool) path: chapter/para
+#: index.docbook:1314
+msgid ""
+"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."
+msgstr ""
+"Este programa se distribuye con la esperanza de que resulte útil, pero SIN "
+"NINGUNA GARANTÍA; ni siquiera cuenta con las garantías implícitas de "
+"COMERCIALIZACIÓN o ADECUACIÓN A UN PROPÓSITO PARTICULAR. Consulte la "
+"Licencia Pública General de GNU para obtener más información."
+
+#. (itstool) path: sect1/title
+#: index.docbook:1321
+msgid "Trademark Disclaimer"
+msgstr "Exención de responsabilidad de la marca comercial"
+
+#. (itstool) path: sect1/para
+#: index.docbook:1322
+msgid ""
+"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."
+msgstr ""
+"La marca comercial Blokus y otras marcas comerciales mencionadas son "
+"propiedad de sus respectivos propietarios. Los propietarios de las marcas "
+"comerciales no están afiliados con el autor del programa Pentobi."
diff --git a/pentobi/icon/pentobi-128.svg b/pentobi/icon/pentobi-128.svg
new file mode 100644 (file)
index 0000000..1969905
--- /dev/null
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="128" height="128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m38 64h52v-26h-26v-26h-44c-4 0-8 4-8 8v18h26z" fill="#e01b24" fill-rule="evenodd"/>
+ <path id="h" d="m12 38 4-4h18l4 4z" fill="#c6161f" fill-rule="evenodd"/>
+ <use transform="translate(26)" width="100%" height="100%" xlink:href="#h"/>
+ <use transform="translate(26,26)" width="100%" height="100%" xlink:href="#h"/>
+ <use transform="translate(52,26)" width="100%" height="100%" xlink:href="#h"/>
+ <path id="n" d="m38 12 4 4h18l4-4z" fill="#e73b43" fill-rule="evenodd"/>
+ <use transform="translate(0,26)" width="100%" height="100%" xlink:href="#n"/>
+ <use transform="translate(26,26)" width="100%" height="100%" xlink:href="#n"/>
+ <path id="m" d="m38 12 4 4v18l-4 4z" fill="#e6323a" fill-rule="evenodd"/>
+ <use transform="translate(0,26)" width="100%" height="100%" xlink:href="#m"/>
+ <use transform="translate(26,26)" width="100%" height="100%" xlink:href="#m"/>
+ <path id="c" d="m38 38-4-4v-18l4-4z" fill="#cd1922" fill-rule="evenodd"/>
+ <use transform="translate(26,26)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="translate(26)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="translate(52,26)" width="100%" height="100%" xlink:href="#c"/>
+ <g fill-rule="evenodd">
+  <path d="m14.503 14.542 1.4971 1.4577v18l-4 4v-18c0.02078-3.241 2.5029-5.4577 2.5029-5.4577z" fill="#e6323a"/>
+  <path d="m14.533 14.527 1.467 1.4727h18l4-4h-18c-3.3465 0.04519-5.467 2.5273-5.467 2.5273z" fill="#e73b42"/>
+  <path d="m64 12h44c3.9627 0.04261 7.9574 4.0799 8 8v70h-26v-52h-26z" fill="#2ec27e"/>
+  <path id="g" d="m64 38 4-4h18l4 4z" fill="#2ab073" fill-rule="evenodd"/>
+ </g>
+ <use transform="translate(26)" width="100%" height="100%" xlink:href="#g"/>
+ <use transform="translate(26,26)" width="100%" height="100%" xlink:href="#g"/>
+ <use transform="translate(26,52)" width="100%" height="100%" xlink:href="#g"/>
+ <path id="f" d="m64 12 4 4v18l-4 4z" fill="#30ca85" fill-rule="evenodd"/>
+ <use transform="translate(26)" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(26,52)" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(26,26)" width="100%" height="100%" xlink:href="#f"/>
+ <path id="p" d="m64 12 4 4h18l4-4z" fill="#4fd398" fill-rule="evenodd"/>
+ <use transform="translate(26,26)" width="100%" height="100%" xlink:href="#p"/>
+ <use transform="translate(26,52)" width="100%" height="100%" xlink:href="#p"/>
+ <path id="o" d="m90 12-4 4v18l4 4z" fill="#2fbb7c" fill-rule="evenodd"/>
+ <use transform="translate(26,26)" width="100%" height="100%" xlink:href="#o"/>
+ <use transform="translate(26,52)" width="100%" height="100%" xlink:href="#o"/>
+ <g fill-rule="evenodd">
+  <path d="m90 12 4 4h18l1.4274-1.4594s-2.4274-2.5406-5.4274-2.5406z" fill="#4fd398"/>
+  <path d="m116 20c-0.0151-2.9828-2.5466-5.4534-2.5466-5.4534l-1.4534 1.4534v18l4 4z" fill="#2fbb7c"/>
+  <path d="m12 38v70c0.0213 4.0053 4 7.9997 7.984 7.9997l44.015-0.00169 9.99e-4 -25.998h-26v-52z" fill="#f6d32d"/>
+  <path id="j" d="m38 64h-26l4-4h18z" fill="#e1bf09" fill-rule="evenodd"/>
+ </g>
+ <use transform="translate(0,26)" width="100%" height="100%" xlink:href="#j"/>
+ <use transform="translate(26,52)" width="100%" height="100%" xlink:href="#j"/>
+ <path id="a" d="m38 38h-26l4 4h18z" fill="#f9e263" fill-rule="evenodd"/>
+ <use transform="translate(0,26)" width="100%" height="100%" xlink:href="#a"/>
+ <use transform="translate(26,52)" width="100%" height="100%" xlink:href="#a"/>
+ <path id="i" d="m12 64v-26l4 4v18z" fill="#f7da51" fill-rule="evenodd"/>
+ <use transform="translate(0,26)" width="100%" height="100%" xlink:href="#i"/>
+ <use transform="translate(26,52)" width="100%" height="100%" xlink:href="#i"/>
+ <path id="b" d="m38 38v26l-4-4v-18z" fill="#efc70b" fill-rule="evenodd"/>
+ <use transform="translate(0,26)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(26,52)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(0,52)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(0,52)" width="100%" height="100%" xlink:href="#a"/>
+ <g fill-rule="evenodd">
+  <path d="m12 108v-18l4 4v18l-1.4487 1.4487s-2.5087-1.9547-2.5513-5.4487z" fill="#f7da51"/>
+  <path d="m38 116h-18c-3.1531 0.0426-5.4487-2.5513-5.4487-2.5513l1.4487-1.4487h18z" fill="#e0bb0a"/>
+  <path d="m38 64h52v26h26v17.931c0 4.1332-3.9521 8.1118-8 8.0692h-44v-26h-26z" fill="#1c71d8"/>
+  <path id="l" d="m38 90 4-4h18l4 4z" fill="#1a68c7" fill-rule="evenodd"/>
+ </g>
+ <use transform="translate(26)" width="100%" height="100%" xlink:href="#l"/>
+ <use transform="translate(25.999,25.998)" width="100%" height="100%" xlink:href="#l"/>
+ <path d="m89.999 116h18c3.1531 0.0426 5.4487-2.5513 5.4487-2.5513l-1.4487-1.4487h-18z" fill="#1a68c7" fill-rule="evenodd"/>
+ <path id="e" d="m38 64 4 4h18l4-4z" fill="#3585e5" fill-rule="evenodd"/>
+ <use transform="translate(26)" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(26,26)" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(52,26)" width="100%" height="100%" xlink:href="#e"/>
+ <path id="d" d="m38 90 4-4v-18l-4-4z" fill="#2078e2" fill-rule="evenodd"/>
+ <use transform="translate(26)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(26,26)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(51.999 25.998)" width="100%" height="100%" xlink:href="#d"/>
+ <path id="k" d="m64 90-4-4v-18l4-4z" fill="#1b6bce" fill-rule="evenodd"/>
+ <use transform="translate(26)" width="100%" height="100%" xlink:href="#k"/>
+ <use transform="translate(26,26)" width="100%" height="100%" xlink:href="#k"/>
+ <path d="m116 108v-18l-4 4v18l1.4487 1.4487s2.5087-1.9547 2.5513-5.4487z" fill="#1b6bce" fill-rule="evenodd"/>
+</svg>
diff --git a/pentobi/icon/pentobi-48.svg b/pentobi/icon/pentobi-48.svg
new file mode 100644 (file)
index 0000000..11bf815
--- /dev/null
@@ -0,0 +1,77 @@
+<?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="m14 24h20v-9.9999h-10v-9.9999h-16.923c-1.5385 0-3.0769 1.5384-3.0769 3.0769v6.923h10z" fill="#e01b24" fill-rule="evenodd" stroke-width=".38461"/>
+ <path id="h" d="m4 14 1.5385-1.5384h6.9231l1.5385 1.5384z" fill="#c6161f" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(10)" width="100%" height="100%" xlink:href="#h"/>
+ <use transform="translate(10 9.9999)" width="100%" height="100%" xlink:href="#h"/>
+ <use transform="translate(20 9.9999)" width="100%" height="100%" xlink:href="#h"/>
+ <path id="n" d="m14 4.0001 1.5385 1.5384h6.9231l1.5385-1.5384z" fill="#e73b43" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(0 9.9999)" width="100%" height="100%" xlink:href="#n"/>
+ <use transform="translate(10 9.9999)" width="100%" height="100%" xlink:href="#n"/>
+ <path id="m" d="m14 4.0001 1.5385 1.5384v6.923l-1.5385 1.5384z" fill="#e6323a" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(0 9.9999)" width="100%" height="100%" xlink:href="#m"/>
+ <use transform="translate(10 9.9999)" width="100%" height="100%" xlink:href="#m"/>
+ <path id="c" d="m14 14-1.5385-1.5384v-6.923l1.5385-1.5384z" fill="#cd1922" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(10 9.9999)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="translate(10)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="translate(20 9.9999)" width="100%" height="100%" xlink:href="#c"/>
+ <g fill-rule="evenodd" stroke-width=".38461">
+  <path d="m4.9627 4.9778 0.57581 0.56065v6.923l-1.5385 1.5384v-6.923c0.00799-1.2465 0.96265-2.0991 0.96265-2.0991z" fill="#e6323a"/>
+  <path d="m4.9742 4.972 0.56423 0.56642h6.9231l1.5385-1.5384h-6.9231c-1.2871 0.017381-2.1027 0.97203-2.1027 0.97203z" fill="#e73b42"/>
+  <path d="m24 4.0001h16.923c1.5241 0.016388 3.0605 1.5692 3.0769 3.0769v26.923h-10v-20h-10z" fill="#2ec27e"/>
+  <path id="g" d="m24 14 1.5385-1.5384h6.9231l1.5385 1.5384z" fill="#2ab073" fill-rule="evenodd" stroke-width=".38461"/>
+ </g>
+ <use transform="translate(10)" width="100%" height="100%" xlink:href="#g"/>
+ <use transform="translate(10 9.9999)" width="100%" height="100%" xlink:href="#g"/>
+ <use transform="translate(10 20)" width="100%" height="100%" xlink:href="#g"/>
+ <path id="f" d="m24 4.0001 1.5385 1.5384v6.923l-1.5385 1.5384z" fill="#30ca85" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(10)" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(10 20)" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(10 9.9999)" width="100%" height="100%" xlink:href="#f"/>
+ <path id="p" d="m24 4.0001 1.5385 1.5384h6.9231l1.5385-1.5384z" fill="#4fd398" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(10 9.9999)" width="100%" height="100%" xlink:href="#p"/>
+ <use transform="translate(10 20)" width="100%" height="100%" xlink:href="#p"/>
+ <path id="o" d="m34 4.0001-1.5385 1.5384v6.923l1.5385 1.5384z" fill="#2fbb7c" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(10 9.9999)" width="100%" height="100%" xlink:href="#o"/>
+ <use transform="translate(10 20)" width="100%" height="100%" xlink:href="#o"/>
+ <g transform="matrix(.38462 0 0 .38461 -.61538 -.61523)" fill-rule="evenodd">
+  <path d="m90 12 4 4h18l1.4274-1.4594s-2.4274-2.5406-5.4274-2.5406z" fill="#4fd398"/>
+  <path d="m116 20c-0.0151-2.9828-2.5466-5.4534-2.5466-5.4534l-1.4534 1.4534v18l4 4z" fill="#2fbb7c"/>
+  <path d="m12 38v70c0.0213 4.0053 4 7.9997 7.984 7.9997l44.015-2e-3 9.99e-4 -25.998h-26v-52z" fill="#f6d32d"/>
+  <path id="j" d="m38 64h-26l4-4h18z" fill="#e1bf09" fill-rule="evenodd"/>
+ </g>
+ <use transform="matrix(.38462 0 0 .38461 -.61538 9.3847)" width="100%" height="100%" xlink:href="#j"/>
+ <use transform="matrix(.38462 0 0 .38461 9.3846 19.385)" width="100%" height="100%" xlink:href="#j"/>
+ <path id="a" d="m14 14h-10l1.5385 1.5384h6.9231z" fill="#f9e263" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(0 9.9999)" width="100%" height="100%" xlink:href="#a"/>
+ <use transform="translate(10 20)" width="100%" height="100%" xlink:href="#a"/>
+ <path id="i" d="m4 24v-9.9999l1.5385 1.5384v6.923z" fill="#f7da51" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(0 9.9999)" width="100%" height="100%" xlink:href="#i"/>
+ <use transform="translate(10 20)" width="100%" height="100%" xlink:href="#i"/>
+ <path id="b" d="m14 14v9.9999l-1.5385-1.5384v-6.923z" fill="#efc70b" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(0 9.9999)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(10 20)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(0 20)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(0 20)" width="100%" height="100%" xlink:href="#a"/>
+ <g transform="matrix(.38462 0 0 .38461 -.61538 -.61523)" fill-rule="evenodd">
+  <path d="m12 108v-18l4 4v18l-1.4487 1.4487s-2.5087-1.9547-2.5513-5.4487z" fill="#f7da51"/>
+  <path d="m38 116h-18c-3.1531 0.0426-5.4487-2.5513-5.4487-2.5513l1.4487-1.4487h18z" fill="#e0bb0a"/>
+  <path d="m38 64h52v26h26v17.931c0 4.1332-3.9521 8.1118-8 8.0692h-44v-26h-26z" fill="#1c71d8"/>
+  <path id="l" d="m38 90 4-4h18l4 4z" fill="#1a68c7" fill-rule="evenodd"/>
+ </g>
+ <use transform="matrix(.38462 0 0 .38461 9.3846 -.61523)" width="100%" height="100%" xlink:href="#l"/>
+ <use transform="matrix(.38462 0 0 .38461 9.3842 9.3839)" width="100%" height="100%" xlink:href="#l"/>
+ <path d="m34 44h6.9231c1.2127 0.01638 2.0957-0.98126 2.0957-0.98126l-0.55719-0.55719h-6.9231z" fill="#1a68c7" fill-rule="evenodd" stroke-width=".38461"/>
+ <path id="e" d="m14 24 1.5385 1.5384h6.9231l1.5385-1.5384z" fill="#3585e5" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(10)" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(10 9.9999)" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(20 9.9999)" width="100%" height="100%" xlink:href="#e"/>
+ <path id="d" d="m14 34 1.5385-1.5384v-6.923l-1.5385-1.5384z" fill="#2078e2" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(10)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(10 9.9999)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(20 9.9992)" width="100%" height="100%" xlink:href="#d"/>
+ <path id="k" d="m24 34-1.5385-1.5384v-6.923l1.5385-1.5384z" fill="#1b6bce" fill-rule="evenodd" stroke-width=".38461"/>
+ <use transform="translate(10)" width="100%" height="100%" xlink:href="#k"/>
+ <use transform="translate(10 9.9999)" width="100%" height="100%" xlink:href="#k"/>
+ <path d="m44 40.923v-6.923l-1.5385 1.5384v6.923l0.55719 0.55719s0.96488-0.7518 0.98127-2.0956z" fill="#1b6bce" fill-rule="evenodd" stroke-width=".38461"/>
+</svg>
diff --git a/pentobi/icon/pentobi_icon.qrc b/pentobi/icon/pentobi_icon.qrc
new file mode 100644 (file)
index 0000000..5a56120
--- /dev/null
@@ -0,0 +1,5 @@
+<RCC>
+<qresource prefix="/pentobi_icon">
+<file>pentobi-128.svg</file>
+</qresource>
+</RCC>
diff --git a/pentobi/qml/AboutDialog.qml b/pentobi/qml/AboutDialog.qml
new file mode 100644 (file)
index 0000000..7f15e39
--- /dev/null
@@ -0,0 +1,72 @@
+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-128.svg"
+                sourceSize { width: 64; height: 64 }
+                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(2020)
+                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/pentobi/qml/AnalyzeDialog.qml b/pentobi/qml/AnalyzeDialog.qml
new file mode 100644 (file)
index 0000000..03ea009
--- /dev/null
@@ -0,0 +1,54 @@
+//-----------------------------------------------------------------------------
+/** @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 { }
+    onOpened: comboBox.currentIndex = 0
+    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:")
+            }
+            Pentobi.ComboBox {
+                id: comboBox
+
+                model:
+                    isAndroid ? [ qsTr("Fast"), qsTr("Normal") ]
+                              : [ qsTr("Fast"), qsTr("Normal"), qsTr("Slow") ]
+                Layout.fillWidth: true
+                Layout.preferredWidth: font.pixelSize * 15
+            }
+        }
+    }
+}
diff --git a/pentobi/qml/AnalyzeGame.qml b/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/pentobi/qml/AppearanceDialog.qml b/pentobi/qml/AppearanceDialog.qml
new file mode 100644 (file)
index 0000000..480cbe2
--- /dev/null
@@ -0,0 +1,214 @@
+//-----------------------------------------------------------------------------
+/** @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 !== gameView.showCoordinates
+                || checkBoxShowVariations.checked !== gameModel.showVariations
+                || checkBoxAnimatePieces.checked !== gameView.enableAnimations
+                || checkBoxMoveNumber.checked !== gameView.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 = gameView.showCoordinates
+        checkBoxShowVariations.checked = gameModel.showVariations
+        checkBoxAnimatePieces.checked = gameView.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 (gameView.moveMarking === "last_dot")
+            currentMoveMarkingIndex = 0
+        else if (gameView.moveMarking === "last_number")
+            currentMoveMarkingIndex = 1
+        else if (gameView.moveMarking === "all_number")
+            currentMoveMarkingIndex = 2
+        else if (gameView.moveMarking === "none")
+            currentMoveMarkingIndex = 3
+        else
+            currentMoveMarkingIndex = 0
+        comboBoxMoveMarking.currentIndex = currentMoveMarkingIndex
+        if (isDesktop) {
+            checkBoxMoveNumber.checked = gameView.showMoveNumber
+            if (gameView.commentMode === "always")
+                currentCommentIndex = 0
+            else if (gameView.commentMode === "never")
+                currentCommentIndex = 2
+            else
+                currentCommentIndex = 1
+            comboBoxComment.currentIndex = currentCommentIndex
+        }
+    }
+    onAccepted: {
+        gameView.showCoordinates = checkBoxCoordinates.checked
+        gameModel.showVariations = checkBoxShowVariations.checked
+        gameView.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: gameView.moveMarking = "last_dot"; break
+        case 1: gameView.moveMarking = "last_number"; break
+        case 2: gameView.moveMarking = "all_number"; break
+        case 3: gameView.moveMarking = "none"; break
+        }
+        if (isDesktop) {
+            gameView.showMoveNumber = checkBoxMoveNumber.checked
+            switch (comboBoxComment.currentIndex) {
+            case 0: gameView.commentMode = "always"; break
+            case 1: gameView.commentMode = "as_needed"; break
+            case 2: gameView.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
+
+
+            }
+            Pentobi.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
+
+
+            }
+            Pentobi.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
+
+
+            }
+            Pentobi.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/pentobi/qml/AsciiArtSaveDialog.qml b/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/pentobi/qml/Board.qml b/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/pentobi/qml/BoardContextMenu.qml b/pentobi/qml/BoardContextMenu.qml
new file mode 100644 (file)
index 0000000..c180d1a
--- /dev/null
@@ -0,0 +1,36 @@
+//-----------------------------------------------------------------------------
+/** @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
+
+    relativeWidth: 15
+    onOpened: {
+        var annotation = gameModel.getMoveAnnotation(moveNumber)
+        itemAnnotation.text =
+                annotation === "" ?
+                    qsTr("Move Annotation") :
+                    //: The argument is the annotation symbol for the current move
+                    qsTr("Move Annotation (%1)").arg(annotation)
+    }
+
+    Pentobi.MenuItem {
+        enabled: moveNumber !== gameModel.moveNumber && ! isRated
+        text: qsTr("Go to Move %1").arg(moveNumber)
+        onTriggered: gameModel.gotoMove(moveNumber)
+    }
+    Pentobi.MenuItem {
+        id: itemAnnotation
+
+        onTriggered: {
+            var dialog = moveAnnotationDialog.get()
+            dialog.moveNumber = moveNumber
+            moveAnnotationDialog.open()
+        }
+    }
+}
diff --git a/pentobi/qml/BusyIndicator.qml b/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/pentobi/qml/Button.qml b/pentobi/qml/Button.qml
new file mode 100644 (file)
index 0000000..78b3ba2
--- /dev/null
@@ -0,0 +1,111 @@
+//-----------------------------------------------------------------------------
+/** @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
+
+    // Used instead of attached tooltip because of QTBUG-30801 (tooltip not
+    // shown when the button is disabled).
+    property string toolTipText
+
+    property bool _toolTipHovered
+    property bool _effectiveHovered: isDesktop && _toolTipHovered && enabled
+    property bool _inhibitToolTip
+
+    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: gameView.animationDurationFast }
+    }
+
+    opacity: root.enabled ? 0.55 : 0.25
+    hoverEnabled: false
+    display: AbstractButton.IconOnly
+    icon {
+        color: theme.colorText
+        width: getIconSize()
+        height: getIconSize()
+    }
+    focusPolicy: Qt.NoFocus
+    flat: true
+    background: Item {
+        id: backgroundItem
+
+        function startClickedAnimation() { pressedAnimation.restart() }
+
+        Rectangle {
+            id: pressedBackground
+
+            anchors.fill: parent
+            radius: 0.05 * width
+            color: theme.colorButtonPressed
+            opacity: down ? 1 : 0
+        }
+        Rectangle {
+            anchors.fill: parent
+            radius: 0.05 * width
+            color: _effectiveHovered  && ! down ? theme.colorButtonHovered
+                                                : "transparent"
+            border.color: _effectiveHovered ? theme.colorButtonBorder
+                                            : "transparent"
+        }
+        NumberAnimation {
+            id: pressedAnimation
+
+            target: pressedBackground
+            property: "opacity"
+            from: 1; to: 0
+            duration: isAndroid ? 300 : 0
+            easing.type: Easing.InQuad
+        }
+    }
+    onPressed: _inhibitToolTip = true
+    onClicked: backgroundItem.startClickedAnimation()
+
+    MouseArea {
+        id: toolTipArea
+
+        parent: root.parent
+        x: root.x
+        y: root.y
+        width: root.width
+        height: root.height
+        visible: isDesktop
+        acceptedButtons: Qt.NoButton
+        hoverEnabled: true
+        onExited: root._inhibitToolTip = false
+        ToolTip.text: root.toolTipText
+        ToolTip.visible: containsMouse && ToolTip.text
+                         && ! root._inhibitToolTip
+        ToolTip.delay: 1000
+        ToolTip.timeout: 7000
+        Component.onCompleted:
+            root._toolTipHovered = Qt.binding(function() {
+                return containsMouse })
+    }
+}
diff --git a/pentobi/qml/ButtonApply.qml b/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/pentobi/qml/ButtonCancel.qml b/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/pentobi/qml/ButtonClose.qml b/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/pentobi/qml/ButtonOk.qml b/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/pentobi/qml/ComboBox.qml b/pentobi/qml/ComboBox.qml
new file mode 100644 (file)
index 0000000..01c0c82
--- /dev/null
@@ -0,0 +1,41 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ComboBox.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import QtQuick.Controls 2.4
+
+/** Custom ComboBox.
+    Explicitly set text color to work around QTBUG-74678 (ComboBox with default
+    style uses white text on light background for highlighted items, last
+    tested with Qt 5.12.2 and 5.13.0-beta1). Using custom colors also ensures
+    that popup menu uses the same colors as our custom MenuItem. */
+ComboBox {
+    id: root
+
+    delegate: ItemDelegate {
+        width: root.width
+        text: modelData
+        highlighted: ListView.isCurrentItem
+        contentItem: Label {
+            text: parent.text
+            // See comment at our custom MenuItem.contentItem/background
+            color: {
+                if (parent.highlighted)
+                    return isDesktop ? palette.highlightedText
+                                     : palette.buttonText
+                return palette.text
+            }
+        }
+        background: Rectangle {
+            // See comment at our custom MenuItem.contentItem/background
+            color: {
+                if (! parent.highlighted)
+                    return "transparent"
+                return isDesktop ? palette.highlight : palette.midlight
+            }
+        }
+    }
+}
diff --git a/pentobi/qml/Comment.qml b/pentobi/qml/Comment.qml
new file mode 100644 (file)
index 0000000..5d5c4d1
--- /dev/null
@@ -0,0 +1,42 @@
+//-----------------------------------------------------------------------------
+/** @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
+
+Rectangle {
+    function dropFocus() { textArea.focus = false }
+
+    color: theme.colorCommentBase
+    radius: 2
+    border.color:
+        textArea.activeFocus ? theme.colorCommentFocus
+                             : theme.colorCommentBorder
+
+    ScrollView {
+        anchors.fill: parent
+        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
+            Keys.onPressed:
+                if (event.key === Qt.Key_Tab) {
+                    focus = false
+                    event.accepted = true
+                }
+        }
+    }
+}
diff --git a/pentobi/qml/ComputerDialog.qml b/pentobi/qml/ComputerDialog.qml
new file mode 100644 (file)
index 0000000..989932f
--- /dev/null
@@ -0,0 +1,178 @@
+//-----------------------------------------------------------------------------
+/** @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: gameView.color0[0]
+                        }
+                        Rectangle {
+                            visible: gameModel.nuColors === 4
+                                     && gameModel.nuPlayers === 2
+                            width: font.pixelSize; height: width
+                            radius: width / 2
+                            anchors.verticalCenter: parent.verticalCenter
+                            color: gameView.color2[0]
+                        }
+                        CheckBox {
+                            id: checkBox0
+
+                            enabled: ! isRated
+                            text:
+                                Logic.getPlayerString(gameModel.gameVariant, 0)
+                            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: gameView.color1[0]
+                        }
+                        Rectangle {
+                            visible: gameModel.nuColors === 4
+                                     && gameModel.nuPlayers === 2
+                            width: font.pixelSize; height: width
+                            radius: width / 2
+                            anchors.verticalCenter: parent.verticalCenter
+                            color: gameView.color3[0]
+                        }
+                        CheckBox {
+                            id: checkBox1
+
+                            enabled: ! isRated
+                            text:
+                                Logic.getPlayerString(gameModel.gameVariant, 1)
+                            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: gameView.color3[0]
+                        }
+                        CheckBox {
+                            id: checkBox3
+
+                            enabled: ! isRated
+                            text:
+                                Logic.getPlayerString(gameModel.gameVariant, 3)
+                        }
+                    }
+                    Row {
+                        visible: gameModel.nuPlayers > 2
+                        Layout.fillWidth: true
+
+                        Rectangle {
+                            width: font.pixelSize; height: width
+                            radius: width / 2
+                            anchors.verticalCenter: parent.verticalCenter
+                            color: gameView.color2[0]
+                        }
+                        CheckBox {
+                            id: checkBox2
+
+                            enabled: ! isRated
+                            text:
+                                Logic.getPlayerString(gameModel.gameVariant, 2)
+                        }
+                    }
+                }
+            }
+            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/pentobi/qml/Dialog.qml b/pentobi/qml/Dialog.qml
new file mode 100644 (file)
index 0000000..4c86ee9
--- /dev/null
@@ -0,0 +1,82 @@
+//-----------------------------------------------------------------------------
+/** @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 90% window width on mobile devices within reason
+        isDesktop ? 0 : Math.min(27 * font.pixelSize, 0.9 * 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
+    }
+
+    // We make all dialogs modal even if they wouldn't need to be because
+    // QtQuickControls2 dialogs are not windows but immovable popup items, so
+    // they inevitably cover parts of the parent window, such that the parent
+    // window is not fully usable anyway.
+    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/pentobi/qml/DialogButtonBox.qml b/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/pentobi/qml/DialogButtonBoxOkCancel.qml b/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/pentobi/qml/DialogLoader.qml b/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/pentobi/qml/ExportImageDialog.qml b/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/pentobi/qml/FatalMessage.qml b/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/pentobi/qml/FileDialog.qml b/pentobi/qml/FileDialog.qml
new file mode 100644 (file)
index 0000000..81b994f
--- /dev/null
@@ -0,0 +1,298 @@
+//-----------------------------------------------------------------------------
+/** @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
+                onVisibleChanged: if (isAndroid) focus = false
+            }
+            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: ItemDelegate {
+                        width: view.width
+                        height: 2 * font.pixelSize
+                        focusPolicy: Qt.NoFocus
+                        highlighted: ListView.isCurrentItem
+                        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: highlighted ?
+                                           // 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 { }
+
+                    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 = ""
+                        }
+                    }
+                }
+            }
+            Pentobi.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/pentobi/qml/GameInfoDialog.qml b/pentobi/qml/GameInfoDialog.qml
new file mode 100644 (file)
index 0000000..cbf86d6
--- /dev/null
@@ -0,0 +1,146 @@
+//-----------------------------------------------------------------------------
+/** @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
+                // Remove focus in case dialog was used before. It might be a
+                // different game now, which makes keeping the last focus
+                // meaningless. Also, it would automatically open the virtual
+                // keaboard on Android.
+                onVisibleChanged: focus = false
+            }
+            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
+                onVisibleChanged: focus = false
+            }
+            Label {
+                visible: textFieldPlayerName2.visible
+                text: qsTr("Player Red:")
+            }
+            TextField {
+                id: textFieldPlayerName2
+
+                visible: gameModel.nuPlayers > 2
+                selectByMouse: true
+                Layout.fillWidth: true
+                onVisibleChanged: focus = false
+            }
+            Label {
+                visible: textFieldPlayerName3.visible
+                text: qsTr("Player Green:")
+            }
+            TextField {
+                id: textFieldPlayerName3
+
+                visible: gameModel.nuPlayers > 3
+                selectByMouse: true
+                Layout.fillWidth: true
+                onVisibleChanged: focus = false
+            }
+            Label { text: qsTr("Date:") }
+            TextField {
+                id: textFieldDate
+
+                selectByMouse: true
+                Layout.fillWidth: true
+                onVisibleChanged: focus = false
+            }
+            Label { text: qsTr("Time:") }
+            TextField {
+                id: textFieldTime
+
+                selectByMouse: true
+                Layout.fillWidth: true
+                onVisibleChanged: focus = false
+            }
+            Label { text: qsTr("Event:") }
+            TextField {
+                id: textFieldEvent
+
+                selectByMouse: true
+                Layout.fillWidth: true
+                onVisibleChanged: focus = false
+            }
+            Label { text: qsTr("Round:") }
+            TextField {
+                id: textFieldRound
+
+                selectByMouse: true
+                Layout.fillWidth: true
+                onVisibleChanged: focus = false
+            }
+        }
+    }
+}
diff --git a/pentobi/qml/GameVariantDialog.qml b/pentobi/qml/GameVariantDialog.qml
new file mode 100644 (file)
index 0000000..92bdfd6
--- /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
+
+            Pentobi.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/pentobi/qml/GameView.js b/pentobi/qml/GameView.js
new file mode 100644 (file)
index 0000000..c6dc6e2
--- /dev/null
@@ -0,0 +1,219 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameView.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(gameView, 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
+    gameView.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) {
+        gameView.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/pentobi/qml/GameViewDesktop.qml b/pentobi/qml/GameViewDesktop.qml
new file mode 100644 (file)
index 0000000..8b8fccb
--- /dev/null
@@ -0,0 +1,354 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameViewDesktop.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 "GameView.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.95 * width, 0.95 * 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 onPositionChanged() {
+        if (analyzeGameModel.elements.length > 0
+                || analyzeGameModel.isRunning)
+            comment.visible = false
+        else
+            _updateCommentVisible()
+    }
+    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: "GameViewDesktop"
+    }
+    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()
+    }
+}
diff --git a/pentobi/qml/GameViewMobile.qml b/pentobi/qml/GameViewMobile.qml
new file mode 100644 (file)
index 0000000..6b1c057
--- /dev/null
@@ -0,0 +1,272 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameViewMobile.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 "GameView.js" as Logic
+
+/** Game view optimized for mobile devices.
+    Landscape orientation is still experimental because of bugs in Qt (with
+    Qt 5.12, there is sometimes a 1-pixel wide white line visible after
+    rotating the screen. So far, this bug has only occured in the Android
+    emulator.) To enable support for landscape, remove
+    android:screenOrientation="portrait" from AndroidManifest.xml. */
+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.95 * width, 0.95 * 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
+    property bool isPortrait: width <= height
+
+    signal play(var pieceModel, point gameCoord)
+
+    function createPieces() { Logic.createPieces() }
+    function destroyPieces() { Logic.destroyPieces() }
+    function findPiece(pieceModel) { return Logic.findPiece(pieceModel) }
+    function onPositionChanged() { }
+    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: "GameViewMobile"
+    }
+    Board {
+        id: board
+
+        x: isPortrait ? (parent.width - width) / 2
+                      : Math.max(
+                            (parent.width - 2 * board.width
+                             - 0.02 * board.width) / 2,
+                            0)
+        width: isPortrait ? Math.min(parent.width, 0.7 * parent.height)
+                          : Math.min(parent.width / 2, parent.height)
+        height: isPortrait ? width : parent.height
+        onClicked: Logic.onBoardClicked(pos)
+        onRightClicked: Logic.onBoardRightClicked(pos)
+
+        Loader {
+            id: boardContextMenu
+
+            Component {
+                id: boardContextMenuComponent
+
+                BoardContextMenu { }
+            }
+        }
+    }
+    SwipeView {
+        id: swipeView
+
+        x: isPortrait ? (parent.width - board.width) / 2
+                      : board.x + board.width + 0.02 * board.width
+        y: isPortrait ? board.height + 0.01 * board.width : 0
+        width: isPortrait ? board.width
+                          : Math.min(board.width, parent.width - x)
+        height: parent.height - y
+        clip: true
+
+        Column {
+            id: columnPieces
+
+            spacing: 2
+
+            ScoreDisplay {
+                id: scoreDisplay
+
+                width: swipeView.width
+                height: 0.06 * swipeView.width
+            }
+            PieceSelectorMobile {
+                id: pieceSelector
+
+                property real elementSize:
+                    // Show at least 3 rows
+                    Math.min(parent.width / columns, height / 3)
+
+                columns: pieces0 && pieces0.length <= 21 ? 7 : 8
+                x: isPortrait ? (parent.width - width) / 2 : 0
+                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
+                }
+                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: swipeView.x + (swipeView.width - width) / 2
+        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: swipeView.x + (swipeView.width - width) / 2
+        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/pentobi/qml/GotoMoveDialog.qml b/pentobi/qml/GotoMoveDialog.qml
new file mode 100644 (file)
index 0000000..ea4ee1f
--- /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
+                selectByMouse: true
+                inputMethodHints: Qt.ImhDigitsOnly
+                validator: IntValidator{
+                    bottom: 0
+                    top: gameModel.moveNumber + gameModel.movesLeft
+                }
+                Layout.preferredWidth: font.pixelSize * 5
+                onVisibleChanged: focus = true
+            }
+            Item { Layout.fillWidth: true }
+        }
+    }
+}
diff --git a/pentobi/qml/HelpWindow.qml b/pentobi/qml/HelpWindow.qml
new file mode 100644 (file)
index 0000000..e663131
--- /dev/null
@@ -0,0 +1,70 @@
+//-----------------------------------------------------------------------------
+/** @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
+        onLoadingChanged:
+            if (loadRequest.status === WebView.LoadFailedStatus)
+                loadHtml(loadRequest.errorString + "<br/>" + loadRequest.url)
+    }
+    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/pentobi/qml/ImageSaveDialog.qml b/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/pentobi/qml/InitialRatingDialog.qml b/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/pentobi/qml/LineSegment.qml b/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/pentobi/qml/Main.js b/pentobi/qml/Main.js
new file mode 100644 (file)
index 0000000..33f6503
--- /dev/null
@@ -0,0 +1,824 @@
+//-----------------------------------------------------------------------------
+/** @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
+    }
+    gameView.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
+        gameView.destroyPieces()
+        gameModel.changeGameVariant(gameVariant)
+        gameView.createPieces()
+        gameView.showToPlay()
+        gameView.setupMode = false
+        isRated = false
+        analyzeGameModel.clear()
+        gameView.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 = gameView.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()) {
+        gameView.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()) {
+        gameView.showComment()
+        return
+    }
+    showInfo(qsTr("No comment found"))
+}
+
+function genMove() {
+    cancelRunning()
+    gameView.pickedPiece = null
+    gameView.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)
+        //: Label for modified loaded game. The argument is the file name.
+        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 getPlayerString(variant, player)
+{
+    var isMultiColor = (variant === "classic_2" || variant === "trigon_2"
+                        || variant === "nexos_2" || variant === "callisto_2_4"
+                        || variant === "gembloq_2_4")
+    switch (player) {
+    case 0:
+        if (isMultiColor)
+            return qsTr("Blue/Red");
+        else if (variant === "duo")
+            return qsTr("Purple");
+        else if (variant === "junior")
+            return qsTr("Green");
+        else
+            return qsTr("Blue");
+    case 1:
+        if (isMultiColor)
+            return qsTr("Yellow/Green");
+        else if (variant === "duo" || variant === "junior")
+            return qsTr("Orange");
+        else if (variant === "callisto_2" || variant === "gembloq_2")
+            return qsTr("Green");
+        else
+            return qsTr("Yellow");
+    case 2:
+        return qsTr("Red");
+    default:
+        return qsTr("Green");
+    }
+}
+
+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 !== "de" && lang !== "es")
+        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)
+        analyzeGameModel.loadAutoSave(gameModel)
+    }
+    playerModel.level = syncSettings.valueInt("level", 1)
+    if (isMultiColor()) {
+        computerPlays2 = computerPlays0
+        computerPlays3 = computerPlays1
+    }
+    gameView.createPieces()
+    if (gameModel.checkFileModifiedOutside())
+    {
+        showWindow()
+        showQuestion(qsTr("File has been modified by another application. Reload?"), reloadFile)
+        return
+    }
+    if (analyzeGameModel.elements.length > 0)
+        gameView.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 && ! initialFile)
+        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) {
+    if (move.isNull()) {
+        showInfo(qsTr("Pentobi failed to generate a move."))
+        isPlaySingleMoveRunning = false
+        return
+    }
+    gameModel.playMove(move)
+    if (isPlaySingleMoveRunning)
+        isPlaySingleMoveRunning = false
+    else
+        delayedCheckComputerMove.restart()
+}
+
+function moveUpVar() {
+    gameModel.moveUpVar()
+    showVariationInfo()
+}
+
+function newGame()
+{
+    verify(newGameNoVerify)
+}
+
+function newGameNoVerify()
+{
+    gameModel.newGame()
+    gameView.setupMode = false
+    gameView.showToPlay()
+    gameView.showPieces()
+    isRated = false
+    analyzeGameModel.clear()
+    initComputerColors()
+}
+
+function nextPiece() {
+    var currentPickedPiece = null
+    if (gameView.pickedPiece)
+        currentPickedPiece = gameView.pickedPiece.pieceModel
+    var pieceModel = gameModel.nextPiece(currentPickedPiece)
+    if (pieceModel)
+        gameView.pickPieceAtBoard(gameView.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 = gameView.enableAnimations
+    gameView.enableAnimations = false
+    if (! gameModel.openFile(file))
+        showInfo(qsTr("Open failed.") + "\n" + gameModel.getError())
+    else
+        setComputerNone()
+    if (gameModel.gameVariant !== oldGameVariant)
+        gameView.createPieces()
+    gameView.showToPlay()
+    gameView.enableAnimations = oldEnableAnimations
+    gameView.setupMode = false
+    isRated = false
+    analyzeGameModel.clear()
+    if (gameModel.comment.length > 0)
+        gameView.showComment()
+    else
+        gameView.showPieces()
+}
+
+function openFileUrl() {
+    openFile(getFileFromUrl(openDialog.item.fileUrl))
+}
+
+function openClipboard()
+{
+    verify(openClipboardNoVerify)
+}
+
+function openClipboardNoVerify() {
+    lengthyCommand.run(function() {
+        var oldGameVariant = gameModel.gameVariant
+        var oldEnableAnimations = gameView.enableAnimations
+        gameView.enableAnimations = false
+        if (! gameModel.openClipboard())
+            showInfo(qsTr("Open failed.") + "\n" + gameModel.getError())
+        else
+            setComputerNone()
+        if (gameModel.gameVariant !== oldGameVariant)
+            gameView.createPieces()
+        gameView.showToPlay()
+        gameView.enableAnimations = oldEnableAnimations
+        gameView.setupMode = false
+        isRated = false
+        analyzeGameModel.clear()
+    })
+}
+
+function openRecentFile(file) {
+    if (! checkStoragePermission())
+        return
+    verify(function() { openFile(file) })
+}
+
+function pickNamedPiece(name) {
+    var currentPickedPiece = null
+    if (gameView.pickedPiece)
+        currentPickedPiece = gameView.pickedPiece.pieceModel
+    var pieceModel = gameModel.pickNamedPiece(name, currentPickedPiece)
+    if (pieceModel)
+        gameView.pickPieceAtBoard(gameView.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 (gameView.pickedPiece)
+        currentPickedPiece = gameView.pickedPiece.pieceModel
+    var pieceModel = gameModel.previousPiece(currentPickedPiece)
+    if (pieceModel)
+        gameView.pickPieceAtBoard(gameView.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
+    gameView.setupMode = false
+    gameView.showToPlay()
+    gameView.showPieces()
+    isRated = true
+    analyzeGameModel.clear()
+    delayedCheckComputerMove.restart()
+}
+
+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) {
+    gameView.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/pentobi/qml/Main.qml b/pentobi/qml/Main.qml
new file mode 100644 (file)
index 0000000..21b47fe
--- /dev/null
@@ -0,0 +1,561 @@
+//-----------------------------------------------------------------------------
+/** @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 gameView: gameViewLoader.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"
+    // "system" theme would be a better default on desktop with Fusion style
+    // but we currently don't use Fusion (see comment in Main.cpp about
+    // QTBUG-77107)
+    property string themeName: isAndroid ? "dark" : "light"
+    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,
+                             defaultWidth * 662 / 1164)
+
+    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()
+    Component.onDestruction: Logic.cancelRunning()
+
+    MouseArea {
+        anchors.fill: parent
+        onClicked: gameView.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: gameViewLoader
+
+        anchors {
+            left: parent.left
+            right: parent.right
+            top: toolBar.visible ? toolBar.bottom : parent.top
+            bottom: parent.bottom
+            margins: isDesktop ? 2 : 0
+        }
+        source: isDesktop ? "GameViewDesktop.qml" : "GameViewMobile.qml"
+
+        Connections {
+            target: gameViewLoader.item
+            // This creates a runtime deprecations warning with Qt 5.15.
+            // Convert to new Connections syntax once we increase our minimum
+            // Qt version requirements.
+            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
+        // Changed alias name from themeName to theme to ignore old saved
+        // settings from Pentobi <=17.1 after changing default theme on desktop
+        // (see comment in Main.cpp about QTBUG-77107)
+        property alias theme: 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: {
+            gameView.pickedPiece = null
+            if (gameModel.canGoBackward || gameModel.canGoForward
+                    || gameModel.moveNumber > 0)
+                gameView.setupMode = false
+            analyzeGameModel.markCurrentMove(gameModel)
+            gameView.onPositionChanged()
+            gameView.dropCommentFocus()
+        }
+        onInvalidSgfFile: Logic.showInfo(gameModel.getError())
+    }
+    PlayerModel {
+        id: playerModel
+
+        gameVariant: gameModel.gameVariant
+        onMoveGenerated: Logic.moveGenerated(move)
+        onSearchCallback: gameView.searchCallback(elapsedSeconds, remainingSeconds)
+        onIsGenMoveRunningChanged:
+            if (isGenMoveRunning) gameView.startSearch()
+            else gameView.endSearch()
+        Component.onCompleted:
+            if (notEnoughMemory())
+                Logic.showFatal(qsTr("Not enough memory"))
+    }
+    AnalyzeGameModel {
+        id: analyzeGameModel
+
+        onIsRunningChanged: if (! isRunning) gameView.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
+        }
+    }
+    Timer {
+        id: pressBackTwice
+
+        interval: 2500
+    }
+    Connections {
+        target: Qt.application
+        enabled: isAndroid
+        // This creates a runtime deprecations warning with Qt 5.15.
+        // Convert to new Connections syntax once we increase our minimum
+        // Qt version requirements.
+        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: gameView.isCommentVisible
+        onTriggered:
+            if (isDesktop)
+                gameView.setCommentVisible(checked)
+            else {
+                if (checked)
+                    gameView.showComment()
+                else
+                    gameView.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: gameView.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")
+        // The conditions canGoBackward/Forward are not really needed because
+        // they can only be true if either a file was loaded or the game is
+        // modified, but we add them to make sure that the New Game button is
+        // always enabled when needed even if there is an inconsistency in the
+        // other conditions due to a bug in Pentobi.
+        enabled: gameView.setupMode || gameModel.isModified
+                 || gameModel.file !== "" || isRated || gameModel.canGoBackward
+                 || gameModel.canGoForward
+                 || analyzeGameModel.elements.length !== 0
+        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 && ! gameView.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 if (pressBackTwice.running)
+                close()
+            else {
+                Logic.showTemporaryMessage(qsTr("Press back again to exit"))
+                pressBackTwice.start()
+            }
+        }
+    }
+    Shortcut {
+        sequence: "Return"
+        enabled: ! isAndroid
+        onActivated: gameView.playPickedPiece()
+    }
+    Shortcut {
+        sequence: "Escape"
+        onActivated:
+            if (gameView.pickedPiece)
+                gameView.pickedPiece = null
+            else if (visibility === Window.FullScreen)
+                visibility = Window.AutomaticVisibility
+    }
+    Shortcut {
+        sequence: "Ctrl+Shift+H"
+        enabled: ! gameModel.isGameOver
+        onActivated: gameView.showMove(gameModel.findMovePrevious())
+    }
+    Shortcut {
+        sequence: "Down"
+        onActivated: gameView.shiftPiece(0, 1)
+    }
+    Shortcut {
+        sequence: "Shift+Down"
+        onActivated: gameView.shiftPieceFast(0, 1)
+    }
+    Shortcut {
+        sequence: "Left"
+        onActivated: gameView.shiftPiece(-1, 0)
+    }
+    Shortcut {
+        sequence: "Shift+Left"
+        onActivated: gameView.shiftPieceFast(-1, 0)
+    }
+    Shortcut {
+        sequence: "Right"
+        onActivated: gameView.shiftPiece(1, 0)
+    }
+    Shortcut {
+        sequence: "Shift+Right"
+        onActivated: gameView.shiftPieceFast(1, 0)
+    }
+    Shortcut {
+        sequence: "Up"
+        onActivated: gameView.shiftPiece(0, -1)
+    }
+    Shortcut {
+        sequence: "Shift+Up"
+        onActivated: gameView.shiftPieceFast(0, -1)
+    }
+    Shortcut {
+        enabled: gameView.pickedPiece
+        sequence: "Space"
+        onActivated: gameView.pickedPiece.pieceModel.nextOrientation()
+    }
+    Shortcut {
+        sequence: "+"
+        onActivated: Logic.nextPiece()
+    }
+    Shortcut {
+        sequence: "Alt+M"
+        onActivated: toolBar.clickMenuButton()
+    }
+    Shortcut {
+        enabled: gameView.pickedPiece
+        sequence: "Shift+Space"
+        onActivated: gameView.pickedPiece.pieceModel.previousOrientation()
+    }
+    Shortcut {
+        sequence: "-"
+        onActivated: Logic.prevPiece()
+    }
+}
diff --git a/pentobi/qml/Menu.qml b/pentobi/qml/Menu.qml
new file mode 100644 (file)
index 0000000..15f0dbc
--- /dev/null
@@ -0,0 +1,48 @@
+//-----------------------------------------------------------------------------
+/** @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 "." as Pentobi
+
+Menu {
+    // Width in font-size units
+    property real relativeWidth: 23
+
+    width: Math.min(font.pixelSize * (isDesktop ? relativeWidth : 23),
+                    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.
+        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
+    }
+}
diff --git a/pentobi/qml/MenuComputer.qml b/pentobi/qml/MenuComputer.qml
new file mode 100644 (file)
index 0000000..550481c
--- /dev/null
@@ -0,0 +1,24 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuComputer.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "." as Pentobi
+
+Pentobi.Menu {
+    title: qsTr("Computer")
+
+    Pentobi.MenuItem {
+        action: actionComputerSettings
+    }
+    Pentobi.MenuItem {
+        action: actionPlay
+    }
+    Pentobi.MenuItem {
+        action: actionPlaySingle
+    }
+    Pentobi.MenuItem {
+        action: actionStop
+    }
+}
diff --git a/pentobi/qml/MenuEdit.qml b/pentobi/qml/MenuEdit.qml
new file mode 100644 (file)
index 0000000..af507cd
--- /dev/null
@@ -0,0 +1,95 @@
+//-----------------------------------------------------------------------------
+/** @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: qsTr("Edit")
+
+    Pentobi.MenuItem {
+        text: qsTr("Annotation…")
+        enabled: gameModel.moveNumber > 0
+        onTriggered: {
+            var dialog = moveAnnotationDialog.get()
+            dialog.moveNumber = gameModel.moveNumber
+            moveAnnotationDialog.open()
+        }
+    }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        text: qsTr("Make Main Variation")
+        enabled: ! gameModel.isMainVar && ! isRated
+        onTriggered: {
+            gameModel.makeMainVar()
+            Logic.showTemporaryMessage(qsTr("Made main variation"))
+        }
+    }
+    Pentobi.MenuItem {
+        //: Short for Move Variation Up
+        text: qsTr("Variation Up")
+        enabled: gameModel.hasPrevVar && ! isRated
+        onTriggered: Logic.moveUpVar()
+    }
+    Pentobi.MenuItem {
+        //: Short for Move Variation Down
+        text: qsTr("Variation Down")
+        enabled: gameModel.hasNextVar && ! isRated
+        onTriggered: Logic.moveDownVar()
+    }
+    Pentobi.MenuItem {
+        text: qsTr("Delete Variations")
+        enabled: gameModel.hasVariations && ! isRated
+        onTriggered: Logic.deleteAllVar()
+    }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        text: qsTr("Truncate")
+        enabled: gameModel.canGoBackward && ! isRated
+        onTriggered: Logic.truncate()
+    }
+    Pentobi.MenuItem {
+        text: qsTr("Truncate Children")
+        enabled: gameModel.canGoForward && ! isRated
+        onTriggered: Logic.truncateChildren()
+    }
+    Pentobi.MenuItem {
+        text: qsTr("Keep Position")
+        enabled: ! gameModel.isBoardEmpty && (gameModel.canGoBackward || gameModel.canGoForward) && ! isRated
+        onTriggered: Logic.keepOnlyPosition()
+    }
+    Pentobi.MenuItem {
+        text: qsTr("Keep Subtree")
+        enabled: gameModel.canGoBackward && gameModel.canGoForward && ! isRated
+        onTriggered: Logic.keepOnlySubtree()
+    }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        text: qsTr("Setup Mode")
+        checkable: true
+        enabled: ! gameModel.canGoBackward && ! gameModel.canGoForward
+                 && gameModel.moveNumber === 0 && ! isRated
+        checked: gameView.setupMode
+        onTriggered: {
+            checked = ! gameView.setupMode // Workaround for QTBUG-69401
+            gameView.setupMode = checked
+            if (checked)
+                gameView.showPieces()
+            else {
+                gameView.pickedPiece = null
+                Logic.setComputerNone()
+            }
+        }
+    }
+    Pentobi.MenuItem {
+        text: qsTr("Next Color")
+        enabled: ! isRated
+        onTriggered: {
+            gameView.pickedPiece = null
+            gameModel.nextColor()
+        }
+    }
+}
diff --git a/pentobi/qml/MenuExport.qml b/pentobi/qml/MenuExport.qml
new file mode 100644 (file)
index 0000000..36d1576
--- /dev/null
@@ -0,0 +1,26 @@
+//-----------------------------------------------------------------------------
+/** @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: qsTr("Export")
+
+    Action {
+        text: qsTr("Image…")
+        onTriggered: exportImageDialog.open()
+    }
+    Action {
+        text: qsTr("ASCII Art…")
+        onTriggered: {
+            var dialog = asciiArtSaveDialog.get()
+            dialog.name = gameModel.suggestFileName(folder, "txt")
+            dialog.selectNameFilter(0)
+            dialog.open()
+        }
+    }
+}
diff --git a/pentobi/qml/MenuGame.qml b/pentobi/qml/MenuGame.qml
new file mode 100644 (file)
index 0000000..c285b29
--- /dev/null
@@ -0,0 +1,56 @@
+//-----------------------------------------------------------------------------
+/** @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: qsTr("Game")
+
+    Pentobi.MenuItem {
+        action: actionNew
+    }
+    Pentobi.MenuItem {
+        action: actionNewRated
+    }
+    Pentobi.MenuSeparator { }
+    Action {
+        text: qsTr("Game Variant…")
+        onTriggered: gameVariantDialog.open()
+    }
+    Pentobi.MenuItem {
+        action: actionGameInfo
+    }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        action: actionUndo
+    }
+    Pentobi.MenuItem {
+        action: actionFindMove
+    }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        action: actionOpen
+    }
+    MenuRecentFiles { }
+    Action {
+        text: qsTr("Open Clipboard")
+        onTriggered: Logic.openClipboard()
+    }
+    Pentobi.MenuItem {
+        action: actionSave
+        enabled: actionSave.enabled && gameModel.file !== ""
+    }
+    Pentobi.MenuItem {
+        action: actionSaveAs
+    }
+    MenuExport { relativeWidth: 10 }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        action: actionQuit
+    }
+}
diff --git a/pentobi/qml/MenuGo.qml b/pentobi/qml/MenuGo.qml
new file mode 100644 (file)
index 0000000..c6f3f9b
--- /dev/null
@@ -0,0 +1,25 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuGo.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "." as Pentobi
+
+Pentobi.Menu {
+    title: qsTr("Go")
+
+    Pentobi.MenuItem {
+        action: actionGotoMove
+    }
+    Pentobi.MenuItem {
+        action: actionBackToMainVar
+    }
+    Pentobi.MenuItem {
+        action: actionBeginningOfBranch
+    }
+    Pentobi.MenuSeparator { }
+    Pentobi.MenuItem {
+        action: actionNextComment
+    }
+}
diff --git a/pentobi/qml/MenuHelp.qml b/pentobi/qml/MenuHelp.qml
new file mode 100644 (file)
index 0000000..8cd2ec0
--- /dev/null
@@ -0,0 +1,24 @@
+//-----------------------------------------------------------------------------
+/** @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: qsTr("Help")
+
+    Pentobi.MenuItem {
+        action: actionHelp
+    }
+    Action {
+        text: qsTr("Report Bug")
+        onTriggered: Qt.openUrlExternally("https://github.com/enz/pentobi/issues")
+    }
+    Action {
+        text: qsTr("About Pentobi")
+        onTriggered: aboutDialog.open()
+    }
+}
diff --git a/pentobi/qml/MenuItem.qml b/pentobi/qml/MenuItem.qml
new file mode 100644 (file)
index 0000000..6eac952
--- /dev/null
@@ -0,0 +1,103 @@
+//-----------------------------------------------------------------------------
+/** @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
+
+// Custom menu item that displays shortcuts (MenuItem in Qt 5.11 does not).
+MenuItem {
+    id: root
+
+    property string shortcut: action && action.shortcut ? action.shortcut : ""
+
+    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
+    }
+
+    // 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.
+            // palette.midlight looks similar to the one used in style Default,
+            // but doesn't work in style Fusion on KDE, so we use
+            // palette.highlight there.
+            return globalStyle.toLowerCase() === "fusion" ?
+                        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 globalStyle.toLowerCase() === "fusion" ?
+                                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: 0.1 * font.pixelSize
+        }
+    }
+}
diff --git a/pentobi/qml/MenuRecentFiles.qml b/pentobi/qml/MenuRecentFiles.qml
new file mode 100644 (file)
index 0000000..5ac8789
--- /dev/null
@@ -0,0 +1,88 @@
+//-----------------------------------------------------------------------------
+/** @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: qsTr("Open Recent")
+    enabled: gameModel.recentFiles.length > 0
+    relativeWidth: 23
+
+    function getText(recentFiles, index) {
+        if (index >= recentFiles.length)
+            return ""
+        var text = recentFiles[index]
+        return text.substring(text.lastIndexOf("/") + 1)
+    }
+
+    // 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: qsTr("Clear List")
+        onTriggered: Qt.callLater(function() { // QTBUG-69682
+            gameModel.clearRecentFiles()
+        })
+    }
+}
diff --git a/pentobi/qml/MenuSeparator.qml b/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/pentobi/qml/MenuTools.qml b/pentobi/qml/MenuTools.qml
new file mode 100644 (file)
index 0000000..28f38d3
--- /dev/null
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------------
+/** @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: qsTr("Tools")
+
+    Pentobi.MenuItem {
+        text: qsTr("Rating")
+        onTriggered: Logic.rating()
+    }
+    Action {
+        enabled: ! isRated && ratingModel.numberGames > 0
+        text: qsTr("Clear Rating")
+        onTriggered: Logic.clearRating()
+    }
+    Pentobi.MenuSeparator { }
+    Action {
+        enabled: ! isRated && (gameModel.canGoBackward || gameModel.canGoForward)
+        text: qsTr("Analyze Game…")
+        onTriggered: analyzeDialog.open()
+    }
+    Action {
+        enabled: analyzeGameModel.elements.length !== 0
+        text: qsTr("Clear Analysis")
+        onTriggered:
+            Qt.callLater(function() { // QTBUG-69682
+                analyzeGameModel.clear()
+                gameView.deleteAnalysis()
+            })
+    }
+}
diff --git a/pentobi/qml/MenuView.qml b/pentobi/qml/MenuView.qml
new file mode 100644 (file)
index 0000000..bc06b57
--- /dev/null
@@ -0,0 +1,33 @@
+//-----------------------------------------------------------------------------
+/** @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: qsTr("View")
+
+    Action {
+        text: qsTr("Appearance")
+        onTriggered: appearanceDialog.open()
+    }
+    Pentobi.MenuItem {
+        visible: isDesktop
+        // Invisible menu item still use space in Qt 5.11
+        height: visible ? implicitHeight : 0
+        text: qsTr("Toolbar")
+        checkable: true
+        checked: rootWindow.showToolBar
+        onTriggered: rootWindow.showToolBar = checked
+    }
+    Pentobi.MenuItem {
+        action: actionComment
+    }
+    Pentobi.MenuItem {
+        action: actionFullscreen
+    }
+}
diff --git a/pentobi/qml/MessageDialog.qml b/pentobi/qml/MessageDialog.qml
new file mode 100644 (file)
index 0000000..c9b5e88
--- /dev/null
@@ -0,0 +1,31 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+    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/pentobi/qml/MoveAnnotationDialog.qml b/pentobi/qml/MoveAnnotationDialog.qml
new file mode 100644 (file)
index 0000000..5bb4afd
--- /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) }
+            Pentobi.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/pentobi/qml/NavigationButtons.qml b/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/pentobi/qml/NavigationPanel.qml b/pentobi/qml/NavigationPanel.qml
new file mode 100644 (file)
index 0000000..213f713
--- /dev/null
@@ -0,0 +1,33 @@
+//-----------------------------------------------------------------------------
+/** @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.margins: 1
+        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/pentobi/qml/NewFolderDialog.qml b/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/pentobi/qml/OpenDialog.qml b/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/pentobi/qml/PieceCallisto.qml b/pentobi/qml/PieceCallisto.qml
new file mode 100644 (file)
index 0000000..a88a75e
--- /dev/null
@@ -0,0 +1,351 @@
+//-----------------------------------------------------------------------------
+/** @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
+
+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] & 1
+                color: root.color[0]
+                width: board.gridWidth - square.width
+                height: 0.95 * 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] & 2
+                color: root.color[0]
+                width: 0.95 * 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
+            }
+            // Right-down junction
+            Rectangle {
+                visible: pieceModel.junctionType[index] === 7
+                color: root.color[0]
+                width: board.gridWidth - square.width
+                height: board.gridHeight - square.height
+                x: (modelData.x - pieceModel.center.x + 1) * board.gridWidth
+                   - width / 2
+                y: (modelData.y - pieceModel.center.y + 1) * board.gridHeight
+                   - height / 2
+                antialiasing: true
+            }
+            Square {
+                id: square
+
+                width: 0.95 * board.gridWidth
+                height: 0.95 * board.gridHeight
+                x: (modelData.x - pieceModel.center.x) * board.gridWidth
+                   + (board.gridWidth - width) / 2
+                y: (modelData.y - pieceModel.center.y) * board.gridHeight
+                   + (board.gridHeight - height) / 2
+            }
+        }
+    }
+    Loader {
+        sourceComponent:
+            (moveMarking == "last_dot" && pieceModel.isLastMove) || item ?
+                dotComponent : null
+
+        Component {
+            id: dotComponent
+
+            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: "90"
+
+                PropertyChanges { target: root; rotation: 90 }
+                // See comment in PieceClassic about flipY property change
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "180"
+
+                PropertyChanges { target: root; rotation: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "270"
+
+                PropertyChanges { target: root; rotation: 270 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "flip"
+
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "90flip"
+
+                PropertyChanges { target: root; rotation: 90 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "180flip"
+
+                PropertyChanges { target: root; rotation: 180 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "270flip"
+
+                PropertyChanges { target: root; rotation: 270 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            }
+        ]
+
+        transitions: [
+            Transition {
+                from: ",180flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "90,270flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "180,flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "270,90flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                enabled: enableAnimations
+
+                PieceRotationAnimation { }
+                PieceRotationAnimation { target: flipX; property: "angle" }
+            }
+        ]
+    }
+
+    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 : gameView
+
+                NumberAnimation {
+                    properties: "x,y,scale"
+                    duration: animationDurationMove
+                    easing.type: Easing.InOutSine
+                }
+            }
+            PropertyAction {
+                target: parentUnplayed.parent
+                property: "z"; value: 0
+            }
+        }
+    }
+}
diff --git a/pentobi/qml/PieceClassic.qml b/pentobi/qml/PieceClassic.qml
new file mode 100644 (file)
index 0000000..265a614
--- /dev/null
@@ -0,0 +1,309 @@
+//-----------------------------------------------------------------------------
+/** @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
+        }
+    }
+    Loader {
+        sourceComponent:
+            (moveMarking == "last_dot" && pieceModel.isLastMove) || item ?
+                dotComponent : null
+
+        Component {
+            id: dotComponent
+
+            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: "90"
+
+                PropertyChanges { target: root; rotation: 90 }
+                // flipY is 0 in all states, but we need to make it part of the
+                // property changes because PieceSwitchedFlipAnimation changes
+                // it temporarily and otherwise it is not guaranteed to be set
+                // to 0 again if a state change is triggered while the last
+                // animation is still running, for example by pressing Ctrl-H
+                // (Find Move) in quick succession.
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "180"
+
+                PropertyChanges { target: root; rotation: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "270"
+
+                PropertyChanges { target: root; rotation: 270 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "flip"
+
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "90flip"
+
+                PropertyChanges { target: root; rotation: 90 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "180flip"
+
+                PropertyChanges { target: root; rotation: 180 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "270flip"
+
+                PropertyChanges { target: root; rotation: 270 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            }
+        ]
+
+        transitions: [
+            Transition {
+                from: ",180flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "90,270flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "180,flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "270,90flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                enabled: enableAnimations
+
+                PieceRotationAnimation { }
+                PieceRotationAnimation { target: flipX; property: "angle" }
+            }
+        ]
+    }
+
+    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 : gameView
+
+                    NumberAnimation {
+                        properties: "x,y,scale"
+                        duration: animationDurationMove
+                        easing.type: Easing.InOutSine
+                    }
+                }
+                PropertyAction {
+                    target: parentUnplayed.parent
+                    property: "z"; value: 0
+                }
+            }
+    }
+}
diff --git a/pentobi/qml/PieceGembloQ.qml b/pentobi/qml/PieceGembloQ.qml
new file mode 100644 (file)
index 0000000..d217db3
--- /dev/null
@@ -0,0 +1,311 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceGembloQ.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+
+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[2]
+    property string imageNameBottom:
+        "image://pentobi/quarter-square/" + color[0] + "/" + color[1]
+    // Avoid fractional sizes for square piece elements
+    property real scaleUnplayed:
+        parentUnplayed ? Math.floor(0.08 * 2 * parentUnplayed.width)
+                         / (2 * 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 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) {
+        // Don't use the faster (pieceAngle - imgAngle + 360) % 360 here. The
+        // effective angle can temporarily become negative for all images
+        // during transitions if the rotate backward button is quickly hit
+        // multiple times, and unlike in the other game variants, the opacity
+        // function here returns 0 on negative angles, making the piece
+        // temporarily disappear.
+        var angle = ((pieceAngle - imgAngle) % 360 + 360) % 360
+        if (angle <= 90) return 0
+        if (angle <= 180) return -Math.cos(angle * Math.PI / 180)
+        if (angle <= 270) {
+            var o = -Math.cos(angle * Math.PI / 180)
+            return o + (1 - o) * (-Math.sin(angle * Math.PI / 180))
+        }
+        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
+            }
+        }
+    }
+    Loader {
+        sourceComponent:
+            (moveMarking == "last_dot" && pieceModel.isLastMove) || item ?
+                dotComponent : null
+
+        Component {
+            id: dotComponent
+
+            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: "90"
+
+                PropertyChanges { target: root; rotation: 90 }
+                // See comment in PieceClassic about flipY property change
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "180"
+
+                PropertyChanges { target: root; rotation: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "270"
+
+                PropertyChanges { target: root; rotation: 270 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "flip"
+
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "90flip"
+
+                PropertyChanges { target: root; rotation: 90 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "180flip"
+
+                PropertyChanges { target: root; rotation: 180 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "270flip"
+
+                PropertyChanges { target: root; rotation: 270 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            }
+        ]
+
+        transitions: [
+            Transition {
+                from: ",180flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "90,270flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "180,flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "270,90flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                enabled: enableAnimations
+
+                PieceRotationAnimation { }
+                PieceRotationAnimation { target: flipX; property: "angle" }
+            }
+        ]
+    }
+
+    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 : gameView
+
+                NumberAnimation {
+                    properties: "x,y,scale"
+                    duration: animationDurationMove
+                    easing.type: Easing.InOutSine
+                }
+            }
+            PropertyAction {
+                target: parentUnplayed.parent
+                property: "z"; value: 0
+            }
+        }
+    }
+}
diff --git a/pentobi/qml/PieceList.qml b/pentobi/qml/PieceList.qml
new file mode 100644 (file)
index 0000000..60d0ff3
--- /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: {
+                gameView.dropCommentFocus()
+                piecePicked(modelData)
+            }
+            Component.onCompleted: modelData.parentUnplayed = mouseArea
+        }
+    }
+}
diff --git a/pentobi/qml/PieceManipulator.qml b/pentobi/qml/PieceManipulator.qml
new file mode 100644 (file)
index 0000000..43f61a5
--- /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 || ! gameView.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/pentobi/qml/PieceNexos.qml b/pentobi/qml/PieceNexos.qml
new file mode 100644 (file)
index 0000000..8f583b0
--- /dev/null
@@ -0,0 +1,353 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceNexos.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+
+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
+        }
+    }
+    Loader {
+        sourceComponent:
+            (moveMarking == "last_dot" && pieceModel.isLastMove) || item ?
+                dotComponent : null
+
+        Component {
+            id: dotComponent
+
+            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: "90"
+
+                PropertyChanges { target: root; rotation: 90 }
+                // See comment in PieceClassic about flipY property change
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "180"
+
+                PropertyChanges { target: root; rotation: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "270"
+
+                PropertyChanges { target: root; rotation: 270 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "flip"
+
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "90flip"
+
+                PropertyChanges { target: root; rotation: 90 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "180flip"
+
+                PropertyChanges { target: root; rotation: 180 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "270flip"
+
+                PropertyChanges { target: root; rotation: 270 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            }
+        ]
+
+        transitions: [
+            Transition {
+                from: ",180flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "90,270flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "180,flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "270,90flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                enabled: enableAnimations
+
+                PieceRotationAnimation { }
+                PieceRotationAnimation { target: flipX; property: "angle" }
+            }
+        ]
+    }
+
+    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 : gameView
+
+                    NumberAnimation {
+                        properties: "x,y,scale"
+                        duration: animationDurationMove
+                        easing.type: Easing.InOutSine
+                    }
+                }
+                PropertyAction {
+                    target: parentUnplayed.parent
+                    property: "z"; value: 0
+                }
+            }
+    }
+}
diff --git a/pentobi/qml/PieceRotationAnimation.qml b/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/pentobi/qml/PieceSelectorDesktop.qml b/pentobi/qml/PieceSelectorDesktop.qml
new file mode 100644 (file)
index 0000000..a6d9c5a
--- /dev/null
@@ -0,0 +1,126 @@
+//-----------------------------------------------------------------------------
+/** @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 alias columns: pieceList0.columns
+    // Dummy for compatibility with PieceSelectorMobile
+    property bool transitionsEnabled
+
+    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: pieces0 ? Math.ceil(pieces0.length / 2) : 11
+                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
+            }
+        }
+    ]
+}
diff --git a/pentobi/qml/PieceSelectorMobile.qml b/pentobi/qml/PieceSelectorMobile.qml
new file mode 100644 (file)
index 0000000..723f15d
--- /dev/null
@@ -0,0 +1,249 @@
+//-----------------------------------------------------------------------------
+/** @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 alias columns: pieceList0.columns
+    property alias rowSpacing: pieceList0.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
+        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/pentobi/qml/PieceSwitchedFlipAnimation.qml b/pentobi/qml/PieceSwitchedFlipAnimation.qml
new file mode 100644 (file)
index 0000000..54dcbe7
--- /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
+    }
+    PieceRotationAnimation { target: flipY; property: "angle"; to: 180 }
+    PropertyAction { target: flipY; property: "angle"; value: 0 }
+}
diff --git a/pentobi/qml/PieceTrigon.qml b/pentobi/qml/PieceTrigon.qml
new file mode 100644 (file)
index 0000000..451f106
--- /dev/null
@@ -0,0 +1,348 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceTrigon.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/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
+        }
+    }
+    Loader {
+        sourceComponent:
+            (moveMarking == "last_dot" && pieceModel.isLastMove) || item ?
+                dotComponent : null
+
+        Component {
+            id: dotComponent
+
+            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: "60"
+
+                PropertyChanges { target: root; rotation: 60 }
+                // See comment in PieceClassic about flipY property change
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "120"
+
+                PropertyChanges { target: root; rotation: 120 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "180"
+
+                PropertyChanges { target: root; rotation: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "240"
+
+                PropertyChanges { target: root; rotation: 240 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "300"
+
+                PropertyChanges { target: root; rotation: 300 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "flip"
+
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "60flip"
+
+                PropertyChanges { target: root; rotation: 60 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "120flip"
+
+                PropertyChanges { target: root; rotation: 120 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "180flip"
+
+                PropertyChanges { target: root; rotation: 180 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "240flip"
+
+                PropertyChanges { target: root; rotation: 240 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            },
+            State {
+                name: "300flip"
+
+                PropertyChanges { target: root; rotation: 300 }
+                PropertyChanges { target: flipX; angle: 180 }
+                PropertyChanges { target: flipY; angle: 0 }
+            }
+        ]
+
+        transitions: [
+            Transition {
+                from: ",180flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "60,240flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "120,300flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "180,flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "240,60flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                from: "300,120flip"; to: from
+                enabled: enableAnimations
+
+                PieceSwitchedFlipAnimation { }
+            },
+            Transition {
+                enabled: enableAnimations
+
+                PieceRotationAnimation { }
+                PieceRotationAnimation { target: flipX; property: "angle" }
+            }
+        ]
+    }
+
+    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 : gameView
+
+                NumberAnimation {
+                    properties: "x,y,scale"
+                    duration: animationDurationMove
+                    easing.type: Easing.InOutSine
+                }
+            }
+            PropertyAction {
+                target: parentUnplayed.parent
+                property: "z"; value: 0
+            }
+        }
+    }
+}
diff --git a/pentobi/qml/QuarterSquare.qml b/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/pentobi/qml/QuestionDialog.qml b/pentobi/qml/QuestionDialog.qml
new file mode 100644 (file)
index 0000000..0187d0b
--- /dev/null
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------------
+/** @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 {
+    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/pentobi/qml/RatingDialog.qml b/pentobi/qml/RatingDialog.qml
new file mode 100644 (file)
index 0000000..03f6b4b
--- /dev/null
@@ -0,0 +1,185 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/RatingDialog.qml
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.12
+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
+
+    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 {
+                visible: ratingModel.ratingHistory.length > 1
+                Layout.fillWidth: true
+
+                Label { text: qsTr("Recent development:") }
+                RatingGraph {
+                    history: ratingModel.ratingHistory
+                    Layout.preferredHeight:
+                        Math.min(font.pixelSize * 8,
+                                 0.22 * rootWindow.contentItem.width,
+                                 0.22 * rootWindow.contentItem.height)
+                    Layout.fillWidth: true
+                }
+            }
+            TableView {
+                visible: ratingModel.ratingHistory.length > 0
+                clip: true
+                boundsBehavior: Flickable.StopAtBounds
+                model: ratingModel.tableModel
+                delegate: Label {
+                    font.underline: row === 0
+                    text: row > 0 && column === 3 ?
+                              Logic.getPlayerString(ratingModel.gameVariant,
+                                                    display)
+                            : display
+                    MouseArea {
+                        anchors.fill: parent
+                        acceptedButtons: Qt.LeftButton | Qt.RightButton
+                        onClicked: menu.openMenu(row, parent)
+                        onPressAndHold: menu.openMenu(row, parent)
+                    }
+                }
+                columnSpacing: 0.4 * font.pixelSize
+                rowSpacing: columnLayout.spacing
+                Layout.fillWidth: true
+                Layout.preferredHeight:
+                    Math.min(font.pixelSize * 8,
+                             0.22 * rootWindow.contentItem.width,
+                             0.22 * rootWindow.contentItem.height)
+                ScrollBar.vertical: ScrollBar { }
+            }
+            Pentobi.Menu {
+                id: menu
+
+                property int row
+
+                function openMenu(row, parent) {
+                    if (row < 1)
+                        return
+                    menu.parent = parent
+                    menu.row = row
+                    popup()
+                }
+
+                relativeWidth: 14
+
+                Pentobi.MenuItem {
+                    text: qsTr("Open Game %1").arg(
+                                ratingModel.getGameNumber(menu.row - 1))
+                    onTriggered: {
+                        var n = ratingModel.getGameNumber(menu.row - 1)
+                        Logic.openFile(ratingModel.getFile(n))
+                        close()
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/pentobi/qml/RatingGraph.qml b/pentobi/qml/RatingGraph.qml
new file mode 100644 (file)
index 0000000..2c20310
--- /dev/null
@@ -0,0 +1,68 @@
+//-----------------------------------------------------------------------------
+/** @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 minY = Number.POSITIVE_INFINITY
+        var maxY = Number.NEGATIVE_INFINITY
+        var info
+        for (i = 0; i < n; ++i) {
+            minY = Math.min(minY, history[i])
+            maxY = Math.max(maxY, history[i])
+        }
+        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)
+            ctx.lineTo(i * w / n, h - (history[i] - minY)  / (maxY - minY) * h)
+        ctx.strokeStyle = "red"
+        ctx.stroke()
+
+        ctx.restore()
+    }
+}
diff --git a/pentobi/qml/SaveDialog.qml b/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/pentobi/qml/ScoreDisplay.qml b/pentobi/qml/ScoreDisplay.qml
new file mode 100644 (file)
index 0000000..e0c421c
--- /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: gameView.color0[0]
+            color2: gameView.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: gameView.color1[0]
+            color2: gameView.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 gameView.color0[0]
+                case 1: return gameView.color1[0]
+                case 2: return gameView.color2[0]
+                }
+        }
+        Item { visible: altColorIndicator.visible; Layout.fillWidth: true }
+    }
+}
diff --git a/pentobi/qml/ScoreElement.qml b/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/pentobi/qml/ScoreElement2.qml b/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/pentobi/qml/Square.qml b/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/pentobi/qml/ToolBar.qml b/pentobi/qml/ToolBar.qml
new file mode 100644 (file)
index 0000000..830957f
--- /dev/null
@@ -0,0 +1,302 @@
+//-----------------------------------------------------------------------------
+/** @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.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(gameView.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)
+            toolTipText: qsTr("Start a new game")
+        }
+        Pentobi.Button {
+            id: newGameRated
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-rated-game")
+            action: actionNewRated
+            toolTipText: qsTr("Start a rated game")
+        }
+        Pentobi.Button {
+            id: undo
+
+            icon.source: theme.getImage("pentobi-undo")
+            action: actionUndo
+            visible: showContent && (isDesktop || enabled)
+            autoRepeat: true
+            autoRepeatInterval:
+                rootWindow.gameView.item ?
+                    2 * rootWindow.gameView.item.animationDuration : 400
+            //: Tooltip for Undo button
+            toolTipText: qsTr("Undo move")
+        }
+        Pentobi.Button {
+            id: computerSettings
+
+            icon.source: theme.getImage("pentobi-computer-colors")
+            action: actionComputerSettings
+            visible: showContent && (isDesktop || enabled)
+            toolTipText: qsTr("Set the colors played by the computer")
+        }
+        Pentobi.Button {
+            id: play
+
+            icon.source: theme.getImage("pentobi-play")
+            action: actionPlay
+            visible: showContent && (isDesktop || enabled)
+            autoRepeat: true
+            // Use fast autorepeat to avoid flickering of
+            // Pentobi.Button.pressedAnimation, presses while computer is
+            // thinking are ignored anyway.
+            autoRepeatInterval: 50
+            toolTipText: {
+                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")
+            }
+        }
+        Pentobi.Button {
+            id: stop
+
+            icon.source: theme.getImage("pentobi-stop")
+            action: actionStop
+            visible: showContent && (isDesktop || ! isRated)
+            toolTipText: analyzeGameModel.isRunning ?
+                              qsTr("Abort game analysis")
+                            : qsTr("Abort computer move")
+        }
+        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
+            toolTipText: qsTr("Go to beginning of game")
+        }
+        Pentobi.Button {
+            id: backward10
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-backward10")
+            action: actionBackward10
+            autoRepeat: true
+            autoRepeatInterval:
+                rootWindow.gameView.item ?
+                    rootWindow.gameView.item.animationDuration : 200
+            toolTipText: qsTr("Go ten moves backward")
+        }
+        Pentobi.Button {
+            id: backward
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-backward")
+            action: actionBackward
+            autoRepeat: true
+            toolTipText: qsTr("Go one move backward")
+        }
+        Pentobi.Button {
+            id: forward
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-forward")
+            action: actionForward
+            autoRepeat: true
+            toolTipText: qsTr("Go one move forward")
+        }
+        Pentobi.Button {
+            id: forward10
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-forward10")
+            action: actionForward10
+            autoRepeat: true
+            autoRepeatInterval:
+                rootWindow.gameView.item ?
+                    rootWindow.gameView.item.animationDuration : 200
+            toolTipText: qsTr("Go ten moves forward")
+        }
+        Pentobi.Button {
+            id: end
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-end")
+            action: actionEnd
+            toolTipText: qsTr("Go to end of moves")
+        }
+        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.gameView.item ?
+                    2 * rootWindow.gameView.item.animationDuration : 400
+            toolTipText: qsTr("Go to previous variation")
+        }
+        Pentobi.Button {
+            id: nextVar
+
+            visible: showContent && isDesktop
+            icon.source: theme.getImage("pentobi-next-variation")
+            action: actionNextVar
+            autoRepeat: true
+            autoRepeatInterval:
+                rootWindow.gameView.item ?
+                    2 * rootWindow.gameView.item.animationDuration : 400
+            toolTipText: qsTr("Go to next variation")
+        }
+        Item {
+            visible: isDesktop
+            Layout.fillWidth: true
+            Layout.maximumWidth: 0.3 * parent.height
+        }
+        Label {
+            visible: showContent && isDesktop
+            text: Logic.getGameLabel(gameView.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 && ! gameView.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(isDesktop ? "menu-desktop" : "menu")
+            down: isDesktop && (pressed || (menu.item && menu.item.opened))
+            onClicked: {
+                if (! menu.item)
+                    menu.sourceComponent = menuComponent
+                if (menu.item.opened)
+                    menu.item.close()
+                else {
+                    gameView.dropCommentFocus()
+                    menu.item.popup(0, isDesktop ? height : 0)
+                }
+            }
+            toolTipText: qsTr("Open menu")
+
+            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 {
+                        relativeWidth: 12
+
+                        closePolicy: Popup.CloseOnPressOutsideParent
+                                     | Popup.CloseOnEscape
+
+                        MenuGame { relativeWidth: 24 }
+                        MenuGo { relativeWidth: 19 }
+                        MenuEdit { relativeWidth: 21 }
+                        MenuView { relativeWidth: 18 }
+                        MenuComputer { relativeWidth: 19 }
+                        MenuTools { relativeWidth: 14 }
+                        MenuHelp { relativeWidth: 14 }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/pentobi/qml/Triangle.qml b/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/pentobi/qml/i18n/qml_de.ts b/pentobi/qml/i18n/qml_de.ts
new file mode 100644 (file)
index 0000000..aeb48a4
--- /dev/null
@@ -0,0 +1,1430 @@
+<?xml version="1.0" ?><!DOCTYPE TS><TS language="de" version="2.1">
+<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>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>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>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>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>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <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>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <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>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>Rot gewinnt (Unentschieden aufgelöst).</translation>
+    </message>
+    <message>
+        <source>Yellow wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <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>GameViewDesktop</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>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>
+        <extracomment>Label for modified loaded game. The argument is the file name.</extracomment>
+        <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>
+    <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>Pentobi failed to generate a move.</source>
+        <translation>Pentobi konnte keinen Zug generieren.</translation>
+    </message>
+    <message>
+        <source>Press back again to exit</source>
+        <translation>Zum Beenden erneut Zurück drücken</translation>
+    </message>
+</context>
+<context>
+    <name>MenuComputer</name>
+    <message>
+        <source>Computer</source>
+        <translation>Computer</translation>
+    </message>
+</context>
+<context>
+    <name>MenuEdit</name>
+    <message>
+        <source>Edit</source>
+        <translation>Bearbeiten</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>Variation Down</source>
+        <extracomment>Short for Move Variation Down</extracomment>
+        <translation>Variante nach unten</translation>
+    </message>
+    <message>
+        <source>Delete Variations</source>
+        <translation>Varianten löschen</translation>
+    </message>
+    <message>
+        <source>Truncate</source>
+        <translation>Abschneiden</translation>
+    </message>
+    <message>
+        <source>Truncate Children</source>
+        <translation>Kindknoten abschneiden</translation>
+    </message>
+    <message>
+        <source>Keep Position</source>
+        <translation>Brettstellung behalten</translation>
+    </message>
+    <message>
+        <source>Keep Subtree</source>
+        <translation>Teilbaum behalten</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Stellungsaufbau</translation>
+    </message>
+    <message>
+        <source>Next Color</source>
+        <translation>Nächste Farbe</translation>
+    </message>
+    <message>
+        <source>Annotation…</source>
+        <translation>Annotierung …</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>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>Game Variant…</source>
+        <translation>Spielvariante …</translation>
+    </message>
+    <message>
+        <source>Open Clipboard</source>
+        <translation>Zwischenablage öffnen</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGo</name>
+    <message>
+        <source>Go</source>
+        <translation>Gehe zu</translation>
+    </message>
+</context>
+<context>
+    <name>MenuHelp</name>
+    <message>
+        <source>Help</source>
+        <translation>Hilfe</translation>
+    </message>
+    <message>
+        <source>About Pentobi</source>
+        <translation>Über Pentobi</translation>
+    </message>
+    <message>
+        <source>Report Bug</source>
+        <translation>Fehler melden</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>Clear List</source>
+        <extracomment>Menu item for clearing the recent files list</extracomment>
+        <translation>Liste leeren</translation>
+    </message>
+</context>
+<context>
+    <name>MenuTools</name>
+    <message>
+        <source>Tools</source>
+        <translation>Extras</translation>
+    </message>
+    <message>
+        <source>Rating</source>
+        <translation>Wertung</translation>
+    </message>
+    <message>
+        <source>Clear Rating</source>
+        <translation>Wertung löschen</translation>
+    </message>
+    <message>
+        <source>Clear Analysis</source>
+        <translation>Analyse löschen</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>Appearance</source>
+        <translation>Erscheinungsbild</translation>
+    </message>
+    <message>
+        <source>Toolbar</source>
+        <translation>Werkzeugleiste</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>
+</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>TableModel</name>
+    <message>
+        <source>Game</source>
+        <extracomment>Table header for game number in rating dialog</extracomment>
+        <translation>Spiel</translation>
+    </message>
+    <message>
+        <source>Result</source>
+        <extracomment>Table header for game result in rating dialog</extracomment>
+        <translation>Ergebnis</translation>
+    </message>
+    <message>
+        <source>Level</source>
+        <extracomment>Table header for level in rating dialog</extracomment>
+        <translation>Stufe</translation>
+    </message>
+    <message>
+        <source>Your Color</source>
+        <extracomment>Table header for player color(s) in rating dialog</extracomment>
+        <translation>Ihre Farbe</translation>
+    </message>
+    <message>
+        <source>Date</source>
+        <extracomment>Table header for game date in rating dialog</extracomment>
+        <translation>Datum</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>
+</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>
+    <message>
+        <source>Open menu</source>
+        <translation>Menü öffnen</translation>
+    </message>
+</context>
+<context>
+    <name>main</name>
+    <message>
+        <source>computer opponent for the board game Blokus</source>
+        <translation>Computer-Gegner für das Brettspiel Blokus</translation>
+    </message>
+    <message>
+        <source>Set maximum level to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --maxlevel</extracomment>
+        <translation>Maximale Spielstufe auf &lt;n&gt; setzen.</translation>
+    </message>
+    <message>
+        <source>Do not use opening books.</source>
+        <extracomment>Description for command line option --nobook</extracomment>
+        <translation>Keine Eröffnungsbibliotheken benutzen.</translation>
+    </message>
+    <message>
+        <source>Use layout optimized for smartphones.</source>
+        <extracomment>Description for command line option --mobile</extracomment>
+        <translation>Für Smartphones optimiertes Layout benutzen.</translation>
+    </message>
+    <message>
+        <source>Do not delay fast computer moves.</source>
+        <extracomment>Description for command line option --nodelay</extracomment>
+        <translation>Schnelle Computer-Züge nicht verzögern.</translation>
+    </message>
+    <message>
+        <source>Set random seed to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --seed</extracomment>
+        <translation>Seed für Zufallszahlen auf &lt;n&gt; setzen.</translation>
+    </message>
+    <message>
+        <source>Use &lt;n&gt; threads (0=auto).</source>
+        <extracomment>Description for command line option --threads</extracomment>
+        <translation>&lt;n&gt; Threads benutzen (0=automatisch).</translation>
+    </message>
+    <message>
+        <source>Print logging information to standard error.</source>
+        <extracomment>Description for command line option --verbose</extracomment>
+        <translation>Log-Informationen auf Standard-Error ausgeben.</translation>
+    </message>
+    <message>
+        <source>file.blksgf</source>
+        <extracomment>Name of command line argument.</extracomment>
+        <translation>Datei.blksgf</translation>
+    </message>
+    <message>
+        <source>Blokus SGF file to open (optional).</source>
+        <translation>Zu öffnende Blokus-SGF-Datei (optional).</translation>
+    </message>
+    <message>
+        <source>--maxlevel must be between 1 and %1</source>
+        <translation>--maxlevel muss zwischen 1 und %1 sein</translation>
+    </message>
+    <message>
+        <source>--seed must be a positive number</source>
+        <translation>--seed muss eine positive Zahl sein</translation>
+    </message>
+    <message>
+        <source>--threads must be a positive number</source>
+        <translation>--threads muss eine positive Zahl sein</translation>
+    </message>
+    <message>
+        <source>Too many arguments</source>
+        <translation>Zu viele Argumente</translation>
+    </message>
+</context>
+</TS>
\ No newline at end of file
diff --git a/pentobi/qml/i18n/qml_en.ts b/pentobi/qml/i18n/qml_en.ts
new file mode 100644 (file)
index 0000000..e0e405d
--- /dev/null
@@ -0,0 +1,1436 @@
+<?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>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>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>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>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <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>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <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>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Yellow wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <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>GameViewDesktop</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>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>
+        <extracomment>Label for modified loaded game. The argument is the file name.</extracomment>
+        <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>
+    <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>Pentobi failed to generate a move.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Press back again to exit</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuComputer</name>
+    <message>
+        <source>Computer</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuEdit</name>
+    <message>
+        <source>Edit</source>
+        <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>Variation Down</source>
+        <extracomment>Short for Move Variation Down</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Delete Variations</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Truncate</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Truncate Children</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Keep Position</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Keep Subtree</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Next Color</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Annotation…</source>
+        <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>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>Game Variant…</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Open Clipboard</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuGo</name>
+    <message>
+        <source>Go</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuHelp</name>
+    <message>
+        <source>Help</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>About Pentobi</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Report Bug</source>
+        <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>Clear List</source>
+        <extracomment>Menu item for clearing the recent files list</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MenuTools</name>
+    <message>
+        <source>Tools</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Rating</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Clear Rating</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Clear Analysis</source>
+        <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>Appearance</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Toolbar</source>
+        <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>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>TableModel</name>
+    <message>
+        <source>Game</source>
+        <extracomment>Table header for game number in rating dialog</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Result</source>
+        <extracomment>Table header for game result in rating dialog</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Level</source>
+        <extracomment>Table header for level in rating dialog</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Your Color</source>
+        <extracomment>Table header for player color(s) in rating dialog</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Date</source>
+        <extracomment>Table header for game date in rating dialog</extracomment>
+        <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>
+</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>
+    <message>
+        <source>Open menu</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>main</name>
+    <message>
+        <source>computer opponent for the board game Blokus</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Set maximum level to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --maxlevel</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Do not use opening books.</source>
+        <extracomment>Description for command line option --nobook</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Use layout optimized for smartphones.</source>
+        <extracomment>Description for command line option --mobile</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Do not delay fast computer moves.</source>
+        <extracomment>Description for command line option --nodelay</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Set random seed to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --seed</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Use &lt;n&gt; threads (0=auto).</source>
+        <extracomment>Description for command line option --threads</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Print logging information to standard error.</source>
+        <extracomment>Description for command line option --verbose</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>file.blksgf</source>
+        <extracomment>Name of command line argument.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Blokus SGF file to open (optional).</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>--maxlevel must be between 1 and %1</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>--seed must be a positive number</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>--threads must be a positive number</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Too many arguments</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+</TS>
diff --git a/pentobi/qml/i18n/qml_es.ts b/pentobi/qml/i18n/qml_es.ts
new file mode 100644 (file)
index 0000000..e6b659d
--- /dev/null
@@ -0,0 +1,1434 @@
+<?xml version="1.0" ?><!DOCTYPE TS><TS language="es" version="2.1">
+<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>Rival virtual para el juego de mesa Blokus</translation>
+    </message>
+    <message>
+        <source>Pentobi %1</source>
+        <extracomment>The argument is the application version.</extracomment>
+        <translation>Pentobi %1</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeDialog</name>
+    <message>
+        <source>Analysis speed:</source>
+        <translation>Análisis de velocidad:</translation>
+    </message>
+    <message>
+        <source>Fast</source>
+        <translation>Rápido</translation>
+    </message>
+    <message>
+        <source>Normal</source>
+        <translation>Normal</translation>
+    </message>
+    <message>
+        <source>Slow</source>
+        <translation>Lento</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeGame</name>
+    <message>
+        <source>(No analysis)</source>
+        <translation>(Sin análisis)</translation>
+    </message>
+</context>
+<context>
+    <name>AppearanceDialog</name>
+    <message>
+        <source>Coordinates</source>
+        <translation>Coordenadas</translation>
+    </message>
+    <message>
+        <source>Show variations</source>
+        <translation>Mostrar variaciones</translation>
+    </message>
+    <message>
+        <source>Light</source>
+        <translation>Blanco</translation>
+    </message>
+    <message>
+        <source>Dark</source>
+        <translation>Negro</translation>
+    </message>
+    <message>
+        <source>Colorblind light</source>
+        <translation>Fondo blanco para daltónicos</translation>
+    </message>
+    <message>
+        <source>Colorblind dark</source>
+        <translation>Fondo negro para daltónicos</translation>
+    </message>
+    <message>
+        <source>System</source>
+        <extracomment>Name of theme using default system colors</extracomment>
+        <translation>Sistema</translation>
+    </message>
+    <message>
+        <source>Move marking:</source>
+        <translation>Marcado de movimientos:</translation>
+    </message>
+    <message>
+        <source>Last with dot</source>
+        <translation>Último con punto</translation>
+    </message>
+    <message>
+        <source>Last with number</source>
+        <translation>Último con número</translation>
+    </message>
+    <message>
+        <source>All with number</source>
+        <translation>Todas con número</translation>
+    </message>
+    <message>
+        <source>None</source>
+        <extracomment>Move marking/None</extracomment>
+        <translation>Ninguno</translation>
+    </message>
+    <message>
+        <source>Animations</source>
+        <translation>Animaciones</translation>
+    </message>
+    <message>
+        <source>Show comment:</source>
+        <translation>Mostrar comentario:</translation>
+    </message>
+    <message>
+        <source>Always</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Siempre</translation>
+    </message>
+    <message>
+        <source>As needed</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Si es preciso</translation>
+    </message>
+    <message>
+        <source>Never</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Nunca</translation>
+    </message>
+    <message>
+        <source>Color theme:</source>
+        <translation>Color del tema:</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>Número de movimiento</translation>
+    </message>
+</context>
+<context>
+    <name>AsciiArtSaveDialog</name>
+    <message>
+        <source>Export ASCII Art</source>
+        <translation>Exportar arte ASCII</translation>
+    </message>
+    <message>
+        <source>Text files</source>
+        <translation>Archivos de texto</translation>
+    </message>
+</context>
+<context>
+    <name>BoardContextMenu</name>
+    <message>
+        <source>Go to Move %1</source>
+        <translation>Ir al movimiento %1</translation>
+    </message>
+    <message>
+        <source>Move Annotation</source>
+        <translation>Valoración del movimiento</translation>
+    </message>
+    <message>
+        <source>Move Annotation (%1)</source>
+        <extracomment>The argument is the annotation symbol for the current move</extracomment>
+        <translation>Valoración del movimiento (%1)</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonApply</name>
+    <message>
+        <source>Apply</source>
+        <translation>Aplicar</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonCancel</name>
+    <message>
+        <source>Cancel</source>
+        <translation>Cancelar</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonClose</name>
+    <message>
+        <source>Close</source>
+        <translation>Cerrar</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonOk</name>
+    <message>
+        <source>OK</source>
+        <translation>Aceptar</translation>
+    </message>
+</context>
+<context>
+    <name>ComputerDialog</name>
+    <message>
+        <source>Computer plays:</source>
+        <translation>La máquina juega con:</translation>
+    </message>
+    <message>
+        <source>Level %1</source>
+        <translation>Nivel %1</translation>
+    </message>
+</context>
+<context>
+    <name>ExportImageDialog</name>
+    <message>
+        <source>Image width:</source>
+        <translation>Ancho de la imagen:</translation>
+    </message>
+</context>
+<context>
+    <name>FileDialog</name>
+    <message>
+        <source>Overwrite file?</source>
+        <translation>¿Sobrescribir archivo?</translation>
+    </message>
+    <message>
+        <source>Open</source>
+        <translation>Abrir</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>Guardar</translation>
+    </message>
+    <message>
+        <source>All files</source>
+        <translation>Todos los archivos</translation>
+    </message>
+</context>
+<context>
+    <name>GameInfoDialog</name>
+    <message>
+        <source>Player Blue/Red:</source>
+        <translation>Jugador con azules/rojas:</translation>
+    </message>
+    <message>
+        <source>Player Purple:</source>
+        <translation>Jugador con moradas:</translation>
+    </message>
+    <message>
+        <source>Player Green:</source>
+        <translation>Jugador con verdes:</translation>
+    </message>
+    <message>
+        <source>Player Blue:</source>
+        <translation>Jugador con azules:</translation>
+    </message>
+    <message>
+        <source>Player Yellow/Green:</source>
+        <translation>Jugador con amarillas/verdes:</translation>
+    </message>
+    <message>
+        <source>Player Orange:</source>
+        <translation>Jugador con naranjas:</translation>
+    </message>
+    <message>
+        <source>Player Yellow:</source>
+        <translation>Jugador con amarillas:</translation>
+    </message>
+    <message>
+        <source>Player Red:</source>
+        <translation>Jugador con rojas:</translation>
+    </message>
+    <message>
+        <source>Date:</source>
+        <translation>Fecha:</translation>
+    </message>
+    <message>
+        <source>Time:</source>
+        <translation>Tiempo:</translation>
+    </message>
+    <message>
+        <source>Event:</source>
+        <translation>Acción:</translation>
+    </message>
+    <message>
+        <source>Round:</source>
+        <translation>Ronda:</translation>
+    </message>
+</context>
+<context>
+    <name>GameModel</name>
+    <message>
+        <source>Purple wins with 1 point.</source>
+        <translation>Moradas ganan con 1 punto.</translation>
+    </message>
+    <message>
+        <source>Purple wins with %L1 points.</source>
+        <translation>Moradas ganan con %L1 puntos.</translation>
+    </message>
+    <message>
+        <source>Orange wins with 1 point.</source>
+        <translation>Naranjas ganan con 1 punto.</translation>
+    </message>
+    <message>
+        <source>Orange wins with %L1 points.</source>
+        <translation>Naranjas ganan con %L1 puntos.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie.</source>
+        <translation>La partida acaba en empate.</translation>
+    </message>
+    <message>
+        <source>Green wins with 1 point.</source>
+        <translation>Verdes ganan con 1 punto.</translation>
+    </message>
+    <message>
+        <source>Green wins with %L1 points.</source>
+        <translation>Verdes ganan con %L1 puntos.</translation>
+    </message>
+    <message>
+        <source>Blue wins with 1 point.</source>
+        <translation>Azules ganan con 1 punto.</translation>
+    </message>
+    <message>
+        <source>Blue wins with %L1 points.</source>
+        <translation>Azules ganan con %L1 puntos.</translation>
+    </message>
+    <message>
+        <source>Green wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>Ganan verdes (empate resuelto).</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with 1 point.</source>
+        <translation>Azules/rojas ganan con 1 punto.</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with %L1 points.</source>
+        <translation>Azules/rojas ganan con %L1 puntos.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with 1 point.</source>
+        <translation>Amarillas/verdes ganan con 1 punto.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with %L1 points.</source>
+        <translation>Amarillas/verdes ganan con %L1 puntos.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>Ganan amarillas/verdes (empate resuelto).</translation>
+    </message>
+    <message>
+        <source>Blue wins.</source>
+        <translation>Azules ganan.</translation>
+    </message>
+    <message>
+        <source>Yellow wins.</source>
+        <translation>Amarillas ganan.</translation>
+    </message>
+    <message>
+        <source>Red wins.</source>
+        <translation>Rojas ganan.</translation>
+    </message>
+    <message>
+        <source>Red wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>Ganan rojas (empate resuelto).</translation>
+    </message>
+    <message>
+        <source>Yellow wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>Ganan amarillas (empate resuelto).</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Yellow.</source>
+        <translation>La partida acaba con empate entre azules y amarillas.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Red.</source>
+        <translation>La partida acaba con empate entre azules y rojas.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow and Red.</source>
+        <translation>La partida acaba con empate entre amarillas y rojas.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between all players.</source>
+        <translation>La partida acaba con empate entre todos los jugadores.</translation>
+    </message>
+    <message>
+        <source>Green wins.</source>
+        <translation>Verdes ganan.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Red.</source>
+        <translation>La partida acaba con empate entre azules, amarillas y rojas.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Green.</source>
+        <translation>La partida acaba con empate entre azules, amarillas y verdes.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Red and Green.</source>
+        <translation>La partida acaba con empate entre azules, rojas y verdes.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow, Red and Green.</source>
+        <translation>La partida acaba con empate entre amarillas, rojas y verdes.</translation>
+    </message>
+    <message>
+        <source>Invalid Blokus SGF file. (%1)</source>
+        <translation>Archivo SGF de Blokus no válido. (%1)</translation>
+    </message>
+    <message>
+        <source>Clipboard is empty.</source>
+        <translation>El portapapeles está vacío.</translation>
+    </message>
+    <message>
+        <source>Untitled</source>
+        <translation>Sin título</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>Sin título %1</translation>
+    </message>
+    <message>
+        <source>New Folder</source>
+        <translation>Nueva carpeta</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>Nueva carpeta %1</translation>
+    </message>
+    <message>
+        <source>(Setup)</source>
+        <translation>(Configuración)</translation>
+    </message>
+    <message>
+        <source>(No moves)</source>
+        <translation>(Sin movimientos)</translation>
+    </message>
+    <message>
+        <source>Move %1</source>
+        <extracomment>The argument is the current move number.</extracomment>
+        <translation>Movimiento %1</translation>
+    </message>
+    <message>
+        <source>Unsupported character set</source>
+        <translation>Juego de caracteres no compatible</translation>
+    </message>
+</context>
+<context>
+    <name>GameVariantDialog</name>
+    <message>
+        <source>Classic</source>
+        <translation>Clásico</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>Dúo</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>Sencillo</translation>
+    </message>
+    <message>
+        <source>Trigon</source>
+        <translation>Trigón</translation>
+    </message>
+    <message>
+        <source>Nexos</source>
+        <translation>Nexos</translation>
+    </message>
+    <message>
+        <source>GembloQ</source>
+        <translation>GembloQ</translation>
+    </message>
+    <message>
+        <source>Callisto</source>
+        <translation>Calisto</translation>
+    </message>
+    <message>
+        <source>Players:</source>
+        <translation>Jugadores:</translation>
+    </message>
+    <message>
+        <source>Colors:</source>
+        <translation>Colores:</translation>
+    </message>
+</context>
+<context>
+    <name>GameViewDesktop</name>
+    <message>
+        <source>Computer is thinking…</source>
+        <translation>La máquina está pensando...</translation>
+    </message>
+    <message>
+        <source>Running game analysis…</source>
+        <translation>Realizando análisis de la partida...</translation>
+    </message>
+    <message>
+        <source>Computer is thinking… (up to %1 seconds remaining)</source>
+        <translation>La máquina está pensando... (queda un máximo de %1 segundos)</translation>
+    </message>
+    <message>
+        <source>Computer is thinking… (up to %1 minutes remaining)</source>
+        <translation>La máquina está pensando... (queda un máximo de %1 minutos)</translation>
+    </message>
+</context>
+<context>
+    <name>GotoMoveDialog</name>
+    <message>
+        <source>Move number:</source>
+        <translation>Número de movimiento:</translation>
+    </message>
+</context>
+<context>
+    <name>HelpWindow</name>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Ayuda para Pentobi</translation>
+    </message>
+</context>
+<context>
+    <name>ImageSaveDialog</name>
+    <message>
+        <source>Save Image</source>
+        <translation>Guardar imagen</translation>
+    </message>
+    <message>
+        <source>PNG image files</source>
+        <translation>Archivos de imagen PNG</translation>
+    </message>
+    <message>
+        <source>JPEG image files</source>
+        <translation>Archivos de imagen JPEG</translation>
+    </message>
+</context>
+<context>
+    <name>InitialRatingDialog</name>
+    <message>
+        <source>Initialize your rating for this game variant.</source>
+        <translation>Inicialice su nivel para esta variante de juego.</translation>
+    </message>
+    <message>
+        <source>Initial rating:</source>
+        <translation>Nivel inicial:</translation>
+    </message>
+    <message>
+        <source>Beginner</source>
+        <translation>Principiante</translation>
+    </message>
+    <message>
+        <source>Expert</source>
+        <translation>Experto</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>Solo se puede analizar la partida en la variación principal.</translation>
+    </message>
+    <message>
+        <source>Autosaved game was changed by another instance of Pentobi. Overwrite?</source>
+        <translation>Existe una partida guardada automáticamente que ha sido modificada por una nueva partida de Pentobi. ¿Desea sobrescribirla?</translation>
+    </message>
+    <message>
+        <source>Your rating has increased from %1 to %2.</source>
+        <translation>Su nivel ha subido de %1 a %2.</translation>
+    </message>
+    <message>
+        <source>Your rating has decreased from %1 to %2.</source>
+        <translation>Tu nivel ha descendido de %1 a %2.</translation>
+    </message>
+    <message>
+        <source>Your rating stays at %1.</source>
+        <translation>Su nivel sigue siendo %1.</translation>
+    </message>
+    <message>
+        <source>No permission to access storage</source>
+        <translation>Sin permiso para acceder al almacenamiento</translation>
+    </message>
+    <message>
+        <source>Delete all rating information for the current game variant?</source>
+        <translation>¿Desactivar la selección de nivel para la variante de juego actual?</translation>
+    </message>
+    <message>
+        <source>Delete all variations?</source>
+        <translation>¿Eliminar todas las variaciones?</translation>
+    </message>
+    <message>
+        <source>Save failed.</source>
+        <translation>Error al guardar.</translation>
+    </message>
+    <message>
+        <source>End of tree was reached. Continue search from start of the tree?</source>
+        <translation>Se ha alcanzado el final del árbol. ¿Desea continuar la búsqueda desde el inicio del árbol?</translation>
+    </message>
+    <message>
+        <source>No comment found</source>
+        <translation>No hay comentarios</translation>
+    </message>
+    <message>
+        <source>%1 (modified)</source>
+        <extracomment>Label for modified loaded game. The argument is the file name.</extracomment>
+        <translation>%1 (modificado)</translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Reload?</source>
+        <translation>Otra aplicación ha modificado el archivo. ¿Desea volver a cargarlo?</translation>
+    </message>
+    <message>
+        <source>Continue computer move?</source>
+        <translation>¿Continuar con el movimiento de la máquina?</translation>
+    </message>
+    <message>
+        <source>Keep only position?</source>
+        <translation>¿Conservar solo posición?</translation>
+    </message>
+    <message>
+        <source>Keep only subtree?</source>
+        <translation>¿Conservar solo subárbol?</translation>
+    </message>
+    <message>
+        <source>Open failed.</source>
+        <translation>Error al abrir.</translation>
+    </message>
+    <message>
+        <source>Start rated game with Purple against Pentobi level %1?</source>
+        <translation>¿Comenzar partida con selección de nivel con moradas contra Pentobi nivel %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Green against Pentobi level %1?</source>
+        <translation>¿Comenzar partida con selección de nivel con verdes contra Pentobi nivel %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue/Red against Pentobi level %1?</source>
+        <translation>¿Comenzar partida con selección de nivel con azules/rojas contra Pentobi nivel %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue against Pentobi level %1?</source>
+        <translation>¿Comenzar partida con selección de nivel con azules contra Pentobi nivel %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Orange against Pentobi level %1?</source>
+        <translation>¿Comenzar partida con selección de nivel con naranjas contra Pentobi nivel %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow/Green against Pentobi level %1?</source>
+        <translation>¿Comenzar partida con selección de nivel con amarillas/verdes contra Pentobi nivel %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow against Pentobi level %1?</source>
+        <translation>¿Comenzar partida con selección de nivel con amarillas contra Pentobi nivel %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Red against Pentobi level %1?</source>
+        <translation>¿Comenzar la partida con rojas, después de la selección de nivel, contra Pentobi nivel %1?</translation>
+    </message>
+    <message>
+        <source>You have not yet played rated games in this game variant.</source>
+        <translation>Todavía no ha jugado ninguna partida con selección de nivel en esta variante de juego.</translation>
+    </message>
+    <message>
+        <source>Truncate this subtree?</source>
+        <translation>¿Eliminar este subárbol?</translation>
+    </message>
+    <message>
+        <source>Truncate children?</source>
+        <translation>¿Eliminar descendientes?</translation>
+    </message>
+    <message>
+        <source>Discard game?</source>
+        <translation>¿Abandonar partida?</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 (nivel %2)</translation>
+    </message>
+    <message>
+        <source>Human</source>
+        <extracomment>Player name for game info in rated game.</extracomment>
+        <translation>Humano</translation>
+    </message>
+    <message>
+        <source>Rated game</source>
+        <translation>Partida con selección de nivel</translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Overwrite?</source>
+        <translation>Otra aplicación ha modificado el archivo. ¿Desea sobrescribirlo?</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>Memoria insuficiente</translation>
+    </message>
+    <message>
+        <source>Game analysis aborted</source>
+        <translation>Se ha anulado el análisis de la partida</translation>
+    </message>
+    <message>
+        <source>Computer move aborted</source>
+        <translation>Se ha anulado el movimiento de la máquina</translation>
+    </message>
+    <message>
+        <source>Rating information deleted</source>
+        <translation>La información sobre el nivel ha sido borrada</translation>
+    </message>
+    <message>
+        <source>Variations deleted</source>
+        <translation>Se han borrado las variaciones</translation>
+    </message>
+    <message>
+        <source>File saved</source>
+        <translation>Archivo guardado</translation>
+    </message>
+    <message>
+        <source>Saving image failed or unsupported image format</source>
+        <translation>Se ha producido un error al guardar la imagen o el formato de archivo de imagen no es compatible</translation>
+    </message>
+    <message>
+        <source>Image saved</source>
+        <translation>Se ha guardado la imagen</translation>
+    </message>
+    <message>
+        <source>Creating image failed</source>
+        <translation>Se ha producido un error al crear la imagen</translation>
+    </message>
+    <message>
+        <source>Continuing rated game</source>
+        <translation>Continuar partida con selección de nivel</translation>
+    </message>
+    <message>
+        <source>Kept only position</source>
+        <translation>Se ha conservado solo la posición</translation>
+    </message>
+    <message>
+        <source>Kept only subtree</source>
+        <translation>Se ha conservado solo el subárbol</translation>
+    </message>
+    <message>
+        <source>Variation is now %1</source>
+        <translation>La variación es ahora %1</translation>
+    </message>
+    <message>
+        <source>Children truncated</source>
+        <translation>Se han eliminado los descendientes</translation>
+    </message>
+    <message>
+        <source>Setup</source>
+        <extracomment>Small-screen label for setup mode (short for &quot;Setup Mode&quot;).</extracomment>
+        <translation>Configuración</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Modo configuración</translation>
+    </message>
+    <message>
+        <source>Rated</source>
+        <extracomment>Label for ongoing rated game</extracomment>
+        <translation>Nivel</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>Nivel %1</translation>
+    </message>
+    <message>
+        <source>Rated Game %1</source>
+        <extracomment>Label for rated game. The argument is the game number.</extracomment>
+        <translation>Partida con selección de nivel %1</translation>
+    </message>
+    <message>
+        <source>Main Variation</source>
+        <translation>Variación principal</translation>
+    </message>
+    <message>
+        <source>Beginning of Branch</source>
+        <translation>Inicio de la rama</translation>
+    </message>
+    <message>
+        <source>Comment</source>
+        <translation>Comentario</translation>
+    </message>
+    <message>
+        <source>Settings</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation>Ajustes</translation>
+    </message>
+    <message>
+        <source>Find Move</source>
+        <translation>Buscar movimiento</translation>
+    </message>
+    <message>
+        <source>Next Comment</source>
+        <translation>Siguiente comentario</translation>
+    </message>
+    <message>
+        <source>Fullscreen</source>
+        <translation>Pantalla completa</translation>
+    </message>
+    <message>
+        <source>Game Info</source>
+        <translation>Información de partida</translation>
+    </message>
+    <message>
+        <source>Move Number…</source>
+        <translation>Número de movimiento...</translation>
+    </message>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Ayuda para Pentobi</translation>
+    </message>
+    <message>
+        <source>New</source>
+        <translation>Nuevo</translation>
+    </message>
+    <message>
+        <source>Rated Game</source>
+        <translation>Partida con selección de nivel</translation>
+    </message>
+    <message>
+        <source>Open…</source>
+        <translation>Abrir...</translation>
+    </message>
+    <message>
+        <source>Play</source>
+        <translation>Jugar</translation>
+    </message>
+    <message>
+        <source>Play Move</source>
+        <extracomment>Play a single move</extracomment>
+        <translation>Realizar movimiento</translation>
+    </message>
+    <message>
+        <source>Quit</source>
+        <translation>Salir</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>Guardar</translation>
+    </message>
+    <message>
+        <source>Save As…</source>
+        <translation>Guardar como...</translation>
+    </message>
+    <message>
+        <source>Stop</source>
+        <translation>Detener</translation>
+    </message>
+    <message>
+        <source>Undo Move</source>
+        <translation>Deshacer movimiento</translation>
+    </message>
+    <message>
+        <source>Blue/Red</source>
+        <translation>Azul/rojo</translation>
+    </message>
+    <message>
+        <source>Purple</source>
+        <translation>Morado</translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation>Verde</translation>
+    </message>
+    <message>
+        <source>Blue</source>
+        <translation>Azul</translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation>Amarillo/Verde</translation>
+    </message>
+    <message>
+        <source>Orange</source>
+        <translation>Naranja</translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation>Amarillo</translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation>Rojo</translation>
+    </message>
+    <message>
+        <source>Pentobi failed to generate a move.</source>
+        <translation>Pentobi no pudo generar un movimiento.</translation>
+    </message>
+    <message>
+        <source>Press back again to exit</source>
+        <translation type="unfinished"/>
+    </message>
+</context>
+<context>
+    <name>MenuComputer</name>
+    <message>
+        <source>Computer</source>
+        <translation>Máquina</translation>
+    </message>
+</context>
+<context>
+    <name>MenuEdit</name>
+    <message>
+        <source>Edit</source>
+        <translation>Editar</translation>
+    </message>
+    <message>
+        <source>Make Main Variation</source>
+        <translation>Convertir en variación principal</translation>
+    </message>
+    <message>
+        <source>Variation Up</source>
+        <extracomment>Short for Move Variation Up</extracomment>
+        <translation>Subir variación</translation>
+    </message>
+    <message>
+        <source>Variation Down</source>
+        <extracomment>Short for Move Variation Down</extracomment>
+        <translation>Bajar variación</translation>
+    </message>
+    <message>
+        <source>Delete Variations</source>
+        <translation>Eliminar variaciones</translation>
+    </message>
+    <message>
+        <source>Truncate</source>
+        <translation>Eliminar</translation>
+    </message>
+    <message>
+        <source>Truncate Children</source>
+        <translation>Eliminar descendientes</translation>
+    </message>
+    <message>
+        <source>Keep Position</source>
+        <translation>Conservar posición</translation>
+    </message>
+    <message>
+        <source>Keep Subtree</source>
+        <translation>Conservar subárbol</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Modo configuración</translation>
+    </message>
+    <message>
+        <source>Next Color</source>
+        <translation>Siguiente color</translation>
+    </message>
+    <message>
+        <source>Annotation…</source>
+        <translation>Valoración...</translation>
+    </message>
+    <message>
+        <source>Made main variation</source>
+        <translation>Se ha convertido en variación principal</translation>
+    </message>
+</context>
+<context>
+    <name>MenuExport</name>
+    <message>
+        <source>Export</source>
+        <translation>Exportar</translation>
+    </message>
+    <message>
+        <source>Image…</source>
+        <translation>Imagen...</translation>
+    </message>
+    <message>
+        <source>ASCII Art…</source>
+        <translation>Arte ASCII...</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGame</name>
+    <message>
+        <source>Game</source>
+        <translation>Partida</translation>
+    </message>
+    <message>
+        <source>Game Variant…</source>
+        <translation>Variante de juego...</translation>
+    </message>
+    <message>
+        <source>Open Clipboard</source>
+        <translation>Abrir portapapeles</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGo</name>
+    <message>
+        <source>Go</source>
+        <translation>Ir a</translation>
+    </message>
+</context>
+<context>
+    <name>MenuHelp</name>
+    <message>
+        <source>Help</source>
+        <translation>Ayuda</translation>
+    </message>
+    <message>
+        <source>About Pentobi</source>
+        <translation>Acerca de Pentobi</translation>
+    </message>
+    <message>
+        <source>Report Bug</source>
+        <translation>Informe de errores</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>Mayús</translation>
+    </message>
+</context>
+<context>
+    <name>MenuRecentFiles</name>
+    <message>
+        <source>Open Recent</source>
+        <translation>Abrir reciente</translation>
+    </message>
+    <message>
+        <source>Clear List</source>
+        <extracomment>Menu item for clearing the recent files list</extracomment>
+        <translation>Borrar lista</translation>
+    </message>
+</context>
+<context>
+    <name>MenuTools</name>
+    <message>
+        <source>Tools</source>
+        <translation>Herramientas</translation>
+    </message>
+    <message>
+        <source>Rating</source>
+        <translation>Selección de nivel</translation>
+    </message>
+    <message>
+        <source>Clear Rating</source>
+        <translation>Borrar nivel</translation>
+    </message>
+    <message>
+        <source>Clear Analysis</source>
+        <translation>Borrar análisis</translation>
+    </message>
+    <message>
+        <source>Analyze Game…</source>
+        <translation>Analizar partida...</translation>
+    </message>
+</context>
+<context>
+    <name>MenuView</name>
+    <message>
+        <source>View</source>
+        <translation>Vista</translation>
+    </message>
+    <message>
+        <source>Appearance</source>
+        <translation>Apariencia</translation>
+    </message>
+    <message>
+        <source>Toolbar</source>
+        <translation>Barra de herramientas</translation>
+    </message>
+</context>
+<context>
+    <name>MoveAnnotationDialog</name>
+    <message>
+        <source>Move %1</source>
+        <translation>Movimiento %1</translation>
+    </message>
+    <message>
+        <source>Very good</source>
+        <translation>Muy bueno</translation>
+    </message>
+    <message>
+        <source>Good</source>
+        <translation>Bueno</translation>
+    </message>
+    <message>
+        <source>Interesting</source>
+        <translation>Interesante</translation>
+    </message>
+    <message>
+        <source>Doubtful</source>
+        <translation>Indeciso</translation>
+    </message>
+    <message>
+        <source>Bad</source>
+        <translation>Malo</translation>
+    </message>
+    <message>
+        <source>Very Bad</source>
+        <translation>Muy mal</translation>
+    </message>
+    <message>
+        <source>No annotation</source>
+        <translation>Sin valoración</translation>
+    </message>
+</context>
+<context>
+    <name>NewFolderDialog</name>
+    <message>
+        <source>Folder name:</source>
+        <translation>Nombre de la carpeta:</translation>
+    </message>
+</context>
+<context>
+    <name>OpenDialog</name>
+    <message>
+        <source>Open</source>
+        <translation>Abrir</translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation>Partidas de Blokus</translation>
+    </message>
+</context>
+<context>
+    <name>RatingDialog</name>
+    <message>
+        <source>Your rating:</source>
+        <translation>Su nivel:</translation>
+    </message>
+    <message>
+        <source>Game variant:</source>
+        <translation>Variante de juego:</translation>
+    </message>
+    <message>
+        <source>Classic (2)</source>
+        <extracomment>Short for Classic (2 players)</extracomment>
+        <translation>Clásico (2)</translation>
+    </message>
+    <message>
+        <source>Classic (3)</source>
+        <extracomment>Short for Classic (3 players)</extracomment>
+        <translation>Clásico (3)</translation>
+    </message>
+    <message>
+        <source>Classic (4)</source>
+        <extracomment>Short for Classic (4 players)</extracomment>
+        <translation>Clásico (4)</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>Dúo</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>Sencillo</translation>
+    </message>
+    <message>
+        <source>Trigon (2)</source>
+        <extracomment>Short for Trigon (2 players)</extracomment>
+        <translation>Trigón (2)</translation>
+    </message>
+    <message>
+        <source>Trigon (3)</source>
+        <extracomment>Short for Trigon (3 players)</extracomment>
+        <translation>Trigón (3)</translation>
+    </message>
+    <message>
+        <source>Trigon (4)</source>
+        <extracomment>Short for Trigon (4 players)</extracomment>
+        <translation>Trigón (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>Calisto (2)</translation>
+    </message>
+    <message>
+        <source>Callisto (2/4)</source>
+        <extracomment>Short for Callisto (2 players, 4 colors)</extracomment>
+        <translation>Calisto (2/4)</translation>
+    </message>
+    <message>
+        <source>Callisto (3)</source>
+        <extracomment>Short for Callisto (3 players)</extracomment>
+        <translation>Calisto (3)</translation>
+    </message>
+    <message>
+        <source>Callisto (4)</source>
+        <extracomment>Short for Callisto (4 players)</extracomment>
+        <translation>Calisto (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>Partidas con selección de nivel:</translation>
+    </message>
+    <message>
+        <source>Best previous rating:</source>
+        <translation>Mejor nivel anterior:</translation>
+    </message>
+    <message>
+        <source>Recent development:</source>
+        <translation>Novedad reciente:</translation>
+    </message>
+    <message>
+        <source>Open Game %1</source>
+        <translation>Abrir partida %1</translation>
+    </message>
+</context>
+<context>
+    <name>SaveDialog</name>
+    <message>
+        <source>Save</source>
+        <translation>Guardar</translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation>Partidas de Blokus</translation>
+    </message>
+</context>
+<context>
+    <name>TableModel</name>
+    <message>
+        <source>Game</source>
+        <extracomment>Table header for game number in rating dialog</extracomment>
+        <translation>Partida</translation>
+    </message>
+    <message>
+        <source>Result</source>
+        <extracomment>Table header for game result in rating dialog</extracomment>
+        <translation>Resultado</translation>
+    </message>
+    <message>
+        <source>Level</source>
+        <extracomment>Table header for level in rating dialog</extracomment>
+        <translation>Nivel</translation>
+    </message>
+    <message>
+        <source>Your Color</source>
+        <extracomment>Table header for player color(s) in rating dialog</extracomment>
+        <translation>Su color</translation>
+    </message>
+    <message>
+        <source>Date</source>
+        <extracomment>Table header for game date in rating dialog</extracomment>
+        <translation>Fecha</translation>
+    </message>
+    <message>
+        <source>Win</source>
+        <extracomment>Result of rated game is a win</extracomment>
+        <translation>Victoria</translation>
+    </message>
+    <message>
+        <source>Loss</source>
+        <extracomment>Result of rated game is a loss</extracomment>
+        <translation>Derrota</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>Empate</translation>
+    </message>
+</context>
+<context>
+    <name>ToolBar</name>
+    <message>
+        <source>Start a new game</source>
+        <translation>Comenzar una partida nueva</translation>
+    </message>
+    <message>
+        <source>Start a rated game</source>
+        <translation>Comenzar una partida con selección de nivel</translation>
+    </message>
+    <message>
+        <source>Set the colors played by the computer</source>
+        <translation>Elija los colores con los que jugará la máquina</translation>
+    </message>
+    <message>
+        <source>Make the computer continue to play the current color</source>
+        <translation>Haga que la máquina siga jugando con el color actual</translation>
+    </message>
+    <message>
+        <source>Make the computer play the current color</source>
+        <translation>Haga que la máquina juegue con el color actual</translation>
+    </message>
+    <message>
+        <source>Go to beginning of game</source>
+        <translation>Ir al inicio de la partida</translation>
+    </message>
+    <message>
+        <source>Go ten moves backward</source>
+        <translation>Retroceder diez movimientos</translation>
+    </message>
+    <message>
+        <source>Go one move backward</source>
+        <translation>Retroceder un movimiento</translation>
+    </message>
+    <message>
+        <source>Go one move forward</source>
+        <translation>Avanzar un movimiento</translation>
+    </message>
+    <message>
+        <source>Go ten moves forward</source>
+        <translation>Avanzar diez movimientos</translation>
+    </message>
+    <message>
+        <source>Go to end of moves</source>
+        <translation>Ir al final de los movimientos</translation>
+    </message>
+    <message>
+        <source>Go to previous variation</source>
+        <translation>Ir a la variación anterior</translation>
+    </message>
+    <message>
+        <source>Go to next variation</source>
+        <translation>Ir a la siguiente variación</translation>
+    </message>
+    <message>
+        <source>Abort game analysis</source>
+        <translation>Anular análisis de la partida</translation>
+    </message>
+    <message>
+        <source>Abort computer move</source>
+        <translation>Anular movimiento de la máquina</translation>
+    </message>
+    <message>
+        <source>Undo move</source>
+        <extracomment>Tooltip for Undo button</extracomment>
+        <translation>Deshacer movimiento</translation>
+    </message>
+    <message>
+        <source>Open menu</source>
+        <translation>Abrir menú</translation>
+    </message>
+</context>
+<context>
+    <name>main</name>
+    <message>
+        <source>computer opponent for the board game Blokus</source>
+        <translation>rival virtual para el juego de mesa Blokus</translation>
+    </message>
+    <message>
+        <source>Set maximum level to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --maxlevel</extracomment>
+        <translation>Establecer nivel máximo en &lt;n&gt;.</translation>
+    </message>
+    <message>
+        <source>Do not use opening books.</source>
+        <extracomment>Description for command line option --nobook</extracomment>
+        <translation>No utilizar abrir libros.</translation>
+    </message>
+    <message>
+        <source>Use layout optimized for smartphones.</source>
+        <extracomment>Description for command line option --mobile</extracomment>
+        <translation>Utilizar el diseño optimizado para móviles.</translation>
+    </message>
+    <message>
+        <source>Do not delay fast computer moves.</source>
+        <extracomment>Description for command line option --nodelay</extracomment>
+        <translation>No ralentizar los movimientos rápidos de la máquina.</translation>
+    </message>
+    <message>
+        <source>Set random seed to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --seed</extracomment>
+        <translation>Establecer valor de inicialización aleatorio en &lt;n&gt;.</translation>
+    </message>
+    <message>
+        <source>Use &lt;n&gt; threads (0=auto).</source>
+        <extracomment>Description for command line option --threads</extracomment>
+        <translation>Usar &lt;n&gt; subprocesos (0=auto).</translation>
+    </message>
+    <message>
+        <source>Print logging information to standard error.</source>
+        <extracomment>Description for command line option --verbose</extracomment>
+        <translation>Imprimir información de registro sobre el error típico.</translation>
+    </message>
+    <message>
+        <source>file.blksgf</source>
+        <extracomment>Name of command line argument.</extracomment>
+        <translation>file.blksgf</translation>
+    </message>
+    <message>
+        <source>Blokus SGF file to open (optional).</source>
+        <translation>Abrir archivo SGF de Blokus (opcional).</translation>
+    </message>
+    <message>
+        <source>--maxlevel must be between 1 and %1</source>
+        <translation>--nivel máximo debe estar entre 1 y %1</translation>
+    </message>
+    <message>
+        <source>--seed must be a positive number</source>
+        <translation>--el valor de inicialización tiene que ser un número positivo</translation>
+    </message>
+    <message>
+        <source>--threads must be a positive number</source>
+        <translation>--los subprocesos tienen que tener un valor positivo</translation>
+    </message>
+    <message>
+        <source>Too many arguments</source>
+        <translation>Demasiados argumentos</translation>
+    </message>
+</context>
+</TS>
\ No newline at end of file
diff --git a/pentobi/qml/i18n/qml_fr.ts b/pentobi/qml/i18n/qml_fr.ts
new file mode 100644 (file)
index 0000000..f1cfce2
--- /dev/null
@@ -0,0 +1,1434 @@
+<?xml version="1.0" ?><!DOCTYPE TS><TS language="fr" version="2.1">
+<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>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>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>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>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>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <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>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <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>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>Rouge gagne (égalité résolue).</translation>
+    </message>
+    <message>
+        <source>Yellow wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <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>GameViewDesktop</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>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>
+        <extracomment>Label for modified loaded game. The argument is the file name.</extracomment>
+        <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>
+    <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>Pentobi failed to generate a move.</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Press back again to exit</source>
+        <translation type="unfinished"/>
+    </message>
+</context>
+<context>
+    <name>MenuComputer</name>
+    <message>
+        <source>Computer</source>
+        <translation>Ordinateur</translation>
+    </message>
+</context>
+<context>
+    <name>MenuEdit</name>
+    <message>
+        <source>Edit</source>
+        <translation>Édition</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>Variation Down</source>
+        <extracomment>Short for Move Variation Down</extracomment>
+        <translation>Variation vers le bas</translation>
+    </message>
+    <message>
+        <source>Delete Variations</source>
+        <translation>Détruire les variations</translation>
+    </message>
+    <message>
+        <source>Truncate</source>
+        <translation>Couper</translation>
+    </message>
+    <message>
+        <source>Truncate Children</source>
+        <translation>Couper les branches filles</translation>
+    </message>
+    <message>
+        <source>Keep Position</source>
+        <translation>Garder la position</translation>
+    </message>
+    <message>
+        <source>Keep Subtree</source>
+        <translation>Garder le sous-arbre</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Position</translation>
+    </message>
+    <message>
+        <source>Next Color</source>
+        <translation>Couleur suivante</translation>
+    </message>
+    <message>
+        <source>Annotation…</source>
+        <translation>Annotation…</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>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>Game Variant…</source>
+        <translation>Variante du jeu…</translation>
+    </message>
+    <message>
+        <source>Open Clipboard</source>
+        <translation>Ouvrir le presse-papiers</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGo</name>
+    <message>
+        <source>Go</source>
+        <translation>Déplacement</translation>
+    </message>
+</context>
+<context>
+    <name>MenuHelp</name>
+    <message>
+        <source>Help</source>
+        <translation>Aide</translation>
+    </message>
+    <message>
+        <source>About Pentobi</source>
+        <translation>À propos de Pentobi</translation>
+    </message>
+    <message>
+        <source>Report Bug</source>
+        <translation>Rapportez une erreur</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>Clear List</source>
+        <extracomment>Menu item for clearing the recent files list</extracomment>
+        <translation>Effacer la liste</translation>
+    </message>
+</context>
+<context>
+    <name>MenuTools</name>
+    <message>
+        <source>Tools</source>
+        <translation>Outils</translation>
+    </message>
+    <message>
+        <source>Rating</source>
+        <translation>Classement</translation>
+    </message>
+    <message>
+        <source>Clear Rating</source>
+        <translation>Effacer le classement</translation>
+    </message>
+    <message>
+        <source>Clear Analysis</source>
+        <translation>Effacer l’analyse</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>Appearance</source>
+        <translation>Apparence</translation>
+    </message>
+    <message>
+        <source>Toolbar</source>
+        <translation>Barre d’outils</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>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>TableModel</name>
+    <message>
+        <source>Game</source>
+        <extracomment>Table header for game number in rating dialog</extracomment>
+        <translation>Partie</translation>
+    </message>
+    <message>
+        <source>Result</source>
+        <extracomment>Table header for game result in rating dialog</extracomment>
+        <translation>Résultat</translation>
+    </message>
+    <message>
+        <source>Level</source>
+        <extracomment>Table header for level in rating dialog</extracomment>
+        <translation>Niveau</translation>
+    </message>
+    <message>
+        <source>Your Color</source>
+        <extracomment>Table header for player color(s) in rating dialog</extracomment>
+        <translation>Votre couleur</translation>
+    </message>
+    <message>
+        <source>Date</source>
+        <extracomment>Table header for game date in rating dialog</extracomment>
+        <translation>Date</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>
+</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>Commencer 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>
+    <message>
+        <source>Open menu</source>
+        <translation>Ouvrir le menu</translation>
+    </message>
+</context>
+<context>
+    <name>main</name>
+    <message>
+        <source>computer opponent for the board game Blokus</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Set maximum level to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --maxlevel</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Do not use opening books.</source>
+        <extracomment>Description for command line option --nobook</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Use layout optimized for smartphones.</source>
+        <extracomment>Description for command line option --mobile</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Do not delay fast computer moves.</source>
+        <extracomment>Description for command line option --nodelay</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Set random seed to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --seed</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Use &lt;n&gt; threads (0=auto).</source>
+        <extracomment>Description for command line option --threads</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Print logging information to standard error.</source>
+        <extracomment>Description for command line option --verbose</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>file.blksgf</source>
+        <extracomment>Name of command line argument.</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Blokus SGF file to open (optional).</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>--maxlevel must be between 1 and %1</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>--seed must be a positive number</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>--threads must be a positive number</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Too many arguments</source>
+        <translation type="unfinished"/>
+    </message>
+</context>
+</TS>
\ No newline at end of file
diff --git a/pentobi/qml/i18n/qml_nb_NO.ts b/pentobi/qml/i18n/qml_nb_NO.ts
new file mode 100644 (file)
index 0000000..91822f8
--- /dev/null
@@ -0,0 +1,1434 @@
+<?xml version="1.0" ?><!DOCTYPE TS><TS language="nb_NO" version="2.1">
+<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>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>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>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>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>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <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>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <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>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>Rød vinner (uavgjort tilstand løst).</translation>
+    </message>
+    <message>
+        <source>Yellow wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <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>GameViewDesktop</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>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>
+        <extracomment>Label for modified loaded game. The argument is the file name.</extracomment>
+        <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>
+    <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>Pentobi failed to generate a move.</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Press back again to exit</source>
+        <translation type="unfinished"/>
+    </message>
+</context>
+<context>
+    <name>MenuComputer</name>
+    <message>
+        <source>Computer</source>
+        <translation>Datamaskin</translation>
+    </message>
+</context>
+<context>
+    <name>MenuEdit</name>
+    <message>
+        <source>Edit</source>
+        <translation>Rediger</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>Variation Down</source>
+        <extracomment>Short for Move Variation Down</extracomment>
+        <translation>Variasjon nedover</translation>
+    </message>
+    <message>
+        <source>Delete Variations</source>
+        <translation>Slett variasjoner</translation>
+    </message>
+    <message>
+        <source>Truncate</source>
+        <translation>Forkort</translation>
+    </message>
+    <message>
+        <source>Truncate Children</source>
+        <translation>Forkort underprosess</translation>
+    </message>
+    <message>
+        <source>Keep Position</source>
+        <translation>Behold posisjon</translation>
+    </message>
+    <message>
+        <source>Keep Subtree</source>
+        <translation>Behold undertreet</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Oppsettsmodus</translation>
+    </message>
+    <message>
+        <source>Next Color</source>
+        <translation>Neste farge</translation>
+    </message>
+    <message>
+        <source>Annotation…</source>
+        <translation>Anmerkning…</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>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>Game Variant…</source>
+        <translation>Spillvariant…</translation>
+    </message>
+    <message>
+        <source>Open Clipboard</source>
+        <translation>Åpne utklippstavle</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGo</name>
+    <message>
+        <source>Go</source>
+        <translation>Gå</translation>
+    </message>
+</context>
+<context>
+    <name>MenuHelp</name>
+    <message>
+        <source>Help</source>
+        <translation>Hjelp</translation>
+    </message>
+    <message>
+        <source>About Pentobi</source>
+        <translation>Om Pentobi</translation>
+    </message>
+    <message>
+        <source>Report Bug</source>
+        <translation>Innrapporter feil</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>Clear List</source>
+        <extracomment>Menu item for clearing the recent files list</extracomment>
+        <translation>Tøm liste</translation>
+    </message>
+</context>
+<context>
+    <name>MenuTools</name>
+    <message>
+        <source>Tools</source>
+        <translation>Verktøy</translation>
+    </message>
+    <message>
+        <source>Rating</source>
+        <translation>Vurdering</translation>
+    </message>
+    <message>
+        <source>Clear Rating</source>
+        <translation>Fjern vurdering</translation>
+    </message>
+    <message>
+        <source>Clear Analysis</source>
+        <translation>Tøm analyse</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>Appearance</source>
+        <translation>Utseende</translation>
+    </message>
+    <message>
+        <source>Toolbar</source>
+        <translation>Verktøyslinje</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>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>TableModel</name>
+    <message>
+        <source>Game</source>
+        <extracomment>Table header for game number in rating dialog</extracomment>
+        <translation>Spil</translation>
+    </message>
+    <message>
+        <source>Result</source>
+        <extracomment>Table header for game result in rating dialog</extracomment>
+        <translation>Resultat</translation>
+    </message>
+    <message>
+        <source>Level</source>
+        <extracomment>Table header for level in rating dialog</extracomment>
+        <translation>Nivå</translation>
+    </message>
+    <message>
+        <source>Your Color</source>
+        <extracomment>Table header for player color(s) in rating dialog</extracomment>
+        <translation>Din farge</translation>
+    </message>
+    <message>
+        <source>Date</source>
+        <extracomment>Table header for game date in rating dialog</extracomment>
+        <translation>Dato</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>
+</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>
+    <message>
+        <source>Open menu</source>
+        <translation type="unfinished"/>
+    </message>
+</context>
+<context>
+    <name>main</name>
+    <message>
+        <source>computer opponent for the board game Blokus</source>
+        <translation>datamaskinsmotstander for brettspillet Blokus</translation>
+    </message>
+    <message>
+        <source>Set maximum level to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --maxlevel</extracomment>
+        <translation>Sett maksimumsnivå til &lt;n&gt;.</translation>
+    </message>
+    <message>
+        <source>Do not use opening books.</source>
+        <extracomment>Description for command line option --nobook</extracomment>
+        <translation>Ikke bruk åpningsbøker.</translation>
+    </message>
+    <message>
+        <source>Use layout optimized for smartphones.</source>
+        <extracomment>Description for command line option --mobile</extracomment>
+        <translation>Bruk visning optimalisert for smarttelefoner.</translation>
+    </message>
+    <message>
+        <source>Do not delay fast computer moves.</source>
+        <extracomment>Description for command line option --nodelay</extracomment>
+        <translation>Ikke forsink raske datamaskintrekk.</translation>
+    </message>
+    <message>
+        <source>Set random seed to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --seed</extracomment>
+        <translation>Sett tilfeldig seeding til &lt;n&gt;.</translation>
+    </message>
+    <message>
+        <source>Use &lt;n&gt; threads (0=auto).</source>
+        <extracomment>Description for command line option --threads</extracomment>
+        <translation>Bruk &lt;n&gt; tråder (0=auto).</translation>
+    </message>
+    <message>
+        <source>Print logging information to standard error.</source>
+        <extracomment>Description for command line option --verbose</extracomment>
+        <translation>Skriv loggingsinfo til forvalg for feil.</translation>
+    </message>
+    <message>
+        <source>file.blksgf</source>
+        <extracomment>Name of command line argument.</extracomment>
+        <translation>fil.blksgf</translation>
+    </message>
+    <message>
+        <source>Blokus SGF file to open (optional).</source>
+        <translation>Blokus SGF-fil å åpne (valgfritt).</translation>
+    </message>
+    <message>
+        <source>--maxlevel must be between 1 and %1</source>
+        <translation>--maxlevel må være mellom 1 og %1</translation>
+    </message>
+    <message>
+        <source>--seed must be a positive number</source>
+        <translation>--seed må være et positivt tall</translation>
+    </message>
+    <message>
+        <source>--threads must be a positive number</source>
+        <translation>--treads må være et positivt tall</translation>
+    </message>
+    <message>
+        <source>Too many arguments</source>
+        <translation>For mange argumenter</translation>
+    </message>
+</context>
+</TS>
\ No newline at end of file
diff --git a/pentobi/qml/i18n/qml_ru.ts b/pentobi/qml/i18n/qml_ru.ts
new file mode 100644 (file)
index 0000000..b26d5f4
--- /dev/null
@@ -0,0 +1,1430 @@
+<?xml version="1.0" ?><!DOCTYPE TS><TS language="ru" version="2.1">
+<context>
+    <name>AboutDialog</name>
+    <message>
+        <source>Copyright © 2011–%1 Markus Enzenberger</source>
+        <translation>Авторское право © 2011–%1 Markus Enzenberger</translation>
+    </message>
+    <message>
+        <source>Computer opponent for the board game Blokus</source>
+        <translation>Оппонент компьютерной настольной игры Блокус</translation>
+    </message>
+    <message>
+        <source>Pentobi %1</source>
+        <extracomment>The argument is the application version.</extracomment>
+        <translation>Pentobi %1</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeDialog</name>
+    <message>
+        <source>Analysis speed:</source>
+        <translation>Скорость анализа:</translation>
+    </message>
+    <message>
+        <source>Fast</source>
+        <translation>Быстро</translation>
+    </message>
+    <message>
+        <source>Normal</source>
+        <translation>Нормально</translation>
+    </message>
+    <message>
+        <source>Slow</source>
+        <translation>Медленно</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeGame</name>
+    <message>
+        <source>(No analysis)</source>
+        <translation>(Без анализа)</translation>
+    </message>
+</context>
+<context>
+    <name>AppearanceDialog</name>
+    <message>
+        <source>Coordinates</source>
+        <translation>Координаты</translation>
+    </message>
+    <message>
+        <source>Show variations</source>
+        <translation>Показать варианты</translation>
+    </message>
+    <message>
+        <source>Light</source>
+        <translation>Светлая</translation>
+    </message>
+    <message>
+        <source>Dark</source>
+        <translation>Темная</translation>
+    </message>
+    <message>
+        <source>Colorblind light</source>
+        <translation>Цветовая слепота светлая</translation>
+    </message>
+    <message>
+        <source>Colorblind dark</source>
+        <translation>Цветовая слепота темная</translation>
+    </message>
+    <message>
+        <source>System</source>
+        <extracomment>Name of theme using default system colors</extracomment>
+        <translation>Системная</translation>
+    </message>
+    <message>
+        <source>Move marking:</source>
+        <translation>Отметка хода:</translation>
+    </message>
+    <message>
+        <source>Last with dot</source>
+        <translation>Последний с точкой</translation>
+    </message>
+    <message>
+        <source>Last with number</source>
+        <translation>Последний с номером</translation>
+    </message>
+    <message>
+        <source>All with number</source>
+        <translation>Все с номерами</translation>
+    </message>
+    <message>
+        <source>None</source>
+        <extracomment>Move marking/None</extracomment>
+        <translation>Нет</translation>
+    </message>
+    <message>
+        <source>Animations</source>
+        <translation>Анимации</translation>
+    </message>
+    <message>
+        <source>Show comment:</source>
+        <translation>Показать комментарий:</translation>
+    </message>
+    <message>
+        <source>Always</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Всегда</translation>
+    </message>
+    <message>
+        <source>As needed</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>По необходимости</translation>
+    </message>
+    <message>
+        <source>Never</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>Никогда</translation>
+    </message>
+    <message>
+        <source>Color theme:</source>
+        <translation>Цветовая тема:</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>Номер хода</translation>
+    </message>
+</context>
+<context>
+    <name>AsciiArtSaveDialog</name>
+    <message>
+        <source>Export ASCII Art</source>
+        <translation>Экспортировать ASCII</translation>
+    </message>
+    <message>
+        <source>Text files</source>
+        <translation>Текстовый файл</translation>
+    </message>
+</context>
+<context>
+    <name>BoardContextMenu</name>
+    <message>
+        <source>Go to Move %1</source>
+        <translation>Перейти к ходу %1</translation>
+    </message>
+    <message>
+        <source>Move Annotation</source>
+        <translation>Пояснение Хода</translation>
+    </message>
+    <message>
+        <source>Move Annotation (%1)</source>
+        <extracomment>The argument is the annotation symbol for the current move</extracomment>
+        <translation>Пояснение Хода (%1)</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonApply</name>
+    <message>
+        <source>Apply</source>
+        <translation>Применить</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonCancel</name>
+    <message>
+        <source>Cancel</source>
+        <translation>Отмена</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonClose</name>
+    <message>
+        <source>Close</source>
+        <translation>Закрыть</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>Играть Компьютеру:</translation>
+    </message>
+    <message>
+        <source>Level %1</source>
+        <translation>Уровень %1</translation>
+    </message>
+</context>
+<context>
+    <name>ExportImageDialog</name>
+    <message>
+        <source>Image width:</source>
+        <translation>Ширина изображения:</translation>
+    </message>
+</context>
+<context>
+    <name>FileDialog</name>
+    <message>
+        <source>Overwrite file?</source>
+        <translation>Перезаписать файл?</translation>
+    </message>
+    <message>
+        <source>Open</source>
+        <translation>Открыть</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>Сохранить</translation>
+    </message>
+    <message>
+        <source>All files</source>
+        <translation>Все файлы</translation>
+    </message>
+</context>
+<context>
+    <name>GameInfoDialog</name>
+    <message>
+        <source>Player Blue/Red:</source>
+        <translation>Игрок Синий/Красный:</translation>
+    </message>
+    <message>
+        <source>Player Purple:</source>
+        <translation>Игрок Фиолетовый:</translation>
+    </message>
+    <message>
+        <source>Player Green:</source>
+        <translation>Игрок Зеленый:</translation>
+    </message>
+    <message>
+        <source>Player Blue:</source>
+        <translation>Игрок Синий:</translation>
+    </message>
+    <message>
+        <source>Player Yellow/Green:</source>
+        <translation>Игрок Желтый/Зеленый:</translation>
+    </message>
+    <message>
+        <source>Player Orange:</source>
+        <translation>Игрок Оранжевый:</translation>
+    </message>
+    <message>
+        <source>Player Yellow:</source>
+        <translation>Игрок Желтый:</translation>
+    </message>
+    <message>
+        <source>Player Red:</source>
+        <translation>Игрок Красный:</translation>
+    </message>
+    <message>
+        <source>Date:</source>
+        <translation>Дата:</translation>
+    </message>
+    <message>
+        <source>Time:</source>
+        <translation>Время:</translation>
+    </message>
+    <message>
+        <source>Event:</source>
+        <translation>Событие:</translation>
+    </message>
+    <message>
+        <source>Round:</source>
+        <translation>Раунд:</translation>
+    </message>
+</context>
+<context>
+    <name>GameModel</name>
+    <message>
+        <source>Purple wins with 1 point.</source>
+        <translation>Фиолетовый выигрывает с 1 очком.</translation>
+    </message>
+    <message>
+        <source>Purple wins with %L1 points.</source>
+        <translation>Фиолетовый выигрывает с %L1 очками.</translation>
+    </message>
+    <message>
+        <source>Orange wins with 1 point.</source>
+        <translation>Оранжевый выигрывает с 1 очком.</translation>
+    </message>
+    <message>
+        <source>Orange wins with %L1 points.</source>
+        <translation>Оранжевый выигрывает с %L1 очками.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie.</source>
+        <translation>Игра заканчивается ничьей.</translation>
+    </message>
+    <message>
+        <source>Green wins with 1 point.</source>
+        <translation>Зеленый выигрывает с 1 очком.</translation>
+    </message>
+    <message>
+        <source>Green wins with %L1 points.</source>
+        <translation>Зеленый выигрывает с %L1 очками.</translation>
+    </message>
+    <message>
+        <source>Blue wins with 1 point.</source>
+        <translation>Синий выигрывает с 1 очком.</translation>
+    </message>
+    <message>
+        <source>Blue wins with %L1 points.</source>
+        <translation>Синий выигрывает с %L1 очками.</translation>
+    </message>
+    <message>
+        <source>Green wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>Зеленый побеждает (ничья).</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with 1 point.</source>
+        <translation>Синий/Красный выигрывают с 1 очком.</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with %L1 points.</source>
+        <translation>Синий/Красный выигрывают с %L1 очками.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with 1 point.</source>
+        <translation>Желтый/Зеленый выигрывают с 1 очком.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with %L1 points.</source>
+        <translation>Желтый/Зеленый выигрывают с %L1 очками.</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>Желтый/Зеленый побеждают (ничья).</translation>
+    </message>
+    <message>
+        <source>Blue wins.</source>
+        <translation>Синий побеждает.</translation>
+    </message>
+    <message>
+        <source>Yellow wins.</source>
+        <translation>Желтый побеждает.</translation>
+    </message>
+    <message>
+        <source>Red wins.</source>
+        <translation>Красный побеждает.</translation>
+    </message>
+    <message>
+        <source>Red wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>Красный побеждает (ничья).</translation>
+    </message>
+    <message>
+        <source>Yellow wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>Желтый побеждает (ничья).</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Yellow.</source>
+        <translation>Игра заканчивается ничьей между Синим и Желтым.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Red.</source>
+        <translation>Игра заканчивается ничьей между Синим и Красными.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow and Red.</source>
+        <translation>Игра заканчивается ничьей между Желтыми и Красными.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between all players.</source>
+        <translation>Игра заканчивается ничьей между всеми игроками.</translation>
+    </message>
+    <message>
+        <source>Green wins.</source>
+        <translation>Зеленый побеждает.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Red.</source>
+        <translation>Игра заканчивается ничьей между Синим, Желтым и Красным.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Green.</source>
+        <translation>Игра заканчивается ничьей между Синим, Желтым и Зеленым.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Red and Green.</source>
+        <translation>Игра заканчивается ничьей между Синим, Красным и Зеленым.</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow, Red and Green.</source>
+        <translation>Игра заканчивается ничьей между Желтым, Красным и Зеленым.</translation>
+    </message>
+    <message>
+        <source>Invalid Blokus SGF file. (%1)</source>
+        <translation>Неверный файл Блокус SGF. (%1)</translation>
+    </message>
+    <message>
+        <source>Clipboard is empty.</source>
+        <translation>Буфер обмена пуст.</translation>
+    </message>
+    <message>
+        <source>Untitled</source>
+        <translation>Без названия</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>Без названия %1</translation>
+    </message>
+    <message>
+        <source>New Folder</source>
+        <translation>Новый Каталог</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>Новый Каталог %1</translation>
+    </message>
+    <message>
+        <source>(Setup)</source>
+        <translation>(Настройка)</translation>
+    </message>
+    <message>
+        <source>(No moves)</source>
+        <translation>(Нет ходов)</translation>
+    </message>
+    <message>
+        <source>Move %1</source>
+        <extracomment>The argument is the current move number.</extracomment>
+        <translation>Ход %1</translation>
+    </message>
+    <message>
+        <source>Unsupported character set</source>
+        <translation>Неподдерживаемый набор символов</translation>
+    </message>
+</context>
+<context>
+    <name>GameVariantDialog</name>
+    <message>
+        <source>Classic</source>
+        <translation>Классическая</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>Два игрока</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>Юниор</translation>
+    </message>
+    <message>
+        <source>Trigon</source>
+        <translation>Тригон</translation>
+    </message>
+    <message>
+        <source>Nexos</source>
+        <translation>Нексос</translation>
+    </message>
+    <message>
+        <source>GembloQ</source>
+        <translation>ГемблоQ</translation>
+    </message>
+    <message>
+        <source>Callisto</source>
+        <translation>Каллисто</translation>
+    </message>
+    <message>
+        <source>Players:</source>
+        <translation>Игроки:</translation>
+    </message>
+    <message>
+        <source>Colors:</source>
+        <translation>Цвета:</translation>
+    </message>
+</context>
+<context>
+    <name>GameViewDesktop</name>
+    <message>
+        <source>Computer is thinking…</source>
+        <translation>Компьютер думает…</translation>
+    </message>
+    <message>
+        <source>Running game analysis…</source>
+        <translation>Начать анализ игры…</translation>
+    </message>
+    <message>
+        <source>Computer is thinking… (up to %1 seconds remaining)</source>
+        <translation>Компьютер думает… (осталось %1 секунд)</translation>
+    </message>
+    <message>
+        <source>Computer is thinking… (up to %1 minutes remaining)</source>
+        <translation>Компьютер думает… (осталось %1 минут)</translation>
+    </message>
+</context>
+<context>
+    <name>GotoMoveDialog</name>
+    <message>
+        <source>Move number:</source>
+        <translation>Номер хода:</translation>
+    </message>
+</context>
+<context>
+    <name>HelpWindow</name>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Помощь</translation>
+    </message>
+</context>
+<context>
+    <name>ImageSaveDialog</name>
+    <message>
+        <source>Save Image</source>
+        <translation>Сохранить изображение</translation>
+    </message>
+    <message>
+        <source>PNG image files</source>
+        <translation>Файлы PNG</translation>
+    </message>
+    <message>
+        <source>JPEG image files</source>
+        <translation>Файлы JPEG</translation>
+    </message>
+</context>
+<context>
+    <name>InitialRatingDialog</name>
+    <message>
+        <source>Initialize your rating for this game variant.</source>
+        <translation>Выберете свой рейтинг для этой игры.</translation>
+    </message>
+    <message>
+        <source>Initial rating:</source>
+        <translation>Начальный рейтинг:</translation>
+    </message>
+    <message>
+        <source>Beginner</source>
+        <translation>Начинающий</translation>
+    </message>
+    <message>
+        <source>Expert</source>
+        <translation>Эксперт</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>Анализ игры возможен только в основном варианте.</translation>
+    </message>
+    <message>
+        <source>Autosaved game was changed by another instance of Pentobi. Overwrite?</source>
+        <translation>Автосохраненная игра была изменена другим экземпляром Pentobi. Переписать?</translation>
+    </message>
+    <message>
+        <source>Your rating has increased from %1 to %2.</source>
+        <translation>Ваш рейтинг вырос с %1 на %2.</translation>
+    </message>
+    <message>
+        <source>Your rating has decreased from %1 to %2.</source>
+        <translation>Ваш рейтинг снизился с %1 на %2.</translation>
+    </message>
+    <message>
+        <source>Your rating stays at %1.</source>
+        <translation>Ваш рейтинг остается на уровне %1.</translation>
+    </message>
+    <message>
+        <source>No permission to access storage</source>
+        <translation>Нет разрешения на доступ к хранилищу</translation>
+    </message>
+    <message>
+        <source>Delete all rating information for the current game variant?</source>
+        <translation>Удалить всю информацию рейтинга для текущей игры?</translation>
+    </message>
+    <message>
+        <source>Delete all variations?</source>
+        <translation>Удалить все варианты?</translation>
+    </message>
+    <message>
+        <source>Save failed.</source>
+        <translation>Сохранить не удалось.</translation>
+    </message>
+    <message>
+        <source>End of tree was reached. Continue search from start of the tree?</source>
+        <translation>Достигнут конец дерева. Продолжить поиск с начала дерева?</translation>
+    </message>
+    <message>
+        <source>No comment found</source>
+        <translation>Комментариев не найдено</translation>
+    </message>
+    <message>
+        <source>%1 (modified)</source>
+        <extracomment>Label for modified loaded game. The argument is the file name.</extracomment>
+        <translation>%1 (измененная)</translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Reload?</source>
+        <translation>Файл был изменен другим приложением. Перезапустить?</translation>
+    </message>
+    <message>
+        <source>Continue computer move?</source>
+        <translation>Передать ход компьютеру?</translation>
+    </message>
+    <message>
+        <source>Keep only position?</source>
+        <translation>Сохранить только позицию?</translation>
+    </message>
+    <message>
+        <source>Keep only subtree?</source>
+        <translation>Сохранить только поддерево?</translation>
+    </message>
+    <message>
+        <source>Open failed.</source>
+        <translation>Открыть не удалось.</translation>
+    </message>
+    <message>
+        <source>Start rated game with Purple against Pentobi level %1?</source>
+        <translation>Начать игру Фиолетовым против Pentobi уровня %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Green against Pentobi level %1?</source>
+        <translation>Начать игру Зеленым против Pentobi уровня %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue/Red against Pentobi level %1?</source>
+        <translation>Начать игру Синим/Красным против Pentobi уровня %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue against Pentobi level %1?</source>
+        <translation>Начать игру Синим против Pentobi уровня %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Orange against Pentobi level %1?</source>
+        <translation>Начать игру Оранжевым против Pentobi уровня %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow/Green against Pentobi level %1?</source>
+        <translation>Начать игру Желтым/Зеленым против Pentobi уровня %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow against Pentobi level %1?</source>
+        <translation>Начать игру Желтым против Pentobi уровня %1?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Red against Pentobi level %1?</source>
+        <translation>Начать игру Красным против Pentobi уровня %1?</translation>
+    </message>
+    <message>
+        <source>You have not yet played rated games in this game variant.</source>
+        <translation>Вы еще не играли в рейтинговые игры в этом варианте игры.</translation>
+    </message>
+    <message>
+        <source>Truncate this subtree?</source>
+        <translation>Защитить это поддерево?</translation>
+    </message>
+    <message>
+        <source>Truncate children?</source>
+        <translation>Защитить от детей?</translation>
+    </message>
+    <message>
+        <source>Discard game?</source>
+        <translation>Сбросить игру?</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 (уровень %2)</translation>
+    </message>
+    <message>
+        <source>Human</source>
+        <extracomment>Player name for game info in rated game.</extracomment>
+        <translation>Человек</translation>
+    </message>
+    <message>
+        <source>Rated game</source>
+        <translation>Рэйтинг</translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Overwrite?</source>
+        <translation>Файл был изменен другим приложением. Перезаписать?</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>Недостаточно памяти</translation>
+    </message>
+    <message>
+        <source>Game analysis aborted</source>
+        <translation>Анализ игры прерван</translation>
+    </message>
+    <message>
+        <source>Computer move aborted</source>
+        <translation>Ход Компьютера прерван</translation>
+    </message>
+    <message>
+        <source>Rating information deleted</source>
+        <translation>Удаление информации рейтинга</translation>
+    </message>
+    <message>
+        <source>Variations deleted</source>
+        <translation>Варианты удалены</translation>
+    </message>
+    <message>
+        <source>File saved</source>
+        <translation>Файл сохранен</translation>
+    </message>
+    <message>
+        <source>Saving image failed or unsupported image format</source>
+        <translation>Ошибка сохранения или неподдерживаемый формат изображения</translation>
+    </message>
+    <message>
+        <source>Image saved</source>
+        <translation>Изображение сохранено</translation>
+    </message>
+    <message>
+        <source>Creating image failed</source>
+        <translation>Не удалось создать изображение</translation>
+    </message>
+    <message>
+        <source>Continuing rated game</source>
+        <translation>Продолжение рейтинговой игры</translation>
+    </message>
+    <message>
+        <source>Kept only position</source>
+        <translation>Сохранять только позицию</translation>
+    </message>
+    <message>
+        <source>Kept only subtree</source>
+        <translation>Сохранять только поддерево</translation>
+    </message>
+    <message>
+        <source>Variation is now %1</source>
+        <translation>Текущий вариант %1</translation>
+    </message>
+    <message>
+        <source>Children truncated</source>
+        <translation>Защищено от детей</translation>
+    </message>
+    <message>
+        <source>Setup</source>
+        <extracomment>Small-screen label for setup mode (short for &quot;Setup Mode&quot;).</extracomment>
+        <translation>Настроить</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Режим настройки</translation>
+    </message>
+    <message>
+        <source>Rated</source>
+        <extracomment>Label for ongoing rated game</extracomment>
+        <translation>Рэйтинг</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>Рэйтинг %1</translation>
+    </message>
+    <message>
+        <source>Rated Game %1</source>
+        <extracomment>Label for rated game. The argument is the game number.</extracomment>
+        <translation>Рэйтинговая игра %1</translation>
+    </message>
+    <message>
+        <source>Main Variation</source>
+        <translation>Основной вариант</translation>
+    </message>
+    <message>
+        <source>Beginning of Branch</source>
+        <translation>Начало Ветки</translation>
+    </message>
+    <message>
+        <source>Comment</source>
+        <translation>Комментарий</translation>
+    </message>
+    <message>
+        <source>Settings</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation>Настройки</translation>
+    </message>
+    <message>
+        <source>Find Move</source>
+        <translation>Подсказать ход</translation>
+    </message>
+    <message>
+        <source>Next Comment</source>
+        <translation>Следующий комментарий</translation>
+    </message>
+    <message>
+        <source>Fullscreen</source>
+        <translation>На весь экран</translation>
+    </message>
+    <message>
+        <source>Game Info</source>
+        <translation>Информация Об Игре</translation>
+    </message>
+    <message>
+        <source>Move Number…</source>
+        <translation>Номер хода...</translation>
+    </message>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Помощь</translation>
+    </message>
+    <message>
+        <source>New</source>
+        <translation>Новая</translation>
+    </message>
+    <message>
+        <source>Rated Game</source>
+        <translation>Рэйтинг игра</translation>
+    </message>
+    <message>
+        <source>Open…</source>
+        <translation>Открыть…</translation>
+    </message>
+    <message>
+        <source>Play</source>
+        <translation>Игра</translation>
+    </message>
+    <message>
+        <source>Play Move</source>
+        <extracomment>Play a single move</extracomment>
+        <translation>Воспроизвести ход</translation>
+    </message>
+    <message>
+        <source>Quit</source>
+        <translation>Выход</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>Сохранить</translation>
+    </message>
+    <message>
+        <source>Save As…</source>
+        <translation>Сохранить как…</translation>
+    </message>
+    <message>
+        <source>Stop</source>
+        <translation>Стоп</translation>
+    </message>
+    <message>
+        <source>Undo Move</source>
+        <translation>Отменить Ход</translation>
+    </message>
+    <message>
+        <source>Blue/Red</source>
+        <translation>Синий/Красный</translation>
+    </message>
+    <message>
+        <source>Purple</source>
+        <translation>Фиолетовый</translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation>Зеленый</translation>
+    </message>
+    <message>
+        <source>Blue</source>
+        <translation>Синий</translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation>Желтый/Зеленый</translation>
+    </message>
+    <message>
+        <source>Orange</source>
+        <translation>Оранжевый</translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation>Желтый</translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation>Красный</translation>
+    </message>
+    <message>
+        <source>Pentobi failed to generate a move.</source>
+        <translation>Pentobi не сумел сгенерировать ход.</translation>
+    </message>
+    <message>
+        <source>Press back again to exit</source>
+        <translation>Нажмите назад еще раз, чтобы выйти</translation>
+    </message>
+</context>
+<context>
+    <name>MenuComputer</name>
+    <message>
+        <source>Computer</source>
+        <translation>Компьютер</translation>
+    </message>
+</context>
+<context>
+    <name>MenuEdit</name>
+    <message>
+        <source>Edit</source>
+        <translation>Редактировать</translation>
+    </message>
+    <message>
+        <source>Make Main Variation</source>
+        <translation>Сделать основным вариантом</translation>
+    </message>
+    <message>
+        <source>Variation Up</source>
+        <extracomment>Short for Move Variation Up</extracomment>
+        <translation>Вариант вверх</translation>
+    </message>
+    <message>
+        <source>Variation Down</source>
+        <extracomment>Short for Move Variation Down</extracomment>
+        <translation>Вариант вниз</translation>
+    </message>
+    <message>
+        <source>Delete Variations</source>
+        <translation>Удалить варианты</translation>
+    </message>
+    <message>
+        <source>Truncate</source>
+        <translation>Защита</translation>
+    </message>
+    <message>
+        <source>Truncate Children</source>
+        <translation>Защита от детей</translation>
+    </message>
+    <message>
+        <source>Keep Position</source>
+        <translation>Сохранить Позицию</translation>
+    </message>
+    <message>
+        <source>Keep Subtree</source>
+        <translation>Сохранить Поддерево</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>Режим Настройки</translation>
+    </message>
+    <message>
+        <source>Next Color</source>
+        <translation>Следующий Цвет</translation>
+    </message>
+    <message>
+        <source>Annotation…</source>
+        <translation>Пояснение…</translation>
+    </message>
+    <message>
+        <source>Made main variation</source>
+        <translation>Основные изменения</translation>
+    </message>
+</context>
+<context>
+    <name>MenuExport</name>
+    <message>
+        <source>Export</source>
+        <translation>Экспорт</translation>
+    </message>
+    <message>
+        <source>Image…</source>
+        <translation>Изображение…</translation>
+    </message>
+    <message>
+        <source>ASCII Art…</source>
+        <translation>ASCII…</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGame</name>
+    <message>
+        <source>Game</source>
+        <translation>Игра</translation>
+    </message>
+    <message>
+        <source>Game Variant…</source>
+        <translation>Вариант игры...</translation>
+    </message>
+    <message>
+        <source>Open Clipboard</source>
+        <translation>Открыть Буфер обмена</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGo</name>
+    <message>
+        <source>Go</source>
+        <translation>Ход</translation>
+    </message>
+</context>
+<context>
+    <name>MenuHelp</name>
+    <message>
+        <source>Help</source>
+        <translation>Помощь</translation>
+    </message>
+    <message>
+        <source>About Pentobi</source>
+        <translation>О Pentobi</translation>
+    </message>
+    <message>
+        <source>Report Bug</source>
+        <translation>Сообщить об ошибке</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>Открыть Недавние</translation>
+    </message>
+    <message>
+        <source>Clear List</source>
+        <extracomment>Menu item for clearing the recent files list</extracomment>
+        <translation>Очистить Список</translation>
+    </message>
+</context>
+<context>
+    <name>MenuTools</name>
+    <message>
+        <source>Tools</source>
+        <translation>Инструменты</translation>
+    </message>
+    <message>
+        <source>Rating</source>
+        <translation>Рейтинг</translation>
+    </message>
+    <message>
+        <source>Clear Rating</source>
+        <translation>Очистить Рейтинг</translation>
+    </message>
+    <message>
+        <source>Clear Analysis</source>
+        <translation>Очистить Анализ</translation>
+    </message>
+    <message>
+        <source>Analyze Game…</source>
+        <translation>Анализировать игру…</translation>
+    </message>
+</context>
+<context>
+    <name>MenuView</name>
+    <message>
+        <source>View</source>
+        <translation>Просмотр</translation>
+    </message>
+    <message>
+        <source>Appearance</source>
+        <translation>Внешний вид</translation>
+    </message>
+    <message>
+        <source>Toolbar</source>
+        <translation>Панель инструментов</translation>
+    </message>
+</context>
+<context>
+    <name>MoveAnnotationDialog</name>
+    <message>
+        <source>Move %1</source>
+        <translation>Ход %1</translation>
+    </message>
+    <message>
+        <source>Very good</source>
+        <translation>Очень хорошо</translation>
+    </message>
+    <message>
+        <source>Good</source>
+        <translation>Хорошо</translation>
+    </message>
+    <message>
+        <source>Interesting</source>
+        <translation>Интересно</translation>
+    </message>
+    <message>
+        <source>Doubtful</source>
+        <translation>Сомнительно</translation>
+    </message>
+    <message>
+        <source>Bad</source>
+        <translation>Плохо</translation>
+    </message>
+    <message>
+        <source>Very Bad</source>
+        <translation>Очень плохо</translation>
+    </message>
+    <message>
+        <source>No annotation</source>
+        <translation>Нет аннотации</translation>
+    </message>
+</context>
+<context>
+    <name>NewFolderDialog</name>
+    <message>
+        <source>Folder name:</source>
+        <translation>Имя каталога:</translation>
+    </message>
+</context>
+<context>
+    <name>OpenDialog</name>
+    <message>
+        <source>Open</source>
+        <translation>Открыть</translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation>Игры Блокус</translation>
+    </message>
+</context>
+<context>
+    <name>RatingDialog</name>
+    <message>
+        <source>Your rating:</source>
+        <translation>Ваш рейтинг:</translation>
+    </message>
+    <message>
+        <source>Game variant:</source>
+        <translation>Вариант игры:</translation>
+    </message>
+    <message>
+        <source>Classic (2)</source>
+        <extracomment>Short for Classic (2 players)</extracomment>
+        <translation>Классическая (2)</translation>
+    </message>
+    <message>
+        <source>Classic (3)</source>
+        <extracomment>Short for Classic (3 players)</extracomment>
+        <translation>Классическая (3)</translation>
+    </message>
+    <message>
+        <source>Classic (4)</source>
+        <extracomment>Short for Classic (4 players)</extracomment>
+        <translation>Классическая (4)</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>Два игрока</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>Юниор</translation>
+    </message>
+    <message>
+        <source>Trigon (2)</source>
+        <extracomment>Short for Trigon (2 players)</extracomment>
+        <translation>Тригон (2)</translation>
+    </message>
+    <message>
+        <source>Trigon (3)</source>
+        <extracomment>Short for Trigon (3 players)</extracomment>
+        <translation>Тригон (3)</translation>
+    </message>
+    <message>
+        <source>Trigon (4)</source>
+        <extracomment>Short for Trigon (4 players)</extracomment>
+        <translation>Тригон (4)</translation>
+    </message>
+    <message>
+        <source>Nexos (2)</source>
+        <extracomment>Short for Nexos (2 players)</extracomment>
+        <translation>Нексос (2)</translation>
+    </message>
+    <message>
+        <source>Nexos (4)</source>
+        <extracomment>Short for Nexos (4 players)</extracomment>
+        <translation>Нексос (4)</translation>
+    </message>
+    <message>
+        <source>Callisto (2)</source>
+        <extracomment>Short for Callisto (2 players, 2 colors)</extracomment>
+        <translation>Каллисто (2)</translation>
+    </message>
+    <message>
+        <source>Callisto (2/4)</source>
+        <extracomment>Short for Callisto (2 players, 4 colors)</extracomment>
+        <translation>Каллисто (2/4)</translation>
+    </message>
+    <message>
+        <source>Callisto (3)</source>
+        <extracomment>Short for Callisto (3 players)</extracomment>
+        <translation>Каллисто (3)</translation>
+    </message>
+    <message>
+        <source>Callisto (4)</source>
+        <extracomment>Short for Callisto (4 players)</extracomment>
+        <translation>Каллисто (4)</translation>
+    </message>
+    <message>
+        <source>GembloQ (4)</source>
+        <extracomment>Short for GembloQ (4 players)</extracomment>
+        <translation>ГемблоQ (4)</translation>
+    </message>
+    <message>
+        <source>GembloQ (2)</source>
+        <extracomment>Short for GembloQ (2 players, 2 colors)</extracomment>
+        <translation>ГемблоQ (2)</translation>
+    </message>
+    <message>
+        <source>GembloQ (2/4)</source>
+        <extracomment>Short for GembloQ (2 players, 4 colors)</extracomment>
+        <translation>ГемблоQ (2/4)</translation>
+    </message>
+    <message>
+        <source>GembloQ (3)</source>
+        <extracomment>Short for GembloQ (3 players)</extracomment>
+        <translation>ГемблоQ (3)</translation>
+    </message>
+    <message>
+        <source>Rated games:</source>
+        <translation>Рэйтинг:</translation>
+    </message>
+    <message>
+        <source>Best previous rating:</source>
+        <translation>Лучший предыдущий рейтинг:</translation>
+    </message>
+    <message>
+        <source>Recent development:</source>
+        <translation>Последняя разработка:</translation>
+    </message>
+</context>
+<context>
+    <name>SaveDialog</name>
+    <message>
+        <source>Save</source>
+        <translation>Сохранить</translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation>Игры Блокус</translation>
+    </message>
+</context>
+<context>
+    <name>TableModel</name>
+    <message>
+        <source>Game</source>
+        <extracomment>Table header for game number in rating dialog</extracomment>
+        <translation>Игра</translation>
+    </message>
+    <message>
+        <source>Result</source>
+        <extracomment>Table header for game result in rating dialog</extracomment>
+        <translation>Результат</translation>
+    </message>
+    <message>
+        <source>Level</source>
+        <extracomment>Table header for level in rating dialog</extracomment>
+        <translation>Уровень</translation>
+    </message>
+    <message>
+        <source>Your Color</source>
+        <extracomment>Table header for player color(s) in rating dialog</extracomment>
+        <translation>Ваш Цвет</translation>
+    </message>
+    <message>
+        <source>Date</source>
+        <extracomment>Table header for game date in rating dialog</extracomment>
+        <translation>Дата:</translation>
+    </message>
+    <message>
+        <source>Win</source>
+        <extracomment>Result of rated game is a win</extracomment>
+        <translation>Победа</translation>
+    </message>
+    <message>
+        <source>Loss</source>
+        <extracomment>Result of rated game is a loss</extracomment>
+        <translation>Проигрыш</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>Ничья</translation>
+    </message>
+</context>
+<context>
+    <name>ToolBar</name>
+    <message>
+        <source>Start a new game</source>
+        <translation>Начать новую игру</translation>
+    </message>
+    <message>
+        <source>Start a rated game</source>
+        <translation>Начать рейтинговую игру</translation>
+    </message>
+    <message>
+        <source>Set the colors played by the computer</source>
+        <translation>Установите цвета игроков компьютера</translation>
+    </message>
+    <message>
+        <source>Make the computer continue to play the current color</source>
+        <translation>Сделать так, чтобы компьютер продолжал играть в текущем цвете</translation>
+    </message>
+    <message>
+        <source>Make the computer play the current color</source>
+        <translation>Сделать так, чтобы компьютер играл в текущем цвете</translation>
+    </message>
+    <message>
+        <source>Go to beginning of game</source>
+        <translation>К началу игры</translation>
+    </message>
+    <message>
+        <source>Go ten moves backward</source>
+        <translation>На десять ходов назад</translation>
+    </message>
+    <message>
+        <source>Go one move backward</source>
+        <translation>На один шаг назад</translation>
+    </message>
+    <message>
+        <source>Go one move forward</source>
+        <translation>Вперед на один ход</translation>
+    </message>
+    <message>
+        <source>Go ten moves forward</source>
+        <translation>Вперед на десять ходов</translation>
+    </message>
+    <message>
+        <source>Go to end of moves</source>
+        <translation>К концу ходов</translation>
+    </message>
+    <message>
+        <source>Go to previous variation</source>
+        <translation>К предыдущему варианту</translation>
+    </message>
+    <message>
+        <source>Go to next variation</source>
+        <translation>К следующему варианту</translation>
+    </message>
+    <message>
+        <source>Abort game analysis</source>
+        <translation>Прервать анализ игры</translation>
+    </message>
+    <message>
+        <source>Abort computer move</source>
+        <translation>Прервать перемещение компьютера</translation>
+    </message>
+    <message>
+        <source>Undo move</source>
+        <extracomment>Tooltip for Undo button</extracomment>
+        <translation>Отменить Ход</translation>
+    </message>
+    <message>
+        <source>Open menu</source>
+        <translation>Открыть меню</translation>
+    </message>
+</context>
+<context>
+    <name>main</name>
+    <message>
+        <source>computer opponent for the board game Blokus</source>
+        <translation>оппонент компьютерной настольной игры Блокус</translation>
+    </message>
+    <message>
+        <source>Set maximum level to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --maxlevel</extracomment>
+        <translation>Установить максимальный уровень на &lt;n&gt;.</translation>
+    </message>
+    <message>
+        <source>Do not use opening books.</source>
+        <extracomment>Description for command line option --nobook</extracomment>
+        <translation>Не использовать открытие книг.</translation>
+    </message>
+    <message>
+        <source>Use layout optimized for smartphones.</source>
+        <extracomment>Description for command line option --mobile</extracomment>
+        <translation>Используйте макет, оптимизированный для смартфонов.</translation>
+    </message>
+    <message>
+        <source>Do not delay fast computer moves.</source>
+        <extracomment>Description for command line option --nodelay</extracomment>
+        <translation>Не снижать быстрые движения компьютера.</translation>
+    </message>
+    <message>
+        <source>Set random seed to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --seed</extracomment>
+        <translation>Установить случайный источник в &lt;n&gt;.</translation>
+    </message>
+    <message>
+        <source>Use &lt;n&gt; threads (0=auto).</source>
+        <extracomment>Description for command line option --threads</extracomment>
+        <translation>Использовать &lt;n&gt; потоков (0=auto).</translation>
+    </message>
+    <message>
+        <source>Print logging information to standard error.</source>
+        <extracomment>Description for command line option --verbose</extracomment>
+        <translation>Печать информации журнала до стандартной ошибки.</translation>
+    </message>
+    <message>
+        <source>file.blksgf</source>
+        <extracomment>Name of command line argument.</extracomment>
+        <translation>файл.blksgf</translation>
+    </message>
+    <message>
+        <source>Blokus SGF file to open (optional).</source>
+        <translation>Blokus SGF файл для открытия (опционно).</translation>
+    </message>
+    <message>
+        <source>--maxlevel must be between 1 and %1</source>
+        <translation>--maxlevel должно быть между 1 и %1</translation>
+    </message>
+    <message>
+        <source>--seed must be a positive number</source>
+        <translation>--seed должно быть положительное число</translation>
+    </message>
+    <message>
+        <source>--threads must be a positive number</source>
+        <translation>--threads должно быть положительное число</translation>
+    </message>
+    <message>
+        <source>Too many arguments</source>
+        <translation>Слишком много аргументов</translation>
+    </message>
+</context>
+</TS>
\ No newline at end of file
diff --git a/pentobi/qml/i18n/qml_zh_CN.ts b/pentobi/qml/i18n/qml_zh_CN.ts
new file mode 100644 (file)
index 0000000..74852a5
--- /dev/null
@@ -0,0 +1,1434 @@
+<?xml version="1.0" ?><!DOCTYPE TS><TS language="zh_CN" version="2.1">
+<context>
+    <name>AboutDialog</name>
+    <message>
+        <source>Copyright © 2011–%1 Markus Enzenberger</source>
+        <translation>版权所有 2011–%1 Markus Enzenberger</translation>
+    </message>
+    <message>
+        <source>Computer opponent for the board game Blokus</source>
+        <translation>经典桌游《角斗士》的计算机对手</translation>
+    </message>
+    <message>
+        <source>Pentobi %1</source>
+        <extracomment>The argument is the application version.</extracomment>
+        <translation>Pentobi 版本%1</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeDialog</name>
+    <message>
+        <source>Analysis speed:</source>
+        <translation>分析速度:</translation>
+    </message>
+    <message>
+        <source>Fast</source>
+        <translation>快速</translation>
+    </message>
+    <message>
+        <source>Normal</source>
+        <translation>中速</translation>
+    </message>
+    <message>
+        <source>Slow</source>
+        <translation>慢速</translation>
+    </message>
+</context>
+<context>
+    <name>AnalyzeGame</name>
+    <message>
+        <source>(No analysis)</source>
+        <translation>(暂无分析)</translation>
+    </message>
+</context>
+<context>
+    <name>AppearanceDialog</name>
+    <message>
+        <source>Coordinates</source>
+        <translation>坐标</translation>
+    </message>
+    <message>
+        <source>Show variations</source>
+        <translation>显示变化</translation>
+    </message>
+    <message>
+        <source>Light</source>
+        <translation>浅色</translation>
+    </message>
+    <message>
+        <source>Dark</source>
+        <translation>深色</translation>
+    </message>
+    <message>
+        <source>Colorblind light</source>
+        <translation>色盲浅</translation>
+    </message>
+    <message>
+        <source>Colorblind dark</source>
+        <translation>色盲深</translation>
+    </message>
+    <message>
+        <source>System</source>
+        <extracomment>Name of theme using default system colors</extracomment>
+        <translation>系统</translation>
+    </message>
+    <message>
+        <source>Move marking:</source>
+        <translation>棋步标记:</translation>
+    </message>
+    <message>
+        <source>Last with dot</source>
+        <translation>最后一步加点</translation>
+    </message>
+    <message>
+        <source>Last with number</source>
+        <translation>最后一步加数字</translation>
+    </message>
+    <message>
+        <source>All with number</source>
+        <translation>所有棋步加点</translation>
+    </message>
+    <message>
+        <source>None</source>
+        <extracomment>Move marking/None</extracomment>
+        <translation>无</translation>
+    </message>
+    <message>
+        <source>Animations</source>
+        <translation>动画</translation>
+    </message>
+    <message>
+        <source>Show comment:</source>
+        <translation>显示注解:</translation>
+    </message>
+    <message>
+        <source>Always</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>总是</translation>
+    </message>
+    <message>
+        <source>As needed</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>按需</translation>
+    </message>
+    <message>
+        <source>Never</source>
+        <extracomment>Show-comment mode</extracomment>
+        <translation>不显示</translation>
+    </message>
+    <message>
+        <source>Color theme:</source>
+        <translation>颜色主题:</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>棋步顺序</translation>
+    </message>
+</context>
+<context>
+    <name>AsciiArtSaveDialog</name>
+    <message>
+        <source>Export ASCII Art</source>
+        <translation>导出字符画</translation>
+    </message>
+    <message>
+        <source>Text files</source>
+        <translation>文本文件</translation>
+    </message>
+</context>
+<context>
+    <name>BoardContextMenu</name>
+    <message>
+        <source>Go to Move %1</source>
+        <translation>转到第%1步</translation>
+    </message>
+    <message>
+        <source>Move Annotation</source>
+        <translation>棋步注解</translation>
+    </message>
+    <message>
+        <source>Move Annotation (%1)</source>
+        <extracomment>The argument is the annotation symbol for the current move</extracomment>
+        <translation>棋步注解(%1)</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonApply</name>
+    <message>
+        <source>Apply</source>
+        <translation>应用</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonCancel</name>
+    <message>
+        <source>Cancel</source>
+        <translation>取消</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonClose</name>
+    <message>
+        <source>Close</source>
+        <translation>关闭</translation>
+    </message>
+</context>
+<context>
+    <name>ButtonOk</name>
+    <message>
+        <source>OK</source>
+        <translation>确定</translation>
+    </message>
+</context>
+<context>
+    <name>ComputerDialog</name>
+    <message>
+        <source>Computer plays:</source>
+        <translation>电脑执:</translation>
+    </message>
+    <message>
+        <source>Level %1</source>
+        <translation>等级 %1</translation>
+    </message>
+</context>
+<context>
+    <name>ExportImageDialog</name>
+    <message>
+        <source>Image width:</source>
+        <translation>图像宽度:</translation>
+    </message>
+</context>
+<context>
+    <name>FileDialog</name>
+    <message>
+        <source>Overwrite file?</source>
+        <translation>覆盖文件?</translation>
+    </message>
+    <message>
+        <source>Open</source>
+        <translation>打开</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>保存</translation>
+    </message>
+    <message>
+        <source>All files</source>
+        <translation>所有文件</translation>
+    </message>
+</context>
+<context>
+    <name>GameInfoDialog</name>
+    <message>
+        <source>Player Blue/Red:</source>
+        <translation>玩家 蓝/红:</translation>
+    </message>
+    <message>
+        <source>Player Purple:</source>
+        <translation>玩家 紫色:</translation>
+    </message>
+    <message>
+        <source>Player Green:</source>
+        <translation>玩家 绿色:</translation>
+    </message>
+    <message>
+        <source>Player Blue:</source>
+        <translation>玩家 蓝色:</translation>
+    </message>
+    <message>
+        <source>Player Yellow/Green:</source>
+        <translation>玩家 黄/绿:</translation>
+    </message>
+    <message>
+        <source>Player Orange:</source>
+        <translation>玩家 橙色:</translation>
+    </message>
+    <message>
+        <source>Player Yellow:</source>
+        <translation>玩家 黄色:</translation>
+    </message>
+    <message>
+        <source>Player Red:</source>
+        <translation>玩家 红色:</translation>
+    </message>
+    <message>
+        <source>Date:</source>
+        <translation>日期:</translation>
+    </message>
+    <message>
+        <source>Time:</source>
+        <translation>时间:</translation>
+    </message>
+    <message>
+        <source>Event:</source>
+        <translation>赛事:</translation>
+    </message>
+    <message>
+        <source>Round:</source>
+        <translation>轮次:</translation>
+    </message>
+</context>
+<context>
+    <name>GameModel</name>
+    <message>
+        <source>Purple wins with 1 point.</source>
+        <translation>紫色赢1分。</translation>
+    </message>
+    <message>
+        <source>Purple wins with %L1 points.</source>
+        <translation>紫色赢%L1分。</translation>
+    </message>
+    <message>
+        <source>Orange wins with 1 point.</source>
+        <translation>橙色赢1分。</translation>
+    </message>
+    <message>
+        <source>Orange wins with %L1 points.</source>
+        <translation>橙色赢%L1分。</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie.</source>
+        <translation>本局以平局结束。</translation>
+    </message>
+    <message>
+        <source>Green wins with 1 point.</source>
+        <translation>绿色赢1分。</translation>
+    </message>
+    <message>
+        <source>Green wins with %L1 points.</source>
+        <translation>绿色赢%L1分。</translation>
+    </message>
+    <message>
+        <source>Blue wins with 1 point.</source>
+        <translation>蓝色赢1分。</translation>
+    </message>
+    <message>
+        <source>Blue wins with %L1 points.</source>
+        <translation>蓝色赢%L1分。</translation>
+    </message>
+    <message>
+        <source>Green wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>绿色 获胜(平局后)。</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with 1 point.</source>
+        <translation>蓝/红 赢1分。</translation>
+    </message>
+    <message>
+        <source>Blue/Red wins with %L1 points.</source>
+        <translation>蓝/红 赢%L1分。</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with 1 point.</source>
+        <translation>黄/绿 赢1分。</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins with %L1 points.</source>
+        <translation>黄/绿 赢%L1分。</translation>
+    </message>
+    <message>
+        <source>Yellow/Green wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>黄/绿 获胜(平局后)。</translation>
+    </message>
+    <message>
+        <source>Blue wins.</source>
+        <translation>蓝色 获胜。</translation>
+    </message>
+    <message>
+        <source>Yellow wins.</source>
+        <translation>黄色 获胜。</translation>
+    </message>
+    <message>
+        <source>Red wins.</source>
+        <translation>红色 获胜。</translation>
+    </message>
+    <message>
+        <source>Red wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>红色 获胜(平局后)。</translation>
+    </message>
+    <message>
+        <source>Yellow wins (tie resolved).</source>
+        <extracomment>Game variant with tie-breaker rule made later player win.</extracomment>
+        <translation>黄色 获胜(平局后)。</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Yellow.</source>
+        <translation>蓝色与黄色共同获胜。</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue and Red.</source>
+        <translation>蓝色与红色共同获胜。</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow and Red.</source>
+        <translation>红色与黄色共同获胜。</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between all players.</source>
+        <translation>所有玩家平局。</translation>
+    </message>
+    <message>
+        <source>Green wins.</source>
+        <translation>绿色 获胜。</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Red.</source>
+        <translation>蓝色、黄色与红色共同获胜。</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Yellow and Green.</source>
+        <translation>蓝色、黄色与绿色共同获胜。</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Blue, Red and Green.</source>
+        <translation>蓝色、红色与绿色共同获胜。</translation>
+    </message>
+    <message>
+        <source>Game ends in a tie between Yellow, Red and Green.</source>
+        <translation>黄色、红色与绿色共同获胜。</translation>
+    </message>
+    <message>
+        <source>Invalid Blokus SGF file. (%1)</source>
+        <translation>无效的角斗士游戏文件。(%1)</translation>
+    </message>
+    <message>
+        <source>Clipboard is empty.</source>
+        <translation>剪贴板为空。</translation>
+    </message>
+    <message>
+        <source>Untitled</source>
+        <translation>无标题</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>无标题%1</translation>
+    </message>
+    <message>
+        <source>New Folder</source>
+        <translation>新文件夹</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>新文件夹%1</translation>
+    </message>
+    <message>
+        <source>(Setup)</source>
+        <translation>(设置模式)</translation>
+    </message>
+    <message>
+        <source>(No moves)</source>
+        <translation>(无棋步)</translation>
+    </message>
+    <message>
+        <source>Move %1</source>
+        <extracomment>The argument is the current move number.</extracomment>
+        <translation>棋步 %1</translation>
+    </message>
+    <message>
+        <source>Unsupported character set</source>
+        <translation>不支持的字符集</translation>
+    </message>
+</context>
+<context>
+    <name>GameVariantDialog</name>
+    <message>
+        <source>Classic</source>
+        <translation>经典</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>双人</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>初级</translation>
+    </message>
+    <message>
+        <source>Trigon</source>
+        <translation>六角</translation>
+    </message>
+    <message>
+        <source>Nexos</source>
+        <translation>线</translation>
+    </message>
+    <message>
+        <source>GembloQ</source>
+        <translation>宝石阵Q</translation>
+    </message>
+    <message>
+        <source>Callisto</source>
+        <translation>八角</translation>
+    </message>
+    <message>
+        <source>Players:</source>
+        <translation>玩家:</translation>
+    </message>
+    <message>
+        <source>Colors:</source>
+        <translation>颜色:</translation>
+    </message>
+</context>
+<context>
+    <name>GameViewDesktop</name>
+    <message>
+        <source>Computer is thinking…</source>
+        <translation>电脑正在思考…</translation>
+    </message>
+    <message>
+        <source>Running game analysis…</source>
+        <translation>正在进行棋局分析…</translation>
+    </message>
+    <message>
+        <source>Computer is thinking… (up to %1 seconds remaining)</source>
+        <translation>电脑正在思考...(最多剩余%1秒)</translation>
+    </message>
+    <message>
+        <source>Computer is thinking… (up to %1 minutes remaining)</source>
+        <translation>电脑正在思考...(最多剩余%1分)</translation>
+    </message>
+</context>
+<context>
+    <name>GotoMoveDialog</name>
+    <message>
+        <source>Move number:</source>
+        <translation>棋步序号:</translation>
+    </message>
+</context>
+<context>
+    <name>HelpWindow</name>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Pentobi 帮助</translation>
+    </message>
+</context>
+<context>
+    <name>ImageSaveDialog</name>
+    <message>
+        <source>Save Image</source>
+        <translation>保存图片</translation>
+    </message>
+    <message>
+        <source>PNG image files</source>
+        <translation>PNG图像文件</translation>
+    </message>
+    <message>
+        <source>JPEG image files</source>
+        <translation>JPEG图像文件</translation>
+    </message>
+</context>
+<context>
+    <name>InitialRatingDialog</name>
+    <message>
+        <source>Initialize your rating for this game variant.</source>
+        <translation>设置您在本游戏规则下的初始等级。</translation>
+    </message>
+    <message>
+        <source>Initial rating:</source>
+        <translation>初始等级:</translation>
+    </message>
+    <message>
+        <source>Beginner</source>
+        <translation>初学者</translation>
+    </message>
+    <message>
+        <source>Expert</source>
+        <translation>专家</translation>
+    </message>
+</context>
+<context>
+    <name>Main</name>
+    <message>
+        <source>Pentobi</source>
+        <extracomment>Window title if no file is loaded.</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Game analysis is only possible in main variation.</source>
+        <translation>棋局分析只能在主变化中进行。</translation>
+    </message>
+    <message>
+        <source>Autosaved game was changed by another instance of Pentobi. Overwrite?</source>
+        <translation>自动保存的棋局被另一个程序实例更改。要覆盖吗?</translation>
+    </message>
+    <message>
+        <source>Your rating has increased from %1 to %2.</source>
+        <translation>您的等级从%1升到%2。</translation>
+    </message>
+    <message>
+        <source>Your rating has decreased from %1 to %2.</source>
+        <translation>您的等级从%1降到%2。</translation>
+    </message>
+    <message>
+        <source>Your rating stays at %1.</source>
+        <translation>您的等级仍然是%1。</translation>
+    </message>
+    <message>
+        <source>No permission to access storage</source>
+        <translation>没有访问存储器的权限</translation>
+    </message>
+    <message>
+        <source>Delete all rating information for the current game variant?</source>
+        <translation>删除此游戏规则下的所有等级数据?</translation>
+    </message>
+    <message>
+        <source>Delete all variations?</source>
+        <translation>删除所有变例?</translation>
+    </message>
+    <message>
+        <source>Save failed.</source>
+        <translation>保存失败。</translation>
+    </message>
+    <message>
+        <source>End of tree was reached. Continue search from start of the tree?</source>
+        <translation>已经到达棋局树的最末端。从树的开头继续搜索?</translation>
+    </message>
+    <message>
+        <source>No comment found</source>
+        <translation>无注解</translation>
+    </message>
+    <message>
+        <source>%1 (modified)</source>
+        <extracomment>Label for modified loaded game. The argument is the file name.</extracomment>
+        <translation>%1 (已修改)</translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Reload?</source>
+        <translation>文件被其他程序修改。重新加载吗?</translation>
+    </message>
+    <message>
+        <source>Continue computer move?</source>
+        <translation>是否由电脑继续行棋?</translation>
+    </message>
+    <message>
+        <source>Keep only position?</source>
+        <translation>只保留棋子位置吗?</translation>
+    </message>
+    <message>
+        <source>Keep only subtree?</source>
+        <translation>只保留子树吗?</translation>
+    </message>
+    <message>
+        <source>Open failed.</source>
+        <translation>打开失败。</translation>
+    </message>
+    <message>
+        <source>Start rated game with Purple against Pentobi level %1?</source>
+        <translation>是否开始等级赛,你执紫色,对阵级别%1的电脑?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Green against Pentobi level %1?</source>
+        <translation>是否开始等级赛,你执绿色,对阵级别%1的电脑?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue/Red against Pentobi level %1?</source>
+        <translation>是否开始等级赛,你执蓝/红,对阵级别%1的电脑?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Blue against Pentobi level %1?</source>
+        <translation>是否开始等级赛,你执蓝色,对阵级别%1的电脑?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Orange against Pentobi level %1?</source>
+        <translation>是否开始等级赛,你执橙色,对阵级别%1的电脑?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow/Green against Pentobi level %1?</source>
+        <translation>是否开始等级赛,你执黄/绿,对阵级别%1的电脑?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Yellow against Pentobi level %1?</source>
+        <translation>是否开始等级赛,你执黄色,对阵级别%1的电脑?</translation>
+    </message>
+    <message>
+        <source>Start rated game with Red against Pentobi level %1?</source>
+        <translation>是否开始等级赛,你执红色,对阵级别%1的电脑?</translation>
+    </message>
+    <message>
+        <source>You have not yet played rated games in this game variant.</source>
+        <translation>在此规则下,您还没有玩过等级赛。</translation>
+    </message>
+    <message>
+        <source>Truncate this subtree?</source>
+        <translation>截断此子树?</translation>
+    </message>
+    <message>
+        <source>Truncate children?</source>
+        <translation>截断此结点下面的子结点?</translation>
+    </message>
+    <message>
+        <source>Discard game?</source>
+        <translation>丢弃本局吗?</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(级别%2)</translation>
+    </message>
+    <message>
+        <source>Human</source>
+        <extracomment>Player name for game info in rated game.</extracomment>
+        <translation>人脑</translation>
+    </message>
+    <message>
+        <source>Rated game</source>
+        <translation>等级赛</translation>
+    </message>
+    <message>
+        <source>File has been modified by another application. Overwrite?</source>
+        <translation>文件已被其他程序修改。覆盖吗?</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"/>
+    </message>
+    <message>
+        <source>Not enough memory</source>
+        <translation>内存不足</translation>
+    </message>
+    <message>
+        <source>Game analysis aborted</source>
+        <translation>棋局分析被取消</translation>
+    </message>
+    <message>
+        <source>Computer move aborted</source>
+        <translation>电脑行棋被取消</translation>
+    </message>
+    <message>
+        <source>Rating information deleted</source>
+        <translation>等级数据已删除</translation>
+    </message>
+    <message>
+        <source>Variations deleted</source>
+        <translation>变例已删除</translation>
+    </message>
+    <message>
+        <source>File saved</source>
+        <translation>文件已保存</translation>
+    </message>
+    <message>
+        <source>Saving image failed or unsupported image format</source>
+        <translation>图像保存失败,或不支持的图像格式</translation>
+    </message>
+    <message>
+        <source>Image saved</source>
+        <translation>图像已保存</translation>
+    </message>
+    <message>
+        <source>Creating image failed</source>
+        <translation>图像创建失败</translation>
+    </message>
+    <message>
+        <source>Continuing rated game</source>
+        <translation>继续等级赛</translation>
+    </message>
+    <message>
+        <source>Kept only position</source>
+        <translation>只保留棋子位置</translation>
+    </message>
+    <message>
+        <source>Kept only subtree</source>
+        <translation>只保留子树</translation>
+    </message>
+    <message>
+        <source>Variation is now %1</source>
+        <translation>当前变例为 %1</translation>
+    </message>
+    <message>
+        <source>Children truncated</source>
+        <translation>子结点已截断</translation>
+    </message>
+    <message>
+        <source>Setup</source>
+        <extracomment>Small-screen label for setup mode (short for &quot;Setup Mode&quot;).</extracomment>
+        <translation>设置模式</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>设置模式</translation>
+    </message>
+    <message>
+        <source>Rated</source>
+        <extracomment>Label for ongoing rated game</extracomment>
+        <translation>等级赛</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>等级赛 第%1局</translation>
+    </message>
+    <message>
+        <source>Rated Game %1</source>
+        <extracomment>Label for rated game. The argument is the game number.</extracomment>
+        <translation>等级赛 第%1局</translation>
+    </message>
+    <message>
+        <source>Main Variation</source>
+        <translation>主变例</translation>
+    </message>
+    <message>
+        <source>Beginning of Branch</source>
+        <translation>分支的开始</translation>
+    </message>
+    <message>
+        <source>Comment</source>
+        <translation>注解</translation>
+    </message>
+    <message>
+        <source>Settings</source>
+        <extracomment>Menu item Computer/Settings</extracomment>
+        <translation>设置</translation>
+    </message>
+    <message>
+        <source>Find Move</source>
+        <translation>寻找棋步</translation>
+    </message>
+    <message>
+        <source>Next Comment</source>
+        <translation>下一个注解</translation>
+    </message>
+    <message>
+        <source>Fullscreen</source>
+        <translation>全屏幕</translation>
+    </message>
+    <message>
+        <source>Game Info</source>
+        <translation>棋局信息</translation>
+    </message>
+    <message>
+        <source>Move Number…</source>
+        <translation>指定棋步…</translation>
+    </message>
+    <message>
+        <source>Pentobi Help</source>
+        <translation>Pentobi 帮助</translation>
+    </message>
+    <message>
+        <source>New</source>
+        <translation>新棋局</translation>
+    </message>
+    <message>
+        <source>Rated Game</source>
+        <translation>等级赛</translation>
+    </message>
+    <message>
+        <source>Open…</source>
+        <translation>打开…</translation>
+    </message>
+    <message>
+        <source>Play</source>
+        <translation>行棋</translation>
+    </message>
+    <message>
+        <source>Play Move</source>
+        <extracomment>Play a single move</extracomment>
+        <translation>走一步</translation>
+    </message>
+    <message>
+        <source>Quit</source>
+        <translation>退出</translation>
+    </message>
+    <message>
+        <source>Save</source>
+        <translation>保存</translation>
+    </message>
+    <message>
+        <source>Save As…</source>
+        <translation>另存为…</translation>
+    </message>
+    <message>
+        <source>Stop</source>
+        <translation>停止</translation>
+    </message>
+    <message>
+        <source>Undo Move</source>
+        <translation>撤销一步</translation>
+    </message>
+    <message>
+        <source>Blue/Red</source>
+        <translation>蓝/红</translation>
+    </message>
+    <message>
+        <source>Purple</source>
+        <translation>紫色</translation>
+    </message>
+    <message>
+        <source>Green</source>
+        <translation>绿色</translation>
+    </message>
+    <message>
+        <source>Blue</source>
+        <translation>蓝色</translation>
+    </message>
+    <message>
+        <source>Yellow/Green</source>
+        <translation>黄/绿</translation>
+    </message>
+    <message>
+        <source>Orange</source>
+        <translation>橙色</translation>
+    </message>
+    <message>
+        <source>Yellow</source>
+        <translation>黄色</translation>
+    </message>
+    <message>
+        <source>Red</source>
+        <translation>红色</translation>
+    </message>
+    <message>
+        <source>Pentobi failed to generate a move.</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Press back again to exit</source>
+        <translation type="unfinished"/>
+    </message>
+</context>
+<context>
+    <name>MenuComputer</name>
+    <message>
+        <source>Computer</source>
+        <translation>电脑</translation>
+    </message>
+</context>
+<context>
+    <name>MenuEdit</name>
+    <message>
+        <source>Edit</source>
+        <translation>编辑</translation>
+    </message>
+    <message>
+        <source>Make Main Variation</source>
+        <translation>设为主变例</translation>
+    </message>
+    <message>
+        <source>Variation Up</source>
+        <extracomment>Short for Move Variation Up</extracomment>
+        <translation>上一变例</translation>
+    </message>
+    <message>
+        <source>Variation Down</source>
+        <extracomment>Short for Move Variation Down</extracomment>
+        <translation>下一变例</translation>
+    </message>
+    <message>
+        <source>Delete Variations</source>
+        <translation>删除所有变例</translation>
+    </message>
+    <message>
+        <source>Truncate</source>
+        <translation>截断</translation>
+    </message>
+    <message>
+        <source>Truncate Children</source>
+        <translation>截断子结点</translation>
+    </message>
+    <message>
+        <source>Keep Position</source>
+        <translation>保存棋子位置</translation>
+    </message>
+    <message>
+        <source>Keep Subtree</source>
+        <translation>保留子树</translation>
+    </message>
+    <message>
+        <source>Setup Mode</source>
+        <translation>设置模式</translation>
+    </message>
+    <message>
+        <source>Next Color</source>
+        <translation>轮到的颜色</translation>
+    </message>
+    <message>
+        <source>Annotation…</source>
+        <translation>注解…</translation>
+    </message>
+    <message>
+        <source>Made main variation</source>
+        <translation>已设为主变例</translation>
+    </message>
+</context>
+<context>
+    <name>MenuExport</name>
+    <message>
+        <source>Export</source>
+        <translation>导出</translation>
+    </message>
+    <message>
+        <source>Image…</source>
+        <translation>图像…</translation>
+    </message>
+    <message>
+        <source>ASCII Art…</source>
+        <translation>字符画…</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGame</name>
+    <message>
+        <source>Game</source>
+        <translation>棋局</translation>
+    </message>
+    <message>
+        <source>Game Variant…</source>
+        <translation>游戏规则…</translation>
+    </message>
+    <message>
+        <source>Open Clipboard</source>
+        <translation>打开剪贴板</translation>
+    </message>
+</context>
+<context>
+    <name>MenuGo</name>
+    <message>
+        <source>Go</source>
+        <translation>转到</translation>
+    </message>
+</context>
+<context>
+    <name>MenuHelp</name>
+    <message>
+        <source>Help</source>
+        <translation>帮助</translation>
+    </message>
+    <message>
+        <source>About Pentobi</source>
+        <translation>关于</translation>
+    </message>
+    <message>
+        <source>Report Bug</source>
+        <translation>报告bug</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>打开最近文件</translation>
+    </message>
+    <message>
+        <source>Clear List</source>
+        <extracomment>Menu item for clearing the recent files list</extracomment>
+        <translation>清除列表</translation>
+    </message>
+</context>
+<context>
+    <name>MenuTools</name>
+    <message>
+        <source>Tools</source>
+        <translation>工具</translation>
+    </message>
+    <message>
+        <source>Rating</source>
+        <translation>等级分</translation>
+    </message>
+    <message>
+        <source>Clear Rating</source>
+        <translation>清除等级分</translation>
+    </message>
+    <message>
+        <source>Clear Analysis</source>
+        <translation>清除分析</translation>
+    </message>
+    <message>
+        <source>Analyze Game…</source>
+        <translation>分析棋局…</translation>
+    </message>
+</context>
+<context>
+    <name>MenuView</name>
+    <message>
+        <source>View</source>
+        <translation>视图</translation>
+    </message>
+    <message>
+        <source>Appearance</source>
+        <translation>界面</translation>
+    </message>
+    <message>
+        <source>Toolbar</source>
+        <translation>工具栏</translation>
+    </message>
+</context>
+<context>
+    <name>MoveAnnotationDialog</name>
+    <message>
+        <source>Move %1</source>
+        <translation>棋步 %1</translation>
+    </message>
+    <message>
+        <source>Very good</source>
+        <translation>妙招</translation>
+    </message>
+    <message>
+        <source>Good</source>
+        <translation>好棋</translation>
+    </message>
+    <message>
+        <source>Interesting</source>
+        <translation>有趣</translation>
+    </message>
+    <message>
+        <source>Doubtful</source>
+        <translation>争议</translation>
+    </message>
+    <message>
+        <source>Bad</source>
+        <translation>坏棋</translation>
+    </message>
+    <message>
+        <source>Very Bad</source>
+        <translation>败招</translation>
+    </message>
+    <message>
+        <source>No annotation</source>
+        <translation>无注解</translation>
+    </message>
+</context>
+<context>
+    <name>NewFolderDialog</name>
+    <message>
+        <source>Folder name:</source>
+        <translation>文件夹名:</translation>
+    </message>
+</context>
+<context>
+    <name>OpenDialog</name>
+    <message>
+        <source>Open</source>
+        <translation>打开</translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation>角斗士游戏</translation>
+    </message>
+</context>
+<context>
+    <name>RatingDialog</name>
+    <message>
+        <source>Your rating:</source>
+        <translation>您的等级分:</translation>
+    </message>
+    <message>
+        <source>Game variant:</source>
+        <translation>游戏规则:</translation>
+    </message>
+    <message>
+        <source>Classic (2)</source>
+        <extracomment>Short for Classic (2 players)</extracomment>
+        <translation>经典(2人)</translation>
+    </message>
+    <message>
+        <source>Classic (3)</source>
+        <extracomment>Short for Classic (3 players)</extracomment>
+        <translation>经典(3人)</translation>
+    </message>
+    <message>
+        <source>Classic (4)</source>
+        <extracomment>Short for Classic (4 players)</extracomment>
+        <translation>经典(4人)</translation>
+    </message>
+    <message>
+        <source>Duo</source>
+        <translation>双人</translation>
+    </message>
+    <message>
+        <source>Junior</source>
+        <translation>初级</translation>
+    </message>
+    <message>
+        <source>Trigon (2)</source>
+        <extracomment>Short for Trigon (2 players)</extracomment>
+        <translation>六角(2人)</translation>
+    </message>
+    <message>
+        <source>Trigon (3)</source>
+        <extracomment>Short for Trigon (3 players)</extracomment>
+        <translation>六角(3人)</translation>
+    </message>
+    <message>
+        <source>Trigon (4)</source>
+        <extracomment>Short for Trigon (4 players)</extracomment>
+        <translation>六角(4人)</translation>
+    </message>
+    <message>
+        <source>Nexos (2)</source>
+        <extracomment>Short for Nexos (2 players)</extracomment>
+        <translation>线(2人)</translation>
+    </message>
+    <message>
+        <source>Nexos (4)</source>
+        <extracomment>Short for Nexos (4 players)</extracomment>
+        <translation>线(4人)</translation>
+    </message>
+    <message>
+        <source>Callisto (2)</source>
+        <extracomment>Short for Callisto (2 players, 2 colors)</extracomment>
+        <translation>八角(2人)</translation>
+    </message>
+    <message>
+        <source>Callisto (2/4)</source>
+        <extracomment>Short for Callisto (2 players, 4 colors)</extracomment>
+        <translation>八角(2人4色)</translation>
+    </message>
+    <message>
+        <source>Callisto (3)</source>
+        <extracomment>Short for Callisto (3 players)</extracomment>
+        <translation>八角(3人)</translation>
+    </message>
+    <message>
+        <source>Callisto (4)</source>
+        <extracomment>Short for Callisto (4 players)</extracomment>
+        <translation>八角(4人)</translation>
+    </message>
+    <message>
+        <source>GembloQ (4)</source>
+        <extracomment>Short for GembloQ (4 players)</extracomment>
+        <translation>宝石阵Q(4人)</translation>
+    </message>
+    <message>
+        <source>GembloQ (2)</source>
+        <extracomment>Short for GembloQ (2 players, 2 colors)</extracomment>
+        <translation>宝石阵Q(2人)</translation>
+    </message>
+    <message>
+        <source>GembloQ (2/4)</source>
+        <extracomment>Short for GembloQ (2 players, 4 colors)</extracomment>
+        <translation>宝石阵Q(2人4色)</translation>
+    </message>
+    <message>
+        <source>GembloQ (3)</source>
+        <extracomment>Short for GembloQ (3 players)</extracomment>
+        <translation>宝石阵Q(3人)</translation>
+    </message>
+    <message>
+        <source>Rated games:</source>
+        <translation>等级赛局数:</translation>
+    </message>
+    <message>
+        <source>Best previous rating:</source>
+        <translation>历史最佳等级分:</translation>
+    </message>
+    <message>
+        <source>Recent development:</source>
+        <translation>最近的发展:</translation>
+    </message>
+    <message>
+        <source>Open Game %1</source>
+        <translation>打开第%1局</translation>
+    </message>
+</context>
+<context>
+    <name>SaveDialog</name>
+    <message>
+        <source>Save</source>
+        <translation>保存</translation>
+    </message>
+    <message>
+        <source>Blokus games</source>
+        <translation>角斗士游戏</translation>
+    </message>
+</context>
+<context>
+    <name>TableModel</name>
+    <message>
+        <source>Game</source>
+        <extracomment>Table header for game number in rating dialog</extracomment>
+        <translation>总局数</translation>
+    </message>
+    <message>
+        <source>Result</source>
+        <extracomment>Table header for game result in rating dialog</extracomment>
+        <translation>结果</translation>
+    </message>
+    <message>
+        <source>Level</source>
+        <extracomment>Table header for level in rating dialog</extracomment>
+        <translation>级别</translation>
+    </message>
+    <message>
+        <source>Your Color</source>
+        <extracomment>Table header for player color(s) in rating dialog</extracomment>
+        <translation>您执</translation>
+    </message>
+    <message>
+        <source>Date</source>
+        <extracomment>Table header for game date in rating dialog</extracomment>
+        <translation>日期</translation>
+    </message>
+    <message>
+        <source>Win</source>
+        <extracomment>Result of rated game is a win</extracomment>
+        <translation>胜</translation>
+    </message>
+    <message>
+        <source>Loss</source>
+        <extracomment>Result of rated game is a loss</extracomment>
+        <translation>负</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>平</translation>
+    </message>
+</context>
+<context>
+    <name>ToolBar</name>
+    <message>
+        <source>Start a new game</source>
+        <translation>开始新游戏</translation>
+    </message>
+    <message>
+        <source>Start a rated game</source>
+        <translation>开始一局等级赛</translation>
+    </message>
+    <message>
+        <source>Set the colors played by the computer</source>
+        <translation>设置电脑控制的颜色</translation>
+    </message>
+    <message>
+        <source>Make the computer continue to play the current color</source>
+        <translation>使电脑执当前颜色继续</translation>
+    </message>
+    <message>
+        <source>Make the computer play the current color</source>
+        <translation>使电脑执当前颜色</translation>
+    </message>
+    <message>
+        <source>Go to beginning of game</source>
+        <translation>转到棋局开始</translation>
+    </message>
+    <message>
+        <source>Go ten moves backward</source>
+        <translation>向后跳转10步</translation>
+    </message>
+    <message>
+        <source>Go one move backward</source>
+        <translation>向后跳转一步</translation>
+    </message>
+    <message>
+        <source>Go one move forward</source>
+        <translation>向前跳转一步</translation>
+    </message>
+    <message>
+        <source>Go ten moves forward</source>
+        <translation>向前跳转10步</translation>
+    </message>
+    <message>
+        <source>Go to end of moves</source>
+        <translation>转到棋局末尾</translation>
+    </message>
+    <message>
+        <source>Go to previous variation</source>
+        <translation>转到上一变例</translation>
+    </message>
+    <message>
+        <source>Go to next variation</source>
+        <translation>转到下一变例</translation>
+    </message>
+    <message>
+        <source>Abort game analysis</source>
+        <translation>取消棋局分析</translation>
+    </message>
+    <message>
+        <source>Abort computer move</source>
+        <translation>取消电脑行棋</translation>
+    </message>
+    <message>
+        <source>Undo move</source>
+        <extracomment>Tooltip for Undo button</extracomment>
+        <translation>撤销一步</translation>
+    </message>
+    <message>
+        <source>Open menu</source>
+        <translation>打开菜单</translation>
+    </message>
+</context>
+<context>
+    <name>main</name>
+    <message>
+        <source>computer opponent for the board game Blokus</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Set maximum level to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --maxlevel</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Do not use opening books.</source>
+        <extracomment>Description for command line option --nobook</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Use layout optimized for smartphones.</source>
+        <extracomment>Description for command line option --mobile</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Do not delay fast computer moves.</source>
+        <extracomment>Description for command line option --nodelay</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Set random seed to &lt;n&gt;.</source>
+        <extracomment>Description for command line option --seed</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Use &lt;n&gt; threads (0=auto).</source>
+        <extracomment>Description for command line option --threads</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Print logging information to standard error.</source>
+        <extracomment>Description for command line option --verbose</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>file.blksgf</source>
+        <extracomment>Name of command line argument.</extracomment>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Blokus SGF file to open (optional).</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>--maxlevel must be between 1 and %1</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>--seed must be a positive number</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>--threads must be a positive number</source>
+        <translation type="unfinished"/>
+    </message>
+    <message>
+        <source>Too many arguments</source>
+        <translation type="unfinished"/>
+    </message>
+</context>
+</TS>
\ No newline at end of file
diff --git a/pentobi/qml/i18n/translations.qrc b/pentobi/qml/i18n/translations.qrc
new file mode 100644 (file)
index 0000000..6acefd6
--- /dev/null
@@ -0,0 +1,10 @@
+<RCC>
+    <qresource prefix="/qml/i18n">
+        <file>qml_de.qm</file>
+        <file>qml_es.qm</file>
+        <file>qml_fr.qm</file>
+        <file>qml_nb_NO.qm</file>
+        <file>qml_ru.qm</file>
+        <file>qml_zh_CN.qm</file>
+    </qresource>
+</RCC>
diff --git a/pentobi/qml/icons/filedialog-folder.svg b/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/pentobi/qml/icons/filedialog-newfolder.svg b/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/pentobi/qml/icons/filedialog-parent.svg b/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/pentobi/qml/themes/colorblind-dark/Theme.qml b/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/pentobi/qml/themes/colorblind-light/Theme.qml b/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/pentobi/qml/themes/dark/Theme.qml b/pentobi/qml/themes/dark/Theme.qml
new file mode 100644 (file)
index 0000000..7757466
--- /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, 5)
+    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/pentobi/qml/themes/dark/pentobi-rated-game.svg b/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/pentobi/qml/themes/dark/piece-manipulator-desktop-legal.svg b/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/pentobi/qml/themes/dark/piece-manipulator-desktop.svg b/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/pentobi/qml/themes/dark/piece-manipulator-legal.svg b/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/pentobi/qml/themes/dark/piece-manipulator.svg b/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/pentobi/qml/themes/light/Theme.qml b/pentobi/qml/themes/light/Theme.qml
new file mode 100644 (file)
index 0000000..c783738
--- /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: "#eff0f1"
+    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/pentobi/qml/themes/light/menu-desktop.svg b/pentobi/qml/themes/light/menu-desktop.svg
new file mode 100644 (file)
index 0000000..3059eee
--- /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="b" d="m2 3h12" fill="none" stroke="#000" stroke-linecap="round" stroke-width="2"/>
+ <use id="a" transform="translate(0,5)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(0,5)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
diff --git a/pentobi/qml/themes/light/menu.svg b/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/pentobi/qml/themes/light/pentobi-backward.svg b/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/pentobi/qml/themes/light/pentobi-backward10.svg b/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/pentobi/qml/themes/light/pentobi-beginning.svg b/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/pentobi/qml/themes/light/pentobi-computer-colors.svg b/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/pentobi/qml/themes/light/pentobi-end.svg b/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/pentobi/qml/themes/light/pentobi-forward.svg b/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/pentobi/qml/themes/light/pentobi-forward10.svg b/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/pentobi/qml/themes/light/pentobi-newgame.svg b/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/pentobi/qml/themes/light/pentobi-next-variation.svg b/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/pentobi/qml/themes/light/pentobi-play.svg b/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/pentobi/qml/themes/light/pentobi-previous-variation.svg b/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/pentobi/qml/themes/light/pentobi-rated-game.svg b/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/pentobi/qml/themes/light/pentobi-stop.svg b/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/pentobi/qml/themes/light/pentobi-undo.svg b/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/pentobi/qml/themes/light/piece-manipulator-desktop-legal.svg b/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/pentobi/qml/themes/light/piece-manipulator-desktop.svg b/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/pentobi/qml/themes/light/piece-manipulator-legal.svg b/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/pentobi/qml/themes/light/piece-manipulator.svg b/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/pentobi/qml/themes/system/Theme.qml b/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/pentobi/qml/themes/themes.qrc b/pentobi/qml/themes/themes.qrc
new file mode 100644 (file)
index 0000000..7f32067
--- /dev/null
@@ -0,0 +1,34 @@
+<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/menu-desktop.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/pentobi/resources.qrc b/pentobi/resources.qrc
new file mode 100644 (file)
index 0000000..b40d2ea
--- /dev/null
@@ -0,0 +1,80 @@
+<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/AnalyzeDialog.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/ComboBox.qml</file>
+        <file>qml/ComputerDialog.qml</file>
+        <file>qml/Comment.qml</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/GameViewMobile.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/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/GameView.js</file>
+    </qresource>
+</RCC>
diff --git a/pentobi/resources_desktop.qrc b/pentobi/resources_desktop.qrc
new file mode 100644 (file)
index 0000000..30601e5
--- /dev/null
@@ -0,0 +1,6 @@
+<RCC>
+    <qresource prefix="/">
+        <file>qml/GameViewDesktop.qml</file>
+        <file>qml/PieceSelectorDesktop.qml</file>
+    </qresource>
+</RCC>
diff --git a/pentobi/unix/CMakeLists.txt b/pentobi/unix/CMakeLists.txt
new file mode 100644 (file)
index 0000000..06d3aed
--- /dev/null
@@ -0,0 +1,165 @@
+find_package(Gettext 0.18 REQUIRED)
+find_package(DocBookXSL REQUIRED)
+find_program(ITSTOOL itstool)
+if(NOT ITSTOOL)
+    message(FATAL_ERROR "itstool not found")
+endif()
+get_filename_component(GETTEXT_BIN_DIR ${GETTEXT_MSGFMT_EXECUTABLE} DIRECTORY)
+get_filename_component(GETTEXT_INSTALL_DIR ${GETTEXT_BIN_DIR} DIRECTORY)
+find_file(METAINFO_ITS NAMES metainfo.its appdata.its
+    HINTS ${GETTEXT_INSTALL_DIR}/share/gettext/its
+    )
+if(NOT METAINFO_ITS)
+    message(FATAL_ERROR
+        "metainfo.its not found. Install appstream and/or use"
+        " -DMETAINFO_ITS=<file> to define the location of the metainfo.its"
+        " or appdata.its file."
+        )
+endif()
+find_program(RSVG_CONVERT rsvg-convert)
+if(NOT RSVG_CONVERT)
+    message(FATAL_ERROR "rsvg-convert not found (install librsvg2-bin)")
+endif()
+find_program(XSLTPROC xsltproc)
+if(NOT XSLTPROC)
+    message(FATAL_ERROR "xsltproc not found")
+endif()
+
+# Binary gettext files
+
+file(READ "${CMAKE_CURRENT_SOURCE_DIR}/po/LINGUAS" linguas)
+string(REGEX REPLACE "\n" ";" linguas "${linguas}")
+
+foreach(lang ${linguas})
+    add_custom_command(OUTPUT ${lang}.mo
+        COMMAND "${GETTEXT_MSGFMT_EXECUTABLE}" -o ${lang}.mo
+        "${CMAKE_CURRENT_SOURCE_DIR}/po/${lang}.po"
+        DEPENDS po/${lang}.po
+        )
+    list(APPEND po_files po/${lang}.po)
+    list(APPEND mo_files ${lang}.mo)
+endforeach()
+
+# Icons
+
+add_custom_command(OUTPUT pentobi.png
+    COMMAND "${RSVG_CONVERT}"
+    "${CMAKE_CURRENT_SOURCE_DIR}/../icon/pentobi-48.svg"
+    > pentobi.png
+    DEPENDS ../icon/pentobi-48.svg)
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pentobi.png"
+    DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps)
+install(FILES ../icon/pentobi-128.svg
+    DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps
+    RENAME pentobi.svg)
+
+add_custom_command(OUTPUT application-x-blokus-sgf.png
+    COMMAND "${RSVG_CONVERT}"
+    "${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf-48.svg"
+    > application-x-blokus-sgf.png
+    DEPENDS application-x-blokus-sgf-48.svg)
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/application-x-blokus-sgf.png"
+    DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/mimetypes)
+install(FILES application-x-blokus-sgf-128.svg
+    DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/mimetypes
+    RENAME application-x-blokus-sgf.svg)
+
+# Desktop entry
+
+configure_file(io.sourceforge.pentobi.desktop.in
+    io.sourceforge.pentobi.desktop.in.2 @ONLY)
+add_custom_command(OUTPUT io.sourceforge.pentobi.desktop
+    COMMAND "${GETTEXT_MSGFMT_EXECUTABLE}" --desktop
+    -d "${CMAKE_CURRENT_SOURCE_DIR}/po"
+    --template io.sourceforge.pentobi.desktop.in.2
+    -o io.sourceforge.pentobi.desktop
+    DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/io.sourceforge.pentobi.desktop.in.2"
+    ${po_files} po/LINGUAS)
+
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/io.sourceforge.pentobi.desktop
+    DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications")
+
+# AppData
+
+configure_file(io.sourceforge.pentobi.appdata.xml.in
+    io.sourceforge.pentobi.appdata.xml @ONLY)
+add_custom_command(OUTPUT io.sourceforge.pentobi.appdata.translated.xml
+    COMMAND "${GETTEXT_MSGFMT_EXECUTABLE}" --xml
+    -d "${CMAKE_CURRENT_SOURCE_DIR}/po"
+    --template io.sourceforge.pentobi.appdata.xml
+    -o io.sourceforge.pentobi.appdata.translated.xml
+    DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/io.sourceforge.pentobi.appdata.xml"
+    ${po_files} po/LINGUAS
+    )
+
+install(
+    FILES
+    "${CMAKE_CURRENT_BINARY_DIR}/io.sourceforge.pentobi.appdata.translated.xml"
+    DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo"
+    RENAME io.sourceforge.pentobi.appdata.xml
+    )
+
+# Shared MIME info
+
+# Use itstool until shared-mime-info includes its/loc files for gettext (see
+# also https://gitlab.freedesktop.org/xdg/shared-mime-info/merge_requests/4)
+add_custom_command(OUTPUT pentobi-mime.xml
+    COMMAND "${ITSTOOL}"
+    -j "${CMAKE_CURRENT_SOURCE_DIR}/pentobi-mime.xml.in"
+    -o pentobi-mime.xml ${mo_files}
+    DEPENDS pentobi-mime.xml.in ${mo_files} po/LINGUAS
+    )
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pentobi-mime.xml"
+    DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/mime/packages")
+
+# Man page
+
+set(man_files "pentobi.6")
+foreach(lang ${linguas})
+    list(APPEND man_files "${lang}/pentobi.6")
+endforeach()
+
+configure_file(pentobi-manpage.docbook.in pentobi-manpage.docbook @ONLY)
+add_custom_command(OUTPUT pentobi.6
+    COMMAND "${XSLTPROC}" --nonet --novalid --path "${DOCBOOKXSL_DIR}/manpages"
+    "${CMAKE_CURRENT_SOURCE_DIR}/manpage.xsl" pentobi-manpage.docbook
+    DEPENDS manpage.xsl "${CMAKE_CURRENT_BINARY_DIR}/pentobi-manpage.docbook"
+    )
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pentobi.6"
+    DESTINATION "${CMAKE_INSTALL_MANDIR}/man6")
+foreach(lang ${linguas})
+    add_custom_command(OUTPUT ${lang}/pentobi-manpage.docbook
+        COMMAND ${CMAKE_COMMAND} -E make_directory ${lang}
+        COMMAND "${ITSTOOL}" -l ${lang} -m ${lang}.mo
+        -o ${lang}/pentobi-manpage.docbook
+        -i "${CMAKE_CURRENT_SOURCE_DIR}/manpage.its"
+        pentobi-manpage.docbook
+        DEPENDS ${lang}.mo "${CMAKE_CURRENT_BINARY_DIR}/pentobi-manpage.docbook"
+        )
+    add_custom_command(OUTPUT ${lang}/pentobi.6
+        COMMAND "${XSLTPROC}" --nonet --novalid
+        --path "${DOCBOOKXSL_DIR}/manpages" -o ${lang}/
+        "${CMAKE_CURRENT_SOURCE_DIR}/manpage.xsl"
+        ${lang}/pentobi-manpage.docbook
+        DEPENDS manpage.xsl ${lang}/pentobi-manpage.docbook
+        )
+    install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${lang}/pentobi.6"
+        DESTINATION "${CMAKE_INSTALL_MANDIR}/${lang}/man6")
+endforeach()
+
+# User manual
+
+install(DIRECTORY help DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}
+    FILES_MATCHING PATTERN "*.css" PATTERN "*.html" PATTERN "*.png"
+    PATTERN "*.jpg")
+
+# Target
+
+add_custom_target(pentobi-unix ALL DEPENDS
+    io.sourceforge.pentobi.desktop
+    io.sourceforge.pentobi.appdata.translated.xml
+    pentobi-mime.xml
+    pentobi.png
+    application-x-blokus-sgf.png
+    ${man_files}
+    )
diff --git a/pentobi/unix/application-x-blokus-sgf-128.svg b/pentobi/unix/application-x-blokus-sgf-128.svg
new file mode 100644 (file)
index 0000000..c186681
--- /dev/null
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="128" height="128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect x="16.89" y="8.8899" width="94.22" height="110.22" ry="2.3046" fill="#f6f5f4" stroke="#b0ada6" stroke-linejoin="round" stroke-width="3.7798"/>
+ <path d="m50 64h28v-14h-14v-14h-23.692c-2.1538 0-4.3077 2.1538-4.3077 4.3077v9.6922h14z" fill="#e01b24" fill-rule="evenodd" stroke-width=".53846"/>
+ <path id="h" d="m36 50 2.1538-2.1538h9.6923l2.1538 2.1538z" fill="#c6161f" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(14)" width="100%" height="100%" xlink:href="#h"/>
+ <use transform="translate(14 14)" width="100%" height="100%" xlink:href="#h"/>
+ <use transform="translate(28 14)" width="100%" height="100%" xlink:href="#h"/>
+ <path id="n" d="m50 36 2.1538 2.1538h9.6923l2.1538-2.1538z" fill="#e73b43" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(0 14)" width="100%" height="100%" xlink:href="#n"/>
+ <use transform="translate(14 14)" width="100%" height="100%" xlink:href="#n"/>
+ <path id="m" d="m50 36 2.1538 2.1538v9.6922l-2.1538 2.1538z" fill="#e6323a" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(0 14)" width="100%" height="100%" xlink:href="#m"/>
+ <use transform="translate(14 14)" width="100%" height="100%" xlink:href="#m"/>
+ <path id="c" d="m50 50-2.1538-2.1538v-9.6922l2.1538-2.1538z" fill="#cd1922" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(14 14)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="translate(14)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="translate(28 14)" width="100%" height="100%" xlink:href="#c"/>
+ <g fill-rule="evenodd" stroke-width=".53846">
+  <path d="m37.348 37.369 0.80613 0.78491v9.6922l-2.1538 2.1538v-9.6922c0.01119-1.7451 1.3477-2.9387 1.3477-2.9387z" fill="#e6323a"/>
+  <path d="m37.364 37.361 0.78992 0.79299h9.6923l2.1538-2.1538h-9.6923c-1.802 0.02433-2.9438 1.3608-2.9438 1.3608z" fill="#e73b42"/>
+  <path d="m64 36h23.692c2.1338 0.022943 4.2848 2.1969 4.3077 4.3077v37.692h-14v-28h-14z" fill="#2ec27e"/>
+  <path id="g" d="m64 50 2.1538-2.1538h9.6923l2.1538 2.1538z" fill="#2ab073" fill-rule="evenodd" stroke-width=".53846"/>
+ </g>
+ <use transform="translate(14)" width="100%" height="100%" xlink:href="#g"/>
+ <use transform="translate(14 14)" width="100%" height="100%" xlink:href="#g"/>
+ <use transform="translate(14 28)" width="100%" height="100%" xlink:href="#g"/>
+ <path id="f" d="m64 36 2.1538 2.1538v9.6922l-2.1538 2.1538z" fill="#30ca85" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(14)" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(14 28)" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(14 14)" width="100%" height="100%" xlink:href="#f"/>
+ <path id="p" d="m64 36 2.1538 2.1538h9.6923l2.1538-2.1538z" fill="#4fd398" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(14 14)" width="100%" height="100%" xlink:href="#p"/>
+ <use transform="translate(14 28)" width="100%" height="100%" xlink:href="#p"/>
+ <path id="o" d="m78 36-2.1538 2.1538v9.6922l2.1538 2.1538z" fill="#2fbb7c" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(14 14)" width="100%" height="100%" xlink:href="#o"/>
+ <use transform="translate(14 28)" width="100%" height="100%" xlink:href="#o"/>
+ <g transform="matrix(.53846 0 0 .53846 29.538 29.539)" fill-rule="evenodd">
+  <path d="m90 12 4 4h18l1.4274-1.4594s-2.4274-2.5406-5.4274-2.5406z" fill="#4fd398"/>
+  <path d="m116 20c-0.0151-2.9828-2.5466-5.4534-2.5466-5.4534l-1.4534 1.4534v18l4 4z" fill="#2fbb7c"/>
+  <path d="m12 38v70c0.0213 4.0053 4 7.9997 7.984 7.9997l44.015-2e-3 9.99e-4 -25.998h-26v-52z" fill="#f6d32d"/>
+  <path id="j" d="m38 64h-26l4-4h18z" fill="#e1bf09" fill-rule="evenodd"/>
+ </g>
+ <use transform="matrix(.53846 0 0 .53846 29.538 43.539)" width="100%" height="100%" xlink:href="#j"/>
+ <use transform="matrix(.53846 0 0 .53846 43.538 57.538)" width="100%" height="100%" xlink:href="#j"/>
+ <path id="a" d="m50 50h-14l2.1538 2.1538h9.6923z" fill="#f9e263" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(0 14)" width="100%" height="100%" xlink:href="#a"/>
+ <use transform="translate(14 28)" width="100%" height="100%" xlink:href="#a"/>
+ <path id="i" d="m36 64v-14l2.1538 2.1538v9.6922z" fill="#f7da51" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(0 14)" width="100%" height="100%" xlink:href="#i"/>
+ <use transform="translate(14 28)" width="100%" height="100%" xlink:href="#i"/>
+ <path id="b" d="m50 50v14l-2.1538-2.1538v-9.6922z" fill="#efc70b" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(0 14)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(14 28)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(0 28)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(0 28)" width="100%" height="100%" xlink:href="#a"/>
+ <g transform="matrix(.53846 0 0 .53846 29.538 29.539)" fill-rule="evenodd">
+  <path d="m12 108v-18l4 4v18l-1.4487 1.4487s-2.5087-1.9547-2.5513-5.4487z" fill="#f7da51"/>
+  <path d="m38 116h-18c-3.1531 0.0426-5.4487-2.5513-5.4487-2.5513l1.4487-1.4487h18z" fill="#e0bb0a"/>
+  <path d="m38 64h52v26h26v17.931c0 4.1332-3.9521 8.1118-8 8.0692h-44v-26h-26z" fill="#1c71d8"/>
+  <path id="l" d="m38 90 4-4h18l4 4z" fill="#1a68c7" fill-rule="evenodd"/>
+ </g>
+ <use transform="matrix(.53846 0 0 .53846 43.538 29.539)" width="100%" height="100%" xlink:href="#l"/>
+ <use transform="matrix(.53846 0 0 .53846 43.538 43.537)" width="100%" height="100%" xlink:href="#l"/>
+ <path d="m77.999 92h9.6923c1.6978 0.02294 2.9339-1.3738 2.9339-1.3738l-0.78007-0.78006h-9.6923z" fill="#1a68c7" fill-rule="evenodd" stroke-width=".53846"/>
+ <path id="e" d="m50 64 2.1538 2.1538h9.6923l2.1538-2.1538z" fill="#3585e5" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(14)" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(14 14)" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(28 14)" width="100%" height="100%" xlink:href="#e"/>
+ <path id="d" d="m50 78 2.1538-2.1538v-9.6922l-2.1538-2.1538z" fill="#2078e2" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(14)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(14 14)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(27.999 13.999)" width="100%" height="100%" xlink:href="#d"/>
+ <path id="k" d="m64 78-2.1538-2.1538v-9.6922l2.1538-2.1538z" fill="#1b6bce" fill-rule="evenodd" stroke-width=".53846"/>
+ <use transform="translate(14)" width="100%" height="100%" xlink:href="#k"/>
+ <use transform="translate(14 14)" width="100%" height="100%" xlink:href="#k"/>
+ <path d="m92 87.692v-9.6922l-2.1538 2.1538v9.6922l0.78007 0.78006s1.3508-1.0525 1.3738-2.9339z" fill="#1b6bce" fill-rule="evenodd" stroke-width=".53846"/>
+</svg>
diff --git a/pentobi/unix/application-x-blokus-sgf-48.svg b/pentobi/unix/application-x-blokus-sgf-48.svg
new file mode 100644 (file)
index 0000000..e88cbbb
--- /dev/null
@@ -0,0 +1,78 @@
+<?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">
+ <rect x="5.5003" y="2.4997" width="37" height="43" ry=".89909" fill="#f6f5f4" stroke="#b0ada6" stroke-linejoin="round"/>
+ <path d="m19 24h10v-5h-5v-5h-8.4615c-0.76923 0-1.5385 0.76922-1.5385 1.5384v3.4615h5z" fill="#e01b24" fill-rule="evenodd" stroke-width=".19231"/>
+ <path id="h" d="m14 19 0.76923-0.76922h3.4615l0.76923 0.76922z" fill="#c6161f" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(5)" width="100%" height="100%" xlink:href="#h"/>
+ <use transform="translate(5 5)" width="100%" height="100%" xlink:href="#h"/>
+ <use transform="translate(10 5)" width="100%" height="100%" xlink:href="#h"/>
+ <path id="n" d="m19 14 0.76923 0.76922h3.4615l0.76923-0.76922z" fill="#e73b43" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(0 5)" width="100%" height="100%" xlink:href="#n"/>
+ <use transform="translate(5 5)" width="100%" height="100%" xlink:href="#n"/>
+ <path id="m" d="m19 14 0.76923 0.76922v3.4615l-0.76923 0.76922z" fill="#e6323a" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(0 5)" width="100%" height="100%" xlink:href="#m"/>
+ <use transform="translate(5 5)" width="100%" height="100%" xlink:href="#m"/>
+ <path id="c" d="m19 19-0.76923-0.76922v-3.4615l0.76923-0.76922z" fill="#cd1922" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(5 5)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="translate(5)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="translate(10 5)" width="100%" height="100%" xlink:href="#c"/>
+ <g fill-rule="evenodd" stroke-width=".19231">
+  <path d="m14.481 14.489 0.2879 0.28032v3.4615l-0.76923 0.76922v-3.4615c4e-3 -0.62326 0.48133-1.0495 0.48133-1.0495z" fill="#e6323a"/>
+  <path d="m14.487 14.486 0.28212 0.28321h3.4615l0.76923-0.76922h-3.4615c-0.64356 0.0087-1.0513 0.48602-1.0513 0.48602z" fill="#e73b42"/>
+  <path d="m24 14h8.4615c0.76206 0.0082 1.5303 0.78459 1.5385 1.5384v13.461h-5v-9.9999h-5z" fill="#2ec27e"/>
+  <path id="g" d="m24 19 0.76923-0.76922h3.4615l0.76923 0.76922z" fill="#2ab073" fill-rule="evenodd" stroke-width=".19231"/>
+ </g>
+ <use transform="translate(5)" width="100%" height="100%" xlink:href="#g"/>
+ <use transform="translate(5 5)" width="100%" height="100%" xlink:href="#g"/>
+ <use transform="translate(5 9.9999)" width="100%" height="100%" xlink:href="#g"/>
+ <path id="f" d="m24 14 0.76923 0.76922v3.4615l-0.76923 0.76922z" fill="#30ca85" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(5)" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(5 9.9999)" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(5 5)" width="100%" height="100%" xlink:href="#f"/>
+ <path id="p" d="m24 14 0.76923 0.76922h3.4615l0.76923-0.76922z" fill="#4fd398" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(5 5)" width="100%" height="100%" xlink:href="#p"/>
+ <use transform="translate(5 9.9999)" width="100%" height="100%" xlink:href="#p"/>
+ <path id="o" d="m29 14-0.76923 0.76922v3.4615l0.76923 0.76922z" fill="#2fbb7c" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(5 5)" width="100%" height="100%" xlink:href="#o"/>
+ <use transform="translate(5 9.9999)" width="100%" height="100%" xlink:href="#o"/>
+ <g transform="matrix(.19231 0 0 .19231 11.692 11.692)" fill-rule="evenodd">
+  <path d="m90 12 4 4h18l1.4274-1.4594s-2.4274-2.5406-5.4274-2.5406z" fill="#4fd398"/>
+  <path d="m116 20c-0.0151-2.9828-2.5466-5.4534-2.5466-5.4534l-1.4534 1.4534v18l4 4z" fill="#2fbb7c"/>
+  <path d="m12 38v70c0.0213 4.0053 4 7.9997 7.984 7.9997l44.015-2e-3 9.99e-4 -25.998h-26v-52z" fill="#f6d32d"/>
+  <path id="j" d="m38 64h-26l4-4h18z" fill="#e1bf09" fill-rule="evenodd"/>
+ </g>
+ <use transform="matrix(.19231 0 0 .19231 11.692 16.692)" width="100%" height="100%" xlink:href="#j"/>
+ <use transform="matrix(.19231 0 0 .19231 16.692 21.692)" width="100%" height="100%" xlink:href="#j"/>
+ <path id="a" d="m19 19h-5l0.76923 0.76922h3.4615z" fill="#f9e263" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(0 5)" width="100%" height="100%" xlink:href="#a"/>
+ <use transform="translate(5 9.9999)" width="100%" height="100%" xlink:href="#a"/>
+ <path id="i" d="m14 24v-5l0.76923 0.76922v3.4615z" fill="#f7da51" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(0 5)" width="100%" height="100%" xlink:href="#i"/>
+ <use transform="translate(5 9.9999)" width="100%" height="100%" xlink:href="#i"/>
+ <path id="b" d="m19 19v5l-0.76923-0.76922v-3.4615z" fill="#efc70b" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(0 5)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(5 9.9999)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(0 9.9999)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(0 9.9999)" width="100%" height="100%" xlink:href="#a"/>
+ <g transform="matrix(.19231 0 0 .19231 11.692 11.692)" fill-rule="evenodd">
+  <path d="m12 108v-18l4 4v18l-1.4487 1.4487s-2.5087-1.9547-2.5513-5.4487z" fill="#f7da51"/>
+  <path d="m38 116h-18c-3.1531 0.0426-5.4487-2.5513-5.4487-2.5513l1.4487-1.4487h18z" fill="#e0bb0a"/>
+  <path d="m38 64h52v26h26v17.931c0 4.1332-3.9521 8.1118-8 8.0692h-44v-26h-26z" fill="#1c71d8"/>
+  <path id="l" d="m38 90 4-4h18l4 4z" fill="#1a68c7" fill-rule="evenodd"/>
+ </g>
+ <use transform="matrix(.19231 0 0 .19231 16.692 11.692)" width="100%" height="100%" xlink:href="#l"/>
+ <use transform="matrix(.19231 0 0 .19231 16.692 16.692)" width="100%" height="100%" xlink:href="#l"/>
+ <path d="m29 34h3.4615c0.60637 0.0082 1.0478-0.49063 1.0478-0.49063l-0.2786-0.27859h-3.4615z" fill="#1a68c7" fill-rule="evenodd" stroke-width=".19231"/>
+ <path id="e" d="m19 24 0.76923 0.76922h3.4615l0.76923-0.76922z" fill="#3585e5" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(5)" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(5 5)" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(10 5)" width="100%" height="100%" xlink:href="#e"/>
+ <path id="d" d="m19 29 0.76923-0.76922v-3.4615l-0.76923-0.76922z" fill="#2078e2" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(5)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(5 5)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(9.9998 4.9996)" width="100%" height="100%" xlink:href="#d"/>
+ <path id="k" d="m24 29-0.76923-0.76922v-3.4615l0.76923-0.76922z" fill="#1b6bce" fill-rule="evenodd" stroke-width=".19231"/>
+ <use transform="translate(5)" width="100%" height="100%" xlink:href="#k"/>
+ <use transform="translate(5 5)" width="100%" height="100%" xlink:href="#k"/>
+ <path d="m34 32.461v-3.4615l-0.76923 0.76922v3.4615l0.2786 0.27859s0.48244-0.3759 0.49064-1.0478z" fill="#1b6bce" fill-rule="evenodd" stroke-width=".19231"/>
+</svg>
diff --git a/pentobi/unix/create-pot b/pentobi/unix/create-pot
new file mode 100755 (executable)
index 0000000..d47c590
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+cd $(dirname "$0")
+itstool -i manpage.its pentobi-manpage.docbook.in > pentobi-unix.pot
+xgettext -j -o pentobi-unix.pot --add-comments="TRANSLATORS:" \
+  io.sourceforge.pentobi.appdata.xml.in\
+  io.sourceforge.pentobi.desktop.in
+xgettext -j -o pentobi-unix.pot --its=mime.its pentobi-mime.xml.in
diff --git a/pentobi/unix/io.sourceforge.pentobi.appdata.xml.in b/pentobi/unix/io.sourceforge.pentobi.appdata.xml.in
new file mode 100644 (file)
index 0000000..456de60
--- /dev/null
@@ -0,0 +1,74 @@
+<?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>
+<summary>Computer opponent for the board game Blokus</summary>
+<description>
+<p>
+Pentobi is a computer opponent for the board game Blokus. It has a
+strong Blokus engine with different playing levels. The supported game
+variants are Classic, Duo, Trigon, Junior, Nexos, Callisto and 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>
+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>
+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>
+</description>
+<launchable type="desktop-id">io.sourceforge.pentobi.desktop</launchable>
+<screenshots>
+<screenshot type="default">
+<image width="1248" height="702">
+https://pentobi.sourceforge.io/pentobi-classic.png</image>
+<caption>Game variant Classic</caption>
+</screenshot>
+<screenshot>
+<image width="1248" height="702">
+https://pentobi.sourceforge.io/pentobi-duo.png</image>
+<caption>Game variant Duo</caption>
+</screenshot>
+<screenshot>
+<image width="1248" height="702">
+https://pentobi.sourceforge.io/pentobi-trigon.png</image>
+<caption>Game variant Trigon</caption>
+</screenshot>
+<screenshot>
+<image width="1248" height="702">
+https://pentobi.sourceforge.io/pentobi-nexos.png</image>
+<caption>Game variant Nexos</caption>
+</screenshot>
+<screenshot>
+<image width="1248" height="702">
+https://pentobi.sourceforge.io/pentobi-gembloq.png</image>
+<caption>Game variant GembloQ</caption>
+</screenshot>
+</screenshots>
+<url type="homepage">https://pentobi.sourceforge.io/</url>
+<url type="bugtracker">https://github.com/enz/pentobi/issues</url>
+<url type="donation">https://sourceforge.net/p/pentobi/donate/</url>
+<url type="translate">https://www.transifex.com/markus-enzenberger/pentobi/</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>
+<content_rating type="oars-1.1"/>
+<releases>
+<release version="@PENTOBI_VERSION@" date="@PENTOBI_RELEASE_DATE@"/>
+</releases>
+</component>
diff --git a/pentobi/unix/io.sourceforge.pentobi.desktop.in b/pentobi/unix/io.sourceforge.pentobi.desktop.in
new file mode 100644 (file)
index 0000000..614c2b8
--- /dev/null
@@ -0,0 +1,13 @@
+[Desktop Entry]
+Name=Pentobi
+GenericName=Computer opponent for Blokus
+Comment=Computer opponent for the board game Blokus
+# TRANSLATORS: keywords in desktop entry, separate with semicolons
+Keywords=Blokus;Blokus Duo;Blokus Trigon;Blokus Junior;Nexos;Callisto;Gemblo Q;GembloQ
+Exec=@CMAKE_INSTALL_FULL_BINDIR@/pentobi %f
+# TRANSLATORS: icon name in desktop entry, probably no need to change this
+Icon=pentobi
+Type=Application
+Categories=Game;BoardGame;
+MimeType=application/x-blokus-sgf;
+StartupWMClass=Pentobi
diff --git a/pentobi/unix/manpage.its b/pentobi/unix/manpage.its
new file mode 100644 (file)
index 0000000..eb195de
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<its:rules xmlns:its="http://www.w3.org/2005/11/its" version="2.0">
+<its:translateRule selector="//refentrytitle" translate="no"/>
+<its:translateRule selector="//manvolnum" translate="no"/>
+<its:translateRule selector="//refmiscinfo[@class='version']" translate="no"/>
+<its:translateRule selector="//articleinfo/date" translate="no"/>
+<its:translateRule selector="//refname" translate="no"/>
+<its:translateRule selector="//command" translate="no"/>
+<its:translateRule selector="//option" translate="no"/>
+</its:rules>
diff --git a/pentobi/unix/manpage.xsl b/pentobi/unix/manpage.xsl
new file mode 100644 (file)
index 0000000..afa0806
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version='1.0'?>
+<xsl:stylesheet
+xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  version="1.0">
+<xsl:import href="docbook.xsl"/>
+<xsl:param name="refentry.meta.get.quietly" select="1"/>
+</xsl:stylesheet>
diff --git a/pentobi/unix/mime.its b/pentobi/unix/mime.its
new file mode 100644 (file)
index 0000000..82a59a8
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0"?>
+<its:rules xmlns:its="http://www.w3.org/2005/11/its" version="2.0">
+<its:translateRule selector="/mime-info" translate="no"/>
+<its:translateRule selector="/mime-info/mime-type/comment" translate="yes"/>
+</its:rules>
diff --git a/pentobi/unix/pentobi-manpage.docbook.in b/pentobi/unix/pentobi-manpage.docbook.in
new file mode 100644 (file)
index 0000000..a802b57
--- /dev/null
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
+<article>
+<articleinfo><date>@PENTOBI_RELEASE_DATE@</date></articleinfo>
+
+<refentry>
+<refmeta>
+<refentrytitle>pentobi</refentrytitle>
+<manvolnum>6</manvolnum>
+<refmiscinfo class="source">Pentobi</refmiscinfo>
+<refmiscinfo class="version">@PENTOBI_VERSION@</refmiscinfo>
+<refmiscinfo class="manual">Pentobi Command Reference</refmiscinfo>
+</refmeta>
+<refnamediv>
+<refname>pentobi</refname>
+<refpurpose>
+computer opponent for the board game Blokus
+</refpurpose>
+</refnamediv>
+
+<refsynopsisdiv>
+<cmdsynopsis>
+<command>pentobi</command>
+<arg><option>--maxlevel</option> <replaceable>n</replaceable></arg>
+<arg><option>--mobile</option></arg>
+<arg><option>--nobook</option></arg>
+<arg><option>--nodelay</option></arg>
+<arg><option>--seed</option> <replaceable>n</replaceable></arg>
+<arg><option>--threads</option> <replaceable>n</replaceable></arg>
+<arg><option>--verbose</option></arg>
+<arg><replaceable>file.blksgf</replaceable></arg>
+</cmdsynopsis>
+<cmdsynopsis>
+<command>pentobi</command>
+<group choice="plain">
+<arg choice="plain"><option>-h</option></arg>
+<arg choice="plain"><option>--help</option></arg>
+</group>
+</cmdsynopsis>
+<cmdsynopsis>
+<command>pentobi</command>
+<group choice="plain">
+<arg choice="plain"><option>-v</option></arg>
+<arg choice="plain"><option>--version</option></arg>
+</group>
+</cmdsynopsis>
+</refsynopsisdiv>
+
+<refsection>
+<title>Description</title>
+<para>
+<command>pentobi</command> is the command to invoke the program Pentobi,
+which is a graphical user interface and computer opponent for the board
+game Blokus.
+</para>
+<para>
+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 Pentobi-SGF.md in the Pentobi source package.
+</para>
+</refsection>
+
+<refsection>
+<title>Options</title>
+<variablelist>
+<varlistentry>
+<term><option>-h</option></term>
+<term><option>--help</option></term>
+<listitem>
+<para>
+Display help and exit.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><option>--maxlevel</option> <replaceable>n</replaceable></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><option>--mobile</option></term>
+<listitem>
+<para>
+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 <option>-style</option>.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><option>--nobook</option></term>
+<listitem>
+<para>
+Do not use opening books.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><option>--nodelay</option></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><option>--seed</option> <replaceable>n</replaceable></term>
+<listitem>
+<para>
+Set the seed for the random generator. Using a fixed seed makes the move
+generation deterministic if no multi-threading is used (see option
+<option>--threads</option>).
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><option>--threads</option> <replaceable>n</replaceable></term>
+<listitem>
+<para>
+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.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><option>--verbose</option></term>
+<listitem>
+<para>
+Print internal information about the move generation and other debugging
+information to standard error.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</refsection>
+
+<refsection>
+<title>Standard Qt Options</title>
+<para>
+Additionally, any options supported by Qt applications can be used,
+such as:
+</para>
+<variablelist>
+<varlistentry>
+<term><option>-display</option> <replaceable>d</replaceable></term>
+<listitem>
+<para>
+Switches displays on X11.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><option>-geometry</option> <replaceable>g</replaceable></term>
+<listitem>
+<para>
+Window geometry using the X11 syntax.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><option>-style</option> <replaceable>s</replaceable></term>
+<listitem>
+<para>
+Set the style for the GUI elements of QQuickControls.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><option>-v</option></term>
+<term><option>--version</option></term>
+<listitem>
+<para>
+Display version and exit.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</refsection>
+
+<refsection>
+<title>See Also</title>
+<para>
+<command>pentobi-thumbnailer</command>
+</para>
+</refsection>
+
+</refentry>
+</article>
diff --git a/pentobi/unix/pentobi-mime.xml.in b/pentobi/unix/pentobi-mime.xml.in
new file mode 100644 (file)
index 0000000..f1df9f9
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
+<mime-type type="application/x-blokus-sgf">
+<comment>Blokus game</comment>
+<magic priority="60">
+<match type="string" offset="2:10" value="GM[Blokus]"/>
+<match type="string" offset="2:10" value="GM[Blokus Duo]"/>
+<match type="string" offset="2:10" value="GM[Blokus Junior]"/>
+<match type="string" offset="2:10" value="GM[Blokus Three-Player]"/>
+<match type="string" offset="2:10" value="GM[Blokus Trigon]"/>
+<match type="string" offset="2:10" value="GM[Blokus Trigon Three-Player]"/>
+<match type="string" offset="2:10" value="GM[Blokus Trigon Two-Player]"/>
+<match type="string" offset="2:10" value="GM[Blokus Two-Player]"/>
+<match type="string" offset="2:10" value="GM[Callisto]"/>
+<match type="string" offset="2:10" value="GM[Callisto Three-Player]"/>
+<match type="string" offset="2:10" value="GM[Callisto Two-Player]"/>
+<match type="string" offset="2:10" value="GM[Callisto Two-Player Four-Color]"/>
+<match type="string" offset="2:10" value="GM[GembloQ]"/>
+<match type="string" offset="2:10" value="GM[GembloQ Three-Player]"/>
+<match type="string" offset="2:10" value="GM[GembloQ Two-Player]"/>
+<match type="string" offset="2:10" value="GM[GembloQ Two-Player Four-Color]"/>
+<match type="string" offset="2:10" value="GM[Nexos]"/>
+<match type="string" offset="2:10" value="GM[Nexos Two-Player]"/>
+</magic>
+<sub-class-of type="text/plain"/>
+<glob pattern="*.blksgf"/>
+</mime-type>
+</mime-info>
diff --git a/pentobi/unix/pentobi-unix.pot b/pentobi/unix/pentobi-unix.pot
new file mode 100644 (file)
index 0000000..db2b07f
--- /dev/null
@@ -0,0 +1,272 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-03-09 10:33+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Put one translator per line, in the form NAME <EMAIL>, YEAR1, YEAR2
+msgctxt "_"
+msgid "translator-credits"
+msgstr ""
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-manpage.docbook.in:11 io.sourceforge.pentobi.appdata.xml.in:6
+#: io.sourceforge.pentobi.desktop.in:3
+msgid "Pentobi"
+msgstr ""
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-manpage.docbook.in:13
+msgid "Pentobi Command Reference"
+msgstr ""
+
+#. (itstool) path: refnamediv/refpurpose
+#: pentobi-manpage.docbook.in:17
+msgid "computer opponent for the board game Blokus"
+msgstr ""
+
+#. (itstool) path: refsynopsisdiv/cmdsynopsis
+#: pentobi-manpage.docbook.in:23
+msgid ""
+"<_:command-1/> <arg><_:option-2/> <replaceable>n</replaceable></arg> <arg><_:"
+"option-3/></arg> <arg><_:option-4/></arg> <arg><_:option-5/></arg> <arg><_:"
+"option-6/> <replaceable>n</replaceable></arg> <arg><_:option-7/> "
+"<replaceable>n</replaceable></arg> <arg><_:option-8/></arg> "
+"<arg><replaceable>file.blksgf</replaceable></arg>"
+msgstr ""
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:51
+msgid "Description"
+msgstr ""
+
+#. (itstool) path: refsection/para
+#: pentobi-manpage.docbook.in:52
+msgid ""
+"<_:command-1/> is the command to invoke the program Pentobi, which is a "
+"graphical user interface and computer opponent for the board game Blokus."
+msgstr ""
+
+#. (itstool) path: refsection/para
+#: pentobi-manpage.docbook.in:57
+msgid ""
+"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 Pentobi-SGF.md in the Pentobi source package."
+msgstr ""
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:65
+msgid "Options"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:71
+msgid "Display help and exit."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:77 pentobi-manpage.docbook.in:123
+#: pentobi-manpage.docbook.in:133
+msgid "<_:option-1/> <replaceable>n</replaceable>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:79
+msgid ""
+"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."
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:93
+msgid ""
+"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 <_:option-1/>."
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:106
+msgid "Do not use opening books."
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:114
+msgid ""
+"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."
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:125
+msgid ""
+"Set the seed for the random generator. Using a fixed seed makes the move "
+"generation deterministic if no multi-threading is used (see option <_:"
+"option-1/>)."
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:135
+msgid ""
+"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."
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:147
+msgid ""
+"Print internal information about the move generation and other debugging "
+"information to standard error."
+msgstr ""
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:157
+msgid "Standard Qt Options"
+msgstr ""
+
+#. (itstool) path: refsection/para
+#: pentobi-manpage.docbook.in:158
+msgid ""
+"Additionally, any options supported by Qt applications can be used, such as:"
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:164
+msgid "<_:option-1/> <replaceable>d</replaceable>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:166
+msgid "Switches displays on X11."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:172
+msgid "<_:option-1/> <replaceable>g</replaceable>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:174
+msgid "Window geometry using the X11 syntax."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:180
+msgid "<_:option-1/> <replaceable>s</replaceable>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:182
+msgid ""
+"Set the style for the GUI elements of QQuickControls."
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:193
+msgid "Display version and exit."
+msgstr ""
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:202
+msgid "See Also"
+msgstr ""
+
+#: io.sourceforge.pentobi.appdata.xml.in:7 io.sourceforge.pentobi.desktop.in:5
+msgid "Computer opponent for the board game Blokus"
+msgstr ""
+
+#: io.sourceforge.pentobi.appdata.xml.in:9
+msgid ""
+"Pentobi is a computer opponent for the board game Blokus. It has a strong "
+"Blokus engine with different playing levels. The supported game variants are "
+"Classic, Duo, Trigon, Junior, Nexos, Callisto and GembloQ."
+msgstr ""
+
+#: io.sourceforge.pentobi.appdata.xml.in:14
+msgid ""
+"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."
+msgstr ""
+
+#: io.sourceforge.pentobi.appdata.xml.in:19
+msgid ""
+"System requirements: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2.5 GHz dual-core or "
+"faster CPU recommended for playing level 9)."
+msgstr ""
+
+#: io.sourceforge.pentobi.appdata.xml.in:23
+msgid ""
+"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."
+msgstr ""
+
+#: io.sourceforge.pentobi.appdata.xml.in:34
+msgid "Game variant Classic"
+msgstr ""
+
+#: io.sourceforge.pentobi.appdata.xml.in:39
+msgid "Game variant Duo"
+msgstr ""
+
+#: io.sourceforge.pentobi.appdata.xml.in:44
+msgid "Game variant Trigon"
+msgstr ""
+
+#: io.sourceforge.pentobi.appdata.xml.in:49
+msgid "Game variant Nexos"
+msgstr ""
+
+#: io.sourceforge.pentobi.appdata.xml.in:54
+msgid "Game variant GembloQ"
+msgstr ""
+
+#: io.sourceforge.pentobi.appdata.xml.in:61
+msgid "Markus Enzenberger"
+msgstr ""
+
+#: io.sourceforge.pentobi.desktop.in:4
+msgid "Computer opponent for Blokus"
+msgstr ""
+
+#. TRANSLATORS: keywords in desktop entry, separate with semicolons
+#: io.sourceforge.pentobi.desktop.in:7
+msgid ""
+"Blokus;Blokus Duo;Blokus Trigon;Blokus Junior;Nexos;Callisto;Gemblo Q;GembloQ"
+msgstr ""
+
+#. TRANSLATORS: icon name in desktop entry, probably no need to change this
+#: io.sourceforge.pentobi.desktop.in:10
+msgid "pentobi"
+msgstr ""
+
+#: pentobi-mime.xml.in:4
+msgid "Blokus game"
+msgstr ""
diff --git a/pentobi/unix/pentobi.6.in b/pentobi/unix/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/pentobi/unix/po/LINGUAS b/pentobi/unix/po/LINGUAS
new file mode 100644 (file)
index 0000000..d0ea0cd
--- /dev/null
@@ -0,0 +1,3 @@
+de
+es
+ru
diff --git a/pentobi/unix/po/de.po b/pentobi/unix/po/de.po
new file mode 100644 (file)
index 0000000..3500c2e
--- /dev/null
@@ -0,0 +1,331 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+# 
+# Translators:
+# Markus Enzenberger <markus.enzenberger@gmail.com>, 2019
+# 
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-03-09 10:33+0100\n"
+"PO-Revision-Date: 2019-02-25 15:36+0000\n"
+"Last-Translator: Markus Enzenberger <markus.enzenberger@gmail.com>, 2019\n"
+"Language-Team: German (https://www.transifex.com/markus-enzenberger/teams/89074/de/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. Put one translator per line, in the form NAME <EMAIL>, YEAR1, YEAR2
+msgctxt "_"
+msgid "translator-credits"
+msgstr "Markus Enzenberger <enz@users.sourceforge.net>, 2019"
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-manpage.docbook.in:11 io.sourceforge.pentobi.appdata.xml.in:6
+#: io.sourceforge.pentobi.desktop.in:3
+msgid "Pentobi"
+msgstr "Pentobi"
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-manpage.docbook.in:13
+msgid "Pentobi Command Reference"
+msgstr "Pentobi Befehlsreferenz"
+
+#. (itstool) path: refnamediv/refpurpose
+#: pentobi-manpage.docbook.in:17
+msgid "computer opponent for the board game Blokus"
+msgstr "Computer-Gegner für das Brettspiel Blokus"
+
+#. (itstool) path: refsynopsisdiv/cmdsynopsis
+#: pentobi-manpage.docbook.in:23
+msgid ""
+"<_:command-1/> <arg><_:option-2/> <replaceable>n</replaceable></arg> "
+"<arg><_:option-3/></arg> <arg><_:option-4/></arg> <arg><_:option-5/></arg> "
+"<arg><_:option-6/> <replaceable>n</replaceable></arg> <arg><_:option-7/> "
+"<replaceable>n</replaceable></arg> <arg><_:option-8/></arg> "
+"<arg><replaceable>file.blksgf</replaceable></arg>"
+msgstr ""
+"<_:command-1/> <arg><_:option-2/> <replaceable>n</replaceable></arg> "
+"<arg><_:option-3/></arg> <arg><_:option-4/></arg> <arg><_:option-5/></arg> "
+"<arg><_:option-6/> <replaceable>n</replaceable></arg> <arg><_:option-7/> "
+"<replaceable>n</replaceable></arg> <arg><_:option-8/></arg> "
+"<arg><replaceable>Datei.blksgf</replaceable></arg>"
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:51
+msgid "Description"
+msgstr "Beschreibung"
+
+#. (itstool) path: refsection/para
+#: pentobi-manpage.docbook.in:52
+msgid ""
+"<_:command-1/> is the command to invoke the program Pentobi, which is a "
+"graphical user interface and computer opponent for the board game Blokus."
+msgstr ""
+"<_:command-1/> ist der Befehl zu Starten des Programms Pentobi, das eine "
+"grafische Oberfläche und ein Computer-Gegner für das Brettspiel Blokus ist."
+
+#. (itstool) path: refsection/para
+#: pentobi-manpage.docbook.in:57
+msgid ""
+"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 Pentobi-SGF.md in the Pentobi source package."
+msgstr ""
+"Der Befehl kann den Namen einer Spieldatei als optionales Argument haben, "
+"die beim Start geöffnet werden soll. Die Spieldatei muss im Pentobi-SGF-"
+"Format sein, wie beschrieben in Pentobi-SGF.md im Quelltextpaket von "
+"Pentobi."
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:65
+msgid "Options"
+msgstr "Optionen"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:71
+msgid "Display help and exit."
+msgstr "Hilfe anzeigen und Programm beenden."
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:77 pentobi-manpage.docbook.in:123
+#: pentobi-manpage.docbook.in:133
+msgid "<_:option-1/> <replaceable>n</replaceable>"
+msgstr "<_:option-1/> <replaceable>n</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:79
+msgid ""
+"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."
+msgstr ""
+"Setzen der maximalen Spielstufe. Wird dieser Wert verkleinert, verringert "
+"sich der Speicherbedarf der Suche, was nützlich sein kann, um Pentobi auf "
+"Systemen laufen zu lassen, die wenig Speicher besitzen oder zu langsam für "
+"die höchsten Spielstufen sind. Standardmäßig belegt Pentobi bis zu 2 GB "
+"(aber nicht mehr als ein Viertel des auf dem System verfügbaren physischen "
+"Speichers). Ein Verringern der maximalen Spielstufe auf 8 vermindert "
+"gegenwärtig den Speicherbedarf um den Faktor 3 bis 4 und geringere maximale "
+"Spielstufen sogar noch mehr."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:93
+msgid ""
+"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 <_:option-1/>."
+msgstr ""
+"Ein Fensterlayout benutzen, das für Smartphones optimiert ist und einige "
+"Änderungen der Benutzerpberfläche vornehmen, die annehmen, dass ein "
+"Touchscreen als primäres Eingabegerät verwendet wird. Das Benutzen dieser "
+"Option ändert auch den voreingestellen Style für Benutzeroberflächenelemente"
+" von QQuickControls 2 nach Default, wenn der Style nicht explizit mit der "
+"Option <_:option-1/> gesetzt wird."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:106
+msgid "Do not use opening books."
+msgstr "Keine Eröffnungsbibliotheken benutzen."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:114
+msgid ""
+"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."
+msgstr ""
+"Computer-Züge nicht verzögern. Voreingestellt ist, dass der Computer-Spieler"
+" Züge ein wenig verzögert, die schneller als innerhalb einer Sekunde "
+"generiert werden, um es dem Benutzer leichter zu machen, dem Spiel zu "
+"folgen, wenn der Computer mehrere Züge nacheinander generiert."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:125
+msgid ""
+"Set the seed for the random generator. Using a fixed seed makes the move "
+"generation deterministic if no multi-threading is used (see option "
+"<_:option-1/>)."
+msgstr ""
+"Setzt die Seed für den Zufallszahlengenerator. Eine feste Seed macht die "
+"Zuggenerierung deterministisch, falls kein Multi-Threading verwendet wird  "
+"(sihen Option <_:option-1/>)."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:135
+msgid ""
+"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."
+msgstr ""
+"Die Anzahl der Threads, die bei der Suche verwendet werden. Voreingestellt "
+"ist, dass die Suche bis zu 8 Threads verwendet, abhängig von Anzahl der auf "
+"dem System verfügbaren Hardware-Threads. Mehr Threads machen die Suche "
+"schneller, aber eine sehr hohen Anzahl (z. B. mehr als 8) kann die "
+"Spielstärke in höheren Spielstufen beeinträchtigen."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:147
+msgid ""
+"Print internal information about the move generation and other debugging "
+"information to standard error."
+msgstr ""
+"Interne Informationen zur Zuggenerierung und andere Debugging-Informationen "
+"auf dem Standard-Error-Stream ausgeben."
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:157
+msgid "Standard Qt Options"
+msgstr "Standardoptionen von Qt"
+
+#. (itstool) path: refsection/para
+#: pentobi-manpage.docbook.in:158
+msgid ""
+"Additionally, any options supported by Qt applications can be used, such as:"
+msgstr ""
+"Zusätzlich können Optionen verwendet werden, die von Qt-Anwendungen "
+"unterstützt werden, wie zum Beispiel:"
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:164
+msgid "<_:option-1/> <replaceable>d</replaceable>"
+msgstr "<_:option-1/> <replaceable>d</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:166
+msgid "Switches displays on X11."
+msgstr "Schaltet unter X11 das Anzeigegerät um."
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:172
+msgid "<_:option-1/> <replaceable>g</replaceable>"
+msgstr "<_:option-1/> <replaceable>g</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:174
+msgid "Window geometry using the X11 syntax."
+msgstr "Fenstergeometrie in X11-Syntax."
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:180
+msgid "<_:option-1/> <replaceable>s</replaceable>"
+msgstr "<_:option-1/> <replaceable>s</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:182
+msgid "Set the style for the GUI elements of QQuickControls."
+msgstr "Setzen des Style für Benutzeroberflächenelemente von QtQuickControls."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:193
+msgid "Display version and exit."
+msgstr "Hilfe anzeigen und Programm beenden."
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:202
+msgid "See Also"
+msgstr "Siehe auch"
+
+#: io.sourceforge.pentobi.appdata.xml.in:7 io.sourceforge.pentobi.desktop.in:5
+msgid "Computer opponent for the board game Blokus"
+msgstr "Computer-Gegner für das Brettspiel Blokus"
+
+#: io.sourceforge.pentobi.appdata.xml.in:9
+msgid ""
+"Pentobi is a computer opponent for the board game Blokus. It has a strong "
+"Blokus engine with different playing levels. The supported game variants are"
+" Classic, Duo, Trigon, Junior, Nexos, Callisto and GembloQ."
+msgstr ""
+"Pentobi ist ein Computer-Gegner für das Brettspiel Blokus. Es hat eine "
+"spielstarke Blokus-Engine mit verschiedenen Spielstufen. Die unterstützten "
+"Spielvarianten sind : Klassisch, Duo, Trigon, Junior, Nexos, Callisto und "
+"GembloQ."
+
+#: io.sourceforge.pentobi.appdata.xml.in:14
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#: io.sourceforge.pentobi.appdata.xml.in:19
+msgid ""
+"System requirements: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2.5 GHz dual-core or "
+"faster CPU recommended for playing level 9)."
+msgstr ""
+"Systemminima: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2,5 GHz Dual-Core- oder "
+"schnellere CPU empfohlen für Spielstufe 9)."
+
+#: io.sourceforge.pentobi.appdata.xml.in:23
+msgid ""
+"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."
+msgstr ""
+"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."
+
+#: io.sourceforge.pentobi.appdata.xml.in:34
+msgid "Game variant Classic"
+msgstr "Spielvariante Klassisch"
+
+#: io.sourceforge.pentobi.appdata.xml.in:39
+msgid "Game variant Duo"
+msgstr "Spielvariante Duo"
+
+#: io.sourceforge.pentobi.appdata.xml.in:44
+msgid "Game variant Trigon"
+msgstr "Spielvariante Trigon"
+
+#: io.sourceforge.pentobi.appdata.xml.in:49
+msgid "Game variant Nexos"
+msgstr "Spielvariante Nexos"
+
+#: io.sourceforge.pentobi.appdata.xml.in:54
+msgid "Game variant GembloQ"
+msgstr "Spielvariante GembloQ"
+
+#: io.sourceforge.pentobi.appdata.xml.in:61
+msgid "Markus Enzenberger"
+msgstr "Markus Enzenberger"
+
+#: io.sourceforge.pentobi.desktop.in:4
+msgid "Computer opponent for Blokus"
+msgstr "Computer-Gegner für Blokus"
+
+#. TRANSLATORS: keywords in desktop entry, separate with semicolons
+#: io.sourceforge.pentobi.desktop.in:7
+msgid ""
+"Blokus;Blokus Duo;Blokus Trigon;Blokus Junior;Nexos;Callisto;Gemblo "
+"Q;GembloQ"
+msgstr ""
+"Blokus;Blokus Duo;Blokus Trigon;Blokus Junior;Nexos;Callisto;Gemblo "
+"Q;GembloQ"
+
+#. TRANSLATORS: icon name in desktop entry, probably no need to change this
+#: io.sourceforge.pentobi.desktop.in:10
+msgid "pentobi"
+msgstr "pentobi"
+
+#: pentobi-mime.xml.in:4
+msgid "Blokus game"
+msgstr "Blokus-Partie"
diff --git a/pentobi/unix/po/es.po b/pentobi/unix/po/es.po
new file mode 100644 (file)
index 0000000..02e9d11
--- /dev/null
@@ -0,0 +1,336 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+# 
+# Translators:
+# Francisco Zamorano <pacozamo@gmail.com>, 2019
+# Markus Enzenberger <markus.enzenberger@gmail.com>, 2019
+# 
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-03-09 10:33+0100\n"
+"PO-Revision-Date: 2019-02-25 15:36+0000\n"
+"Last-Translator: Markus Enzenberger <markus.enzenberger@gmail.com>, 2019\n"
+"Language-Team: Spanish (https://www.transifex.com/markus-enzenberger/teams/89074/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. Put one translator per line, in the form NAME <EMAIL>, YEAR1, YEAR2
+msgctxt "_"
+msgid "translator-credits"
+msgstr "traductor-créditos"
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-manpage.docbook.in:11 io.sourceforge.pentobi.appdata.xml.in:6
+#: io.sourceforge.pentobi.desktop.in:3
+msgid "Pentobi"
+msgstr "Pentobi"
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-manpage.docbook.in:13
+msgid "Pentobi Command Reference"
+msgstr "Referencia de comandos de Pentobi"
+
+#. (itstool) path: refnamediv/refpurpose
+#: pentobi-manpage.docbook.in:17
+msgid "computer opponent for the board game Blokus"
+msgstr "rival virtual para el juego de mesa Blokus"
+
+#. (itstool) path: refsynopsisdiv/cmdsynopsis
+#: pentobi-manpage.docbook.in:23
+msgid ""
+"<_:command-1/> <arg><_:option-2/> <replaceable>n</replaceable></arg> "
+"<arg><_:option-3/></arg> <arg><_:option-4/></arg> <arg><_:option-5/></arg> "
+"<arg><_:option-6/> <replaceable>n</replaceable></arg> <arg><_:option-7/> "
+"<replaceable>n</replaceable></arg> <arg><_:option-8/></arg> "
+"<arg><replaceable>file.blksgf</replaceable></arg>"
+msgstr ""
+"<_:command-1/> <arg><_:option-2/> <replaceable>n</replaceable></arg> "
+"<arg><_:option-3/></arg> <arg><_:option-4/></arg> <arg><_:option-5/></arg> "
+"<arg><_:option-6/> <replaceable>n</replaceable></arg> <arg><_:option-7/> "
+"<replaceable>n</replaceable></arg> <arg><_:option-8/></arg> "
+"<arg><replaceable>file.blksgf</replaceable></arg>"
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:51
+msgid "Description"
+msgstr "Descripción"
+
+#. (itstool) path: refsection/para
+#: pentobi-manpage.docbook.in:52
+msgid ""
+"<_:command-1/> is the command to invoke the program Pentobi, which is a "
+"graphical user interface and computer opponent for the board game Blokus."
+msgstr ""
+"<_:command-1/> es el comando para invocar el programa Pentobi, que es una "
+"interfaz gráfica de usuario y un rival virtual para el juego de mesa Blokus."
+
+#. (itstool) path: refsection/para
+#: pentobi-manpage.docbook.in:57
+msgid ""
+"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 Pentobi-SGF.md in the Pentobi source package."
+msgstr ""
+"El comando puede seleccionar el nombre de un archivo de partida para abrir "
+"al inicio como un argumento opcional. Es necesario que el archivo de partida"
+" esté en formato SGF de Pentobi tal y como se explica en Pentobi-SGF.md "
+"dentro del paquete de origen de Pentobi."
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:65
+msgid "Options"
+msgstr "Opciones"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:71
+msgid "Display help and exit."
+msgstr "Mostrar ayuda y salir."
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:77 pentobi-manpage.docbook.in:123
+#: pentobi-manpage.docbook.in:133
+msgid "<_:option-1/> <replaceable>n</replaceable>"
+msgstr "<_:option-1/> <replaceable>n</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:79
+msgid ""
+"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."
+msgstr ""
+"Seleccione el nivel máximo de juego. Si reduce este valor se reduce también "
+"la cantidad de memoria utilizada para realizar la búsqueda, lo que puede "
+"resultar útil para jugar a Pentobi en sistemas que tengan poca memoria o que"
+" sean demasiado lentos cuando se seleccionan los niveles más altos. Por "
+"defecto, Pentobi actualmente asigna hasta un máximo de 2 GB (pero nunca más "
+"de un cuarto de la memoria física disponible en el sistema). Actualmente, al"
+" reducir el nivel máximo a 8 contribuye a reducir esta cantidad por un "
+"factor de 3 a 4 y a reducir los niveles máximos aún más."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:93
+msgid ""
+"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 <_:option-1/>."
+msgstr ""
+"Utiliza una configuración de ventana optimizada para móviles y aplica "
+"algunos cambios a la interfaz de usuario que implican que la pantalla táctil"
+" es el dispositivo principal de entrada. Si no se utiliza esta opción, la "
+"configuración por defecto depende de la plataforma. Utilizar esta opción "
+"también supone cambiar el estilo por defecto de los elementos GUI de "
+"QQuickControls 2 a los que vienen por defecto si el estilo no está "
+"explícitamente determinado con la opción <_:option-1/>."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:106
+msgid "Do not use opening books."
+msgstr "No utilizar abrir libros."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:114
+msgid ""
+"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."
+msgstr ""
+"No ralentice los movimientos rápidos de la máquina. Por defecto, el jugador "
+"de la máquina añade un pequeño retraso si la generación del movimiento tarda"
+" menos de un segundo para así hacer más fácil al contrincante humano poder "
+"seguir la partida si la máquina realiza varios movimientos seguidos."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:125
+msgid ""
+"Set the seed for the random generator. Using a fixed seed makes the move "
+"generation deterministic if no multi-threading is used (see option "
+"<_:option-1/>)."
+msgstr ""
+"Seleccione el valor de inicialización para el generador aleatorio. Si usa un"
+" valor de inicialización fijo la generación de movimientos será determinista"
+" si no se utilizan subprocesos múltiples (consulte la opción <_:option-1/>)."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:135
+msgid ""
+"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."
+msgstr ""
+"El número de subprocesos que se pueden utilizar en la búsqueda. Por defecto,"
+" se utilizan hasta un máximo de 8 subprocesos en la búsqueda dependiendo del"
+" número de subprocesos de hardware admitidos por el sistema actual. Usar más"
+" subprocesos supone acelerar la generación de movimientos aunque si se usan "
+"demasiados subprocesos (p. ej., más de 8) se puede reducir la habilidad del "
+"juego en los niveles de juego más altos."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:147
+msgid ""
+"Print internal information about the move generation and other debugging "
+"information to standard error."
+msgstr ""
+"Imprimir documentación interna relacionada con la generación de movimientos "
+"y otra información de depuración de errores típicos."
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:157
+msgid "Standard Qt Options"
+msgstr "Opciones habituales de Qt"
+
+#. (itstool) path: refsection/para
+#: pentobi-manpage.docbook.in:158
+msgid ""
+"Additionally, any options supported by Qt applications can be used, such as:"
+msgstr ""
+"Además, se puede usar cualquier opción compatible con las aplicaciones Qt, "
+"como:"
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:164
+msgid "<_:option-1/> <replaceable>d</replaceable>"
+msgstr "<_:option-1/> <replaceable>d</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:166
+msgid "Switches displays on X11."
+msgstr "Los cambios se muestran en X11."
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:172
+msgid "<_:option-1/> <replaceable>g</replaceable>"
+msgstr "<_:option-1/> <replaceable>g</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:174
+msgid "Window geometry using the X11 syntax."
+msgstr "Geometría de ventana utilizando la sintaxis de X11."
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:180
+msgid "<_:option-1/> <replaceable>s</replaceable>"
+msgstr "<_:option-1/> <replaceable>s</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:182
+msgid "Set the style for the GUI elements of QQuickControls."
+msgstr "Seleccione el estilo de los elementos GUI de QQuickControls."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:193
+msgid "Display version and exit."
+msgstr "Mostrar versión y salir."
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:202
+msgid "See Also"
+msgstr "Consulte también"
+
+#: io.sourceforge.pentobi.appdata.xml.in:7 io.sourceforge.pentobi.desktop.in:5
+msgid "Computer opponent for the board game Blokus"
+msgstr "Rival virtual para el juego de mesa Blokus"
+
+#: io.sourceforge.pentobi.appdata.xml.in:9
+msgid ""
+"Pentobi is a computer opponent for the board game Blokus. It has a strong "
+"Blokus engine with different playing levels. The supported game variants are"
+" Classic, Duo, Trigon, Junior, Nexos, Callisto and GembloQ."
+msgstr ""
+"Pentobi es un rival virtual para el juego de mesa Blokus. Cuenta con un "
+"potente motor para Blokus con diferentes niveles de juego. Las variantes de "
+"juego compatibles son Clásico, Dúo, Trigón, Sencillo, Nexos, Calisto y "
+"GembloQ."
+
+#: io.sourceforge.pentobi.appdata.xml.in:14
+msgid ""
+"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."
+msgstr ""
+"Los jugadores pueden conocer su habilidad participando en partidas con "
+"selección de nivel contra la máquina y usar una función de análisis de la "
+"partida. Las partidas se pueden guardar en formato Smart Game Format con "
+"comentarios y variaciones de movimientos."
+
+#: io.sourceforge.pentobi.appdata.xml.in:19
+msgid ""
+"System requirements: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2.5 GHz dual-core or "
+"faster CPU recommended for playing level 9)."
+msgstr ""
+"Requisitos de sistema: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2,5 GHz dual-core o  "
+"CPU más rápida recomendada para jugar en el nivel 9)."
+
+#: io.sourceforge.pentobi.appdata.xml.in:23
+msgid ""
+"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."
+msgstr ""
+"Exención de responsabilidad de la marca comercial: la marca comercial Blokus"
+" y otras marcas comerciales mencionadas son propiedad de sus respectivos "
+"propietarios. Los propietarios de las marcas comerciales no están afiliados "
+"con el autor del programa Pentobi."
+
+#: io.sourceforge.pentobi.appdata.xml.in:34
+msgid "Game variant Classic"
+msgstr "Variante de juego Clásico"
+
+#: io.sourceforge.pentobi.appdata.xml.in:39
+msgid "Game variant Duo"
+msgstr "Variante de juego Dúo"
+
+#: io.sourceforge.pentobi.appdata.xml.in:44
+msgid "Game variant Trigon"
+msgstr "Variante de juego Trigón"
+
+#: io.sourceforge.pentobi.appdata.xml.in:49
+msgid "Game variant Nexos"
+msgstr "Variante de juego Nexos"
+
+#: io.sourceforge.pentobi.appdata.xml.in:54
+msgid "Game variant GembloQ"
+msgstr "Variante de juego GembloQ"
+
+#: io.sourceforge.pentobi.appdata.xml.in:61
+msgid "Markus Enzenberger"
+msgstr "Markus Enzenberger"
+
+#: io.sourceforge.pentobi.desktop.in:4
+msgid "Computer opponent for Blokus"
+msgstr "Rival virtual para Blokus"
+
+#. TRANSLATORS: keywords in desktop entry, separate with semicolons
+#: io.sourceforge.pentobi.desktop.in:7
+msgid ""
+"Blokus;Blokus Duo;Blokus Trigon;Blokus Junior;Nexos;Callisto;Gemblo "
+"Q;GembloQ"
+msgstr ""
+"Blokus;Blokus Dúo;Blokus Trigón;Blokus Sencillo;Nexos;Calisto;Gemblo "
+"Q;GembloQ"
+
+#. TRANSLATORS: icon name in desktop entry, probably no need to change this
+#: io.sourceforge.pentobi.desktop.in:10
+msgid "pentobi"
+msgstr "pentobi"
+
+#: pentobi-mime.xml.in:4
+msgid "Blokus game"
+msgstr "Partida de Blokus"
diff --git a/pentobi/unix/po/ru.po b/pentobi/unix/po/ru.po
new file mode 100644 (file)
index 0000000..f50d835
--- /dev/null
@@ -0,0 +1,334 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+# 
+# Translators:
+# Виктор Ерухин <official159ru@mail.ru>, 2020
+# 
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-03-09 10:33+0100\n"
+"PO-Revision-Date: 2019-02-25 15:36+0000\n"
+"Last-Translator: Виктор Ерухин <official159ru@mail.ru>, 2020\n"
+"Language-Team: Russian (https://www.transifex.com/markus-enzenberger/teams/89074/ru/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
+
+#. Put one translator per line, in the form NAME <EMAIL>, YEAR1, YEAR2
+msgctxt "_"
+msgid "translator-credits"
+msgstr "переводчик-вклад"
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-manpage.docbook.in:11 io.sourceforge.pentobi.appdata.xml.in:6
+#: io.sourceforge.pentobi.desktop.in:3
+msgid "Pentobi"
+msgstr "Pentobi"
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-manpage.docbook.in:13
+msgid "Pentobi Command Reference"
+msgstr "Справочник команд Pentobi"
+
+#. (itstool) path: refnamediv/refpurpose
+#: pentobi-manpage.docbook.in:17
+msgid "computer opponent for the board game Blokus"
+msgstr "конкурент компьютерной настольной игры Блокус"
+
+#. (itstool) path: refsynopsisdiv/cmdsynopsis
+#: pentobi-manpage.docbook.in:23
+msgid ""
+"<_:command-1/> <arg><_:option-2/> <replaceable>n</replaceable></arg> "
+"<arg><_:option-3/></arg> <arg><_:option-4/></arg> <arg><_:option-5/></arg> "
+"<arg><_:option-6/> <replaceable>n</replaceable></arg> <arg><_:option-7/> "
+"<replaceable>n</replaceable></arg> <arg><_:option-8/></arg> "
+"<arg><replaceable>file.blksgf</replaceable></arg>"
+msgstr ""
+"<_:command-1/> <arg><_:option-2/> <replaceable>n</replaceable></arg> "
+"<arg><_:option-3/></arg> <arg><_:option-4/></arg> <arg><_:option-5/></arg> "
+"<arg><_:option-6/> <replaceable>n</replaceable></arg> <arg><_:option-7/> "
+"<replaceable>n</replaceable></arg> <arg><_:option-8/></arg> "
+"<arg><replaceable>файл.blksgf</replaceable></arg>"
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:51
+msgid "Description"
+msgstr "Описание"
+
+#. (itstool) path: refsection/para
+#: pentobi-manpage.docbook.in:52
+msgid ""
+"<_:command-1/> is the command to invoke the program Pentobi, which is a "
+"graphical user interface and computer opponent for the board game Blokus."
+msgstr ""
+"<_:command-1/> это команда для вызова программы Pentobi, которая "
+"представляет собой графический интерфейс пользователя и компьютерный "
+"оппонент для настольной игры Блокус."
+
+#. (itstool) path: refsection/para
+#: pentobi-manpage.docbook.in:57
+msgid ""
+"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 Pentobi-SGF.md in the Pentobi source package."
+msgstr ""
+"В качестве необязательного аргумента команда может принимать имя файла игры,"
+" открываемого при запуске. Ожидается, что файл игры будет в формате SGF "
+"Pentobi, как описано в Pentobi-SGF.md в исходном пакете Pentobi."
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:65
+msgid "Options"
+msgstr "Параметры"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:71
+msgid "Display help and exit."
+msgstr "Показать справку и выйти."
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:77 pentobi-manpage.docbook.in:123
+#: pentobi-manpage.docbook.in:133
+msgid "<_:option-1/> <replaceable>n</replaceable>"
+msgstr "<_:option-1/> <replaceable>n</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:79
+msgid ""
+"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."
+msgstr ""
+"Установите максимальный уровень игры. Уменьшение этого значения уменьшает "
+"объем памяти, используемый поиском, что может быть полезно для запуска "
+"Pentobi в системах с низким объемом памяти или слишком медленными для "
+"использования самых высоких уровней. По умолчанию Pentobi в настоящее время "
+"выделяет до 2 ГБ (но не более четверти физической памяти, доступной в "
+"системе). Снижение максимального уровня до 8 в настоящее время уменьшает это"
+" количество в 3–4 раза и еще больше снижает максимальные уровни."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:93
+msgid ""
+"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 <_:option-1/>."
+msgstr ""
+"Используйте макет окна, оптимизированный для смартфонов, и внесите некоторые"
+" изменения в пользовательский интерфейс, предполагающие, что сенсорный экран"
+" является основным устройством ввода. Если этот параметр не используется, "
+"макет по умолчанию зависит от платформы. Использование этой опции также "
+"изменяет стиль по умолчанию для элементов графического интерфейса "
+"QQuickControls 2 на Дефолтный, если стиль не задан явно с помощью опции "
+"<_:option-1/>."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:106
+msgid "Do not use opening books."
+msgstr "Не использовать открытие книг."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:114
+msgid ""
+"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."
+msgstr ""
+"Не снижайте быстрые ходы компьютера. По умолчанию компьютерный игрок "
+"добавляет небольшую задержку, если генерация хода заняла менее секунды, "
+"чтобы человеку было легче следить за игрой, если компьютер выполняет "
+"несколько ходов подряд."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:125
+msgid ""
+"Set the seed for the random generator. Using a fixed seed makes the move "
+"generation deterministic if no multi-threading is used (see option "
+"<_:option-1/>)."
+msgstr ""
+"Задайте начальное число для генератора случайных чисел. Использование "
+"фиксированного начального числа делает генерацию перемещения "
+"детерминированной, если не используется многопоточность (см. Параметр "
+"<_:option-1/>)."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:135
+msgid ""
+"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."
+msgstr ""
+"Количество потоков, используемых при поиске. По умолчанию при поиске "
+"используется до 8 потоков в зависимости от количества аппаратных потоков, "
+"поддерживаемых текущей системой. Использование большего количества потоков "
+"ускорит создание ходов, но использование очень большого количества потоков "
+"(например, более 8) может снизить эффективность игры на более высоких "
+"уровнях игры."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:147
+msgid ""
+"Print internal information about the move generation and other debugging "
+"information to standard error."
+msgstr ""
+"Вывести внутреннюю информацию о генерации хода и другую отладочную "
+"информацию в стандартную ошибку."
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:157
+msgid "Standard Qt Options"
+msgstr "Стандартные параметры Qt"
+
+#. (itstool) path: refsection/para
+#: pentobi-manpage.docbook.in:158
+msgid ""
+"Additionally, any options supported by Qt applications can be used, such as:"
+msgstr ""
+"Кроме того, можно использовать любые опции, поддерживаемые приложениями Qt, "
+"такие как:"
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:164
+msgid "<_:option-1/> <replaceable>d</replaceable>"
+msgstr "<_:option-1/> <replaceable>d</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:166
+msgid "Switches displays on X11."
+msgstr "Переключает дисплеи на X11."
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:172
+msgid "<_:option-1/> <replaceable>g</replaceable>"
+msgstr "<_:option-1/> <replaceable>g</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:174
+msgid "Window geometry using the X11 syntax."
+msgstr "Геометрия окна использует синтаксис X11."
+
+#. (itstool) path: varlistentry/term
+#: pentobi-manpage.docbook.in:180
+msgid "<_:option-1/> <replaceable>s</replaceable>"
+msgstr "<_:option-1/> <replaceable>s</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:182
+msgid "Set the style for the GUI elements of QQuickControls."
+msgstr ""
+"Установить стиль для элементов графического интерфейса QQuickControls."
+
+#. (itstool) path: listitem/para
+#: pentobi-manpage.docbook.in:193
+msgid "Display version and exit."
+msgstr "Показать версию и выйти."
+
+#. (itstool) path: refsection/title
+#: pentobi-manpage.docbook.in:202
+msgid "See Also"
+msgstr "Смотрите также"
+
+#: io.sourceforge.pentobi.appdata.xml.in:7 io.sourceforge.pentobi.desktop.in:5
+msgid "Computer opponent for the board game Blokus"
+msgstr "Конкурент компьютерной настольной игры Блокус"
+
+#: io.sourceforge.pentobi.appdata.xml.in:9
+msgid ""
+"Pentobi is a computer opponent for the board game Blokus. It has a strong "
+"Blokus engine with different playing levels. The supported game variants are"
+" Classic, Duo, Trigon, Junior, Nexos, Callisto and GembloQ."
+msgstr ""
+"Pentobi оппонент компьютерной настольной игры Блокус. Он имеет сильный "
+"движок Блокус с различными уровнями игры. Поддерживаемые варианты игры-"
+"Классический, Двое, Тригон, Юниор, Нексос, Каллисто и ГемблоQ."
+
+#: io.sourceforge.pentobi.appdata.xml.in:14
+msgid ""
+"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."
+msgstr ""
+"Игроки могут определить свою силу, играя в рейтинговые игры против "
+"компьютера и используя функцию анализа игры. Игры могут быть сохранены в "
+"формате Smart Game с комментариями и вариациями ходов."
+
+#: io.sourceforge.pentobi.appdata.xml.in:19
+msgid ""
+"System requirements: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2.5 GHz dual-core or "
+"faster CPU recommended for playing level 9)."
+msgstr ""
+"Системные требования: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2.5 GHz dual-core или "
+"больше CPU рекомендуется для игрового уровня 9)."
+
+#: io.sourceforge.pentobi.appdata.xml.in:23
+msgid ""
+"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."
+msgstr ""
+"Отказ от ответственности за товарный знак: товарный знак Blokus и другие "
+"упомянутые товарные знаки являются собственностью их соответствующих "
+"владельцев товарных знаков. Владельцы товарных знаков не связаны с автором "
+"программы Pentobi."
+
+#: io.sourceforge.pentobi.appdata.xml.in:34
+msgid "Game variant Classic"
+msgstr "Вариант игры Классический"
+
+#: io.sourceforge.pentobi.appdata.xml.in:39
+msgid "Game variant Duo"
+msgstr "Вариант игры Двое"
+
+#: io.sourceforge.pentobi.appdata.xml.in:44
+msgid "Game variant Trigon"
+msgstr "Вариант игры Тригон"
+
+#: io.sourceforge.pentobi.appdata.xml.in:49
+msgid "Game variant Nexos"
+msgstr "Вариант игры Нексос"
+
+#: io.sourceforge.pentobi.appdata.xml.in:54
+msgid "Game variant GembloQ"
+msgstr "Вариант игры ГемблоQ"
+
+#: io.sourceforge.pentobi.appdata.xml.in:61
+msgid "Markus Enzenberger"
+msgstr "Markus Enzenberger"
+
+#: io.sourceforge.pentobi.desktop.in:4
+msgid "Computer opponent for Blokus"
+msgstr "Оппонент компьютерной настольной игры Блокус"
+
+#. TRANSLATORS: keywords in desktop entry, separate with semicolons
+#: io.sourceforge.pentobi.desktop.in:7
+msgid ""
+"Blokus;Blokus Duo;Blokus Trigon;Blokus Junior;Nexos;Callisto;Gemblo "
+"Q;GembloQ"
+msgstr ""
+"Блокус;Блокус Двое;Блокус Тригон;Блокус Юниор;Нексос;Каллисто;Гембло "
+"Q;ГемблоQ"
+
+#. TRANSLATORS: icon name in desktop entry, probably no need to change this
+#: io.sourceforge.pentobi.desktop.in:10
+msgid "pentobi"
+msgstr "pentobi"
+
+#: pentobi-mime.xml.in:4
+msgid "Blokus game"
+msgstr "Игра Блокус"
diff --git a/pentobi_gtp/CMakeLists.txt b/pentobi_gtp/CMakeLists.txt
new file mode 100644 (file)
index 0000000..13dd261
--- /dev/null
@@ -0,0 +1,10 @@
+add_executable(pentobi-gtp
+    GtpEngine.h
+    GtpEngine.cpp
+    Main.cpp
+    )
+
+target_compile_definitions(pentobi-gtp PRIVATE VERSION="${PENTOBI_VERSION}")
+
+target_link_libraries(pentobi-gtp pentobi_gtp pentobi_mcts)
+
diff --git a/pentobi_gtp/GtpEngine.cpp b/pentobi_gtp/GtpEngine.cpp
new file mode 100644 (file)
index 0000000..9b42e6a
--- /dev/null
@@ -0,0 +1,205 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi_gtp/GtpEngine.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "GtpEngine.h"
+
+#include <fstream>
+#include "libboardgame_base/Writer.h"
+#include "libpentobi_mcts/Util.h"
+
+using libboardgame_base::Writer;
+using libboardgame_gtp::Failure;
+using libpentobi_base::Board;
+using libpentobi_base::get_color_id;
+using libpentobi_mcts::Float;
+
+//-----------------------------------------------------------------------------
+
+GtpEngine::GtpEngine(
+        Variant variant, unsigned level, bool use_book,
+        const string& books_dir, unsigned nu_threads)
+    : libpentobi_gtp::GtpEngine(variant)
+{
+    create_player(variant, level, books_dir, nu_threads);
+    get_mcts_player().set_use_book(use_book);
+    add("get_value", &GtpEngine::cmd_get_value);
+    add("name", &GtpEngine::cmd_name);
+    add("param", &GtpEngine::cmd_param);
+    add("move_values", &GtpEngine::cmd_move_values);
+    add("save_tree", &GtpEngine::cmd_save_tree);
+    add("selfplay", &GtpEngine::cmd_selfplay);
+    add("version", &GtpEngine::cmd_version);
+}
+
+GtpEngine::~GtpEngine() = default; // Non-inline to avoid GCC -Winline warning
+
+void GtpEngine::cmd_get_value(Response& response)
+{
+    response << get_search().get_tree().get_root().get_value();
+}
+
+void GtpEngine::cmd_move_values(Response& response)
+{
+    auto children = get_search().get_tree().get_root_children();
+    if (children.empty())
+        return;
+    auto& bd = get_board();
+    vector<const Search::Node*> sorted_children;
+    sorted_children.reserve(children.size());
+    for (auto& i : children)
+        sorted_children.push_back(&i);
+    sort(sorted_children.begin(), sorted_children.end(), libpentobi_mcts::compare_node);
+    response << fixed;
+    for (auto node : sorted_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 GtpEngine::cmd_name(Response& response)
+{
+    response.set("Pentobi");
+}
+
+void GtpEngine::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<string>());
+    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 GtpEngine::cmd_selfplay(Arguments args)
+{
+    args.check_size(2);
+    auto nu_games = args.get<int>(0);
+    ofstream out(args.get<string>(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 GtpEngine::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);
+        auto name = args.get(0);
+        if (name == "avoid_symmetric_draw")
+            s.set_avoid_symmetric_draw(args.get<bool>(1));
+        else if (name == "exploration_constant")
+            s.set_exploration_constant(args.get<Float>(1));
+        else if (name == "fixed_simulations")
+            p.set_fixed_simulations(args.get<Float>(1));
+        else if (name == "rave_child_max")
+            s.set_rave_child_max(args.get<Float>(1));
+        else if (name == "rave_parent_max")
+            s.set_rave_parent_max(args.get<Float>(1));
+        else if (name == "rave_weight")
+            s.set_rave_weight(args.get<Float>(1));
+        else if (name == "reuse_subtree")
+            s.set_reuse_subtree(args.get<bool>(1));
+        else if (name == "use_book")
+            p.set_use_book(args.get<bool>(1));
+        else
+        {
+            ostringstream msg;
+            msg << "unknown parameter '" << name << "'";
+            throw Failure(msg.str());
+        }
+    }
+}
+
+void GtpEngine::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 GtpEngine::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& GtpEngine::get_mcts_player()
+{
+    try
+    {
+        return dynamic_cast<Player&>(*m_player);
+    }
+    catch (const bad_cast&)
+    {
+        throw Failure("current player is not mcts player");
+    }
+}
+
+Search& GtpEngine::get_search()
+{
+    return get_mcts_player().get_search();
+}
+
+void GtpEngine::use_cpu_time(bool enable)
+{
+    get_mcts_player().use_cpu_time(enable);
+}
+
+//-----------------------------------------------------------------------------
diff --git a/pentobi_gtp/GtpEngine.h b/pentobi_gtp/GtpEngine.h
new file mode 100644 (file)
index 0000000..f5a74a5
--- /dev/null
@@ -0,0 +1,57 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi_gtp/GtpEngine.h
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_GTP_GTP_ENGINE_H
+#define PENTOBI_GTP_GTP_ENGINE_H
+
+#include "libpentobi_gtp/GtpEngine.h"
+#include "libpentobi_mcts/Player.h"
+
+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 GtpEngine
+    : public libpentobi_gtp::GtpEngine
+{
+public:
+    explicit GtpEngine(
+            Variant variant, unsigned level = 5, bool use_book = true,
+            const string& books_dir = "", unsigned nu_threads = 0);
+
+    ~GtpEngine() 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();
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_GTP_GTP_ENGINE_H
diff --git a/pentobi_gtp/Main.cpp b/pentobi_gtp/Main.cpp
new file mode 100644 (file)
index 0000000..5b8803d
--- /dev/null
@@ -0,0 +1,166 @@
+//-----------------------------------------------------------------------------
+/** @file pentobi_gtp/Main.cpp
+    @author Markus Enzenberger
+    @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include <fstream>
+#include "GtpEngine.h"
+#include "libboardgame_base/Log.h"
+#include "libboardgame_base/Options.h"
+#include "libboardgame_base/RandomGenerator.h"
+
+using namespace std;
+using libboardgame_base::Options;
+using libboardgame_base::RandomGenerator;
+using libboardgame_gtp::Failure;
+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_base::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_base::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;
+        GtpEngine 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_base::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/pentobi_gtp/Pentobi-GTP.md b/pentobi_gtp/Pentobi-GTP.md
new file mode 100644 (file)
index 0000000..1481332
--- /dev/null
@@ -0,0 +1,398 @@
+Pentobi GTP Interface
+=====================
+
+This document describes the text-based interface to the engine of the
+Blokus program [Pentobi](https://pentobi.sourceforge.io). The interface
+is an adaption of the
+[Go Text Protocol](https://www.lysator.liu.se/~gunnar/gtp/) (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.
+
+Go Text Protocol
+----------------
+
+The Go Text Protocol is a simple text-based protocol. The engine reads
+single-line commands from its standard input stream and writes
+multi-line responses to its standard output stream. The first character
+of a response is a status character: `=` for success, `?` for failure,
+followed by the actual response. The response ends with two consecutive
+newline characters. See the
+[GTP specification](https://www.lysator.liu.se/~gunnar/gtp/gtp2-spec-draft2/gtp2-spec.html)
+for details.
+
+Controllers
+-----------
+
+To use the engine from a controller program, the controller typically
+creates a child process by running `pentobi-gtp` and then sends commands
+and receives responses through the input/output streams of the child
+process. How this is done depends on the platform (programming language
+and/or operating system). In Java, for example, a child process can be
+created with `java.lang.Runtime.exec()`.
+
+Note that the input/output streams of child processes are often fully
+buffered. You should explicitly flush the output stream after sending a
+command. Another caveat is that `pentobi-gtp` writes debugging
+information to its standard error stream. On some platforms the standard
+error stream of the child process is automatically connected to the
+standard error stream of its parent process. If not (this happens for
+example in Java), the controller needs to read everything from the
+standard error stream of the child process. This can be done for example
+by running a separate thread in the parent process that has a simple
+read loop, which writes everything that it reads to its own standard
+error stream or discards it. Otherwise the child process will block as
+soon as the buffer for its standard error stream is full. Alternatively,
+you can disable debugging output of `pentobi-gtp` with the command line
+option `--quiet`, but it is generally better to assume that a GTP engine
+writes text to standard error.
+
+An example for a controller written in C++ for Linux is included in
+Pentobi since version 9.0 in `twogtp`. The controller starts two GTP
+engines and plays a number of Blokus games between them. Older versions
+of Pentobi included a Python script with a similar functionality in
+`tools/twogtp/twogtp.py`.
+
+Building
+--------
+
+Since the GTP engine is a developer tool, building it is not enabled by
+default. To enable it, run `cmake` with the option
+`-DPENTOBI_BUILD_GTP=ON`. After building, there will be an executable in
+the build directory named `pentobi_gtp/pentobi-gtp`. The GTP engine
+requires only standard C++ and has no dependency on other libraries like
+Qt, which is needed for the GUI version of Pentobi. If you only want to
+build the GTP engine, you can disable building the GUI with
+`-DPENTOBI_BUILD_GUI=OFF`.
+
+Options
+-------
+
+The following command-line options are supported by `pentobi-gtp`:
+
+`--book` _file_
+
+Specify a file name for the opening book. Opening books are blksgf files
+containing trees, in which moves that Pentobi should select are marked
+as good moves with the corresponding SGF property (see the files in
+`opening_books`). If no opening book is specified and opening books are
+not disabled, `pentobi-gtp` will automatically search for an opening
+book for the current game variant in the directory of the executable
+using the same file name conventions as in `opening_books`. If no such
+file is found it will print an error message to standard error and
+disable the use of opening books.
+
+`--config,-c` _file_
+
+Load a file with GTP commands and execute them before starting the main
+loop, which reads commands from standard input. This can be used for
+configuration files that contain GTP commands for setting parameters of
+the engine (see below).
+
+`--color`
+
+Use ANSI escape sequences to colorize the text output of boards (for
+example in the response to the `showboard` command or with the
+--showboard command line option).
+
+`--cputime`
+
+Use CPU time instead of wall time for time measurement. Currently, there
+is no way to make Pentobi play with time limits, the levels are defined
+by the number of simulations in the MCTS search, so this affects only
+the debugging output, which prints the time used after each search.
+
+`--game,-g` _variant_
+
+Specify the game variant used at start-up. Valid arguments are classic,
+classic_2, duo, trigon, trigon_2, trigon_3, junior, 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.
+
+`--help,-h`
+
+Print a list of the command-line options and exit.
+
+`--level,-l` _n_
+
+Set the level of playing strength to n. Valid values are 1 to 9.
+
+`--seed,-r` _n_
+
+Use _n_ as the seed for the random generator. Specifying a random seed
+will make the move generation deterministic as long as the search is
+single-threaded.
+
+`--showboard`
+
+Automatically write a text representation of the current position to
+standard error after each command that alters the position.
+
+`--nobook`
+
+Disable the use of opening books.
+
+`--noresign`
+
+Disable resignation. If resignation is disabled, the `genmove` command
+will never respond with `resign`. Resignation can speed up the playing
+of test games if only the win/loss information is wanted.
+
+`--quiet,-q`
+
+Do not print any debugging messages, errors or warnings to standard
+error.
+
+`--threads` _n_
+
+Use _n_ threads during the search. Note that the default is 1, unlike
+in the GUI version of Pentobi, which sets the default according to the number
+of hardware threads (CPUs, cores or virtual cores) available on the current
+system. The reason is that, for example, using 2 threads makes the search twice
+as fast but may lose a bit of playing strength compared to the single-threaded
+search. Therefore, if the GTP engine is used to play many test games with
+twogtp (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.
+
+`--version,-v`
+
+Print the version of Pentobi and exit.
+
+Standard Commands
+-----------------
+
+The following GTP commands have the same or an equivalent meaning as
+specified by the GTP standard. Colors or players in arguments or
+responses are represented as in the property IDs of blksgf files (`B`,
+`W` if two colors; `1`, `2`, `3`, `4` if more than two). Moves in
+arguments or responses are represented as in the move property values
+of blksgf files. See the specification for
+[Pentobi SGF files](../libpentobi_base/Pentobi-SGF.md) for details.
+
+`all_legal` _color_
+
+List all legal moves for a color.
+
+`clear_board`
+
+Clear the board and start a new game in the current game variant.
+
+`final_score`
+
+Get the score of a final board position. In two-player game variants,
+the format of the response is as in the result property in the SGF
+standard for the game of Go (e.g. `B+2` if the first player wins with
+two points, or `0` for a draw). In game variants with more than two
+players, the response is a list of the points for each player (e.g.
+`64 69 70 40`). If the current position is not a final position, the
+response is undefined.
+
+`genmove` _color_
+
+Generate and play a move for a given color in the current position. If
+the color has no more moves, the response is `pass`. If resignation is
+not disabled, the response is `resign` if the players is very likely to
+lose. Otherwise the response is the move.
+
+`known_command` _command_
+
+The response is `true` if _command_ is a GTP command supported
+by the engine, `false` otherwise.
+
+`list_commands`
+
+List all supported GTP commands, one command per line.
+
+`loadsgf` _file_ [_move_number_]
+
+Load a board position from a blksgf file with name _file_. If
+_move_number_ is specified, the board position will be set to the
+position in the main variation of the file before the move with
+the given number was played, otherwise to the last position in the main
+variation.
+
+`name`
+
+Return the name of the GTP engine (`Pentobi`).
+
+`play` _color_ _move_
+
+Play a move for a given color in the current board position.
+
+`quit`
+
+Exit the command loop and quit the engine.
+
+`reg_genmove` _color_
+
+Like the `genmove` command, but only generates a move and does not
+play it on the board.
+
+`showboard`
+
+Return a text representation of the current board position.
+
+`undo`
+
+Undo the last move played.
+
+`version`
+
+Return the version of Pentobi.
+
+Generally Useful Extension Commands
+-----------------------------------
+
+`cputime`
+
+Return the CPU time used by the engine since the start of the program.
+
+`g`
+
+Shortcut for the `genmove` command with the color argument set to
+the current color to play.
+
+`get_place` _color_
+
+Get the place of a given color in the list of scores in a final position
+(e.g. in game variant Classic, 1 is the place with the highest score,
+4 the one with the lowest, if all players have a different score). If
+some colors have the same score, they share the same place and the
+string `shared` is appended to the place number.
+
+`get_value`
+
+Get an estimated value of the board position from the view point of the
+color of the last generated move. The return value is a win/loss
+estimation between 0 (loss) and 1 (win) as produced by the last search
+performed by the engine. This command should only be used immediately
+after a `reg_genmove` or `genmove` command, otherwise the result is
+undefined. The value is not very meaningful at the lowest playing
+levels. Note that no searches are performed if the opening book is used
+for a move generation and there is currently no way to check if this was
+so. Therefore, the opening book should be disabled if the `get_value`
+command is used.
+
+`p` _move_
+
+Shortcut for the `play` command with the color argument set to the
+current color to play.
+
+`param` [_key_ _value_]
+
+Set or query parameters specific to the Pentobi engine that can be
+changed at run-time. If no arguments are given, the response is a list
+of the current value with one key/value pair per line, otherwise the
+parameter with the given key will be set to the given value. Generally
+useful parameters are:
+
+`param avoid_symmetric_draw 0|1`
+In some game variants (Duo, Trigon_2), the second player can enforce a
+tie by answering each move by its symmetric counterpart if the first
+players misses the opportunity to break the symmetry in the center.
+Technically, exploiting this mistake by the first player is a good
+strategy for the second player because a draw is a good result
+considering the first-play advantage. However, playing symmetrically
+could be considered bad style, so this behavior is avoided (value `1`)
+by default.
+
+`param fixed_simulations` _n_
+Use exactly _n_ MCTS simulations during a search. By default, the
+search engine uses levels, which determine how many MCTS simulations are
+run during a search, but as a function that increases with the move
+number (because the simulations become much faster at the end of the
+game). For some experiments, it can be desirable to use a fixed number
+of simulations for each move. If this number is specified, the playing
+level is ignored.
+
+`param use_book 0|1`
+Enable or disable the opening book.
+
+The other parameters are only interesting for developers.
+
+`param_base` [_key_ _value_]
+
+Set or query basic parameters that are not specific to the Pentobi
+engine. If no arguments are given, the response is a list of the current
+value with one key/value pair per line, otherwise the parameter with the
+given key will be set to the given value.
+
+`param_base accept_illegal 0|1`
+Accept move arguments to the `play` command that violate the rules
+of the game. If disabled, the `play` command will respond with an error,
+otherwise it will perform the moves.
+
+`param_base resign 0|1`
+Allow the engine to respond with `resign` to the `genmove` command.
+
+`set_game` _variant_
+
+Set the current game variant and clear the board. The argument is the
+name of the game variant as in the game property value of blksgf files
+(e.g. `Blokus Duo`, see the specification for
+[Pentobi SGF files](../libpentobi_base/Pentobi-SGF.md) for details).
+
+`set_random_seed` _n_
+
+Set the seed of the random generator to _n_. See the documentation for
+the command-line option --seed.
+
+Extension Commands for Developers
+---------------------------------
+
+The remaining commands are only interesting for developers. See
+Pentobi's source code for details.
+
+Example
+-------
+
+The following GTP session queries the engine name and version, plays and
+generates a move in game variant Duo and shows the resulting board
+position.
+```
+$ ./pentobi-gtp --quiet
+name
+= Pentobi
+
+version
+= 7.1
+
+set_game Blokus Duo
+=
+
+play b e8,d9,e9,f9,e10
+=
+
+genmove w
+= i4,h5,i5,j5,i6
+
+showboard
+=
+   A B C D E F G H I J K L M N
+14 . . . . . . . . . . . . . . 14  *Blue(X): 5
+13 . . . . . . . . . . . . . . 13  1 F L5 N P T5 U V5 W Y
+12 . . . . . . . . . . . . . . 12  Z5 I5 O T4 Z4 L4 I4 V3 I3 2
+11 . . . . . . . . . . . . . . 11
+10 . . . . X . . . . . . . . . 10  Green(O): 5
+ 9 . . . X X X . . . . . . . . 9   1 F L5 N P T5 U V5 W Y
+ 8 . . . . X . . . . . . . . . 8   Z5 I5 O T4 Z4 L4 I4 V3 I3 2
+ 7 . . . . . . . . . . . . . . 7
+ 6 . . . . . . . .>O . . . . . 6
+ 5 . . . . . . . O O O . . . . 5
+ 4 . . . . . . . . O . . . . . 4
+ 3 . . . . . . . . . . . . . . 3
+ 2 . . . . . . . . . . . . . . 2
+ 1 . . . . . . . . . . . . . . 1
+   A B C D E F G H I J K L M N
+
+quit
+=
+```
diff --git a/pentobi_kde_thumbnailer/CMakeLists.txt b/pentobi_kde_thumbnailer/CMakeLists.txt
new file mode 100644 (file)
index 0000000..b75f997
--- /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}")
+
+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/pentobi_kde_thumbnailer/CTestCustom.cmake b/pentobi_kde_thumbnailer/CTestCustom.cmake
new file mode 100644 (file)
index 0000000..a93a163
--- /dev/null
@@ -0,0 +1,4 @@
+# We don't want to run appstreamtest added by KDECMakeSettings because it
+# requires an Internet connection to check the screenshot images. This file
+# must be copied to the build root directory to disable the test.
+set(CTEST_CUSTOM_TESTS_IGNORE appstreamtest)
diff --git a/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp b/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp
new file mode 100644 (file)
index 0000000..c7bf90a
--- /dev/null
@@ -0,0 +1,29 @@
+//-----------------------------------------------------------------------------
+/** @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);
+    return createThumbnail(path, width, height, image);
+}
+
+//-----------------------------------------------------------------------------
diff --git a/pentobi_kde_thumbnailer/PentobiThumbCreator.h b/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/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop b/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop
new file mode 100644 (file)
index 0000000..0848d63
--- /dev/null
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Type=Service
+Name=Blokus games
+Name[de]=Blokus-Partien
+Name[fr]=Parties de Blokus
+Name[nb_NO]=Blokus-spill
+Name[zh_CN]=角斗士游戏
+ServiceTypes=ThumbCreator
+MimeType=application/x-blokus-sgf;
+X-KDE-Library=pentobi-thumbnail
diff --git a/pentobi_thumbnailer/CMakeLists.txt b/pentobi_thumbnailer/CMakeLists.txt
new file mode 100644 (file)
index 0000000..3c9240b
--- /dev/null
@@ -0,0 +1,101 @@
+set(CMAKE_AUTORCC ON)
+
+find_package(Qt5Core 5.11 REQUIRED)
+find_package(Qt5LinguistTools 5.11 REQUIRED)
+find_package(Gettext 0.18 REQUIRED)
+find_package(DocBookXSL REQUIRED)
+find_program(ITSTOOL itstool)
+if(NOT ITSTOOL)
+    message(FATAL_ERROR "itstool not found")
+endif()
+find_program(XSLTPROC xsltproc)
+if(NOT XSLTPROC)
+    message(FATAL_ERROR "xsltproc not found")
+endif()
+
+file(READ "${CMAKE_CURRENT_SOURCE_DIR}/po/LINGUAS" linguas)
+string(REGEX REPLACE "\n" ";" linguas "${linguas}")
+
+set(man_files "pentobi-thumbnailer.6")
+foreach(lang ${linguas})
+    list(APPEND man_files ${lang}/pentobi-thumbnailer.6)
+endforeach()
+
+foreach(lang ${linguas})
+    add_custom_command(OUTPUT ${lang}.mo
+        COMMAND "${GETTEXT_MSGFMT_EXECUTABLE}" -o ${lang}.mo
+        "${CMAKE_CURRENT_SOURCE_DIR}/po/${lang}.po"
+        DEPENDS po/${lang}.po
+        )
+    list(APPEND po_files po/${lang}.po)
+    list(APPEND mo_files ${lang}.mo)
+endforeach()
+
+qt5_add_translation(pentobi_thumbnailer_QM
+    i18n/de.ts
+    i18n/es.ts
+    i18n/ru.ts
+    OPTIONS -removeidentical -nounfinished
+    )
+add_custom_command(
+    OUTPUT "translations.qrc"
+    COMMAND ${CMAKE_COMMAND} -E copy
+    ${CMAKE_CURRENT_SOURCE_DIR}/i18n/translations.qrc .
+    DEPENDS i18n/translations.qrc ${pentobi_thumbnailer_QM}
+    )
+
+add_executable(pentobi-thumbnailer
+    Main.cpp
+    ${CMAKE_CURRENT_BINARY_DIR}/translations.qrc
+    ${man_files}
+    )
+
+target_link_libraries(pentobi-thumbnailer pentobi_thumbnail)
+
+target_compile_definitions(pentobi-thumbnailer PRIVATE
+    QT_DEPRECATED_WARNINGS
+    QT_DISABLE_DEPRECATED_BEFORE=0x060000
+    VERSION="${PENTOBI_VERSION}"
+    )
+
+configure_file(pentobi.thumbnailer.in pentobi.thumbnailer @ONLY)
+
+install(TARGETS pentobi-thumbnailer DESTINATION ${CMAKE_INSTALL_BINDIR})
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.thumbnailer
+  DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/thumbnailers)
+
+# Man page
+
+configure_file(pentobi-thumbnailer-manpage.docbook.in
+    pentobi-thumbnailer-manpage.docbook @ONLY)
+add_custom_command(OUTPUT pentobi-thumbnailer.6
+    COMMAND "${XSLTPROC}" --nonet --novalid --path "${DOCBOOKXSL_DIR}/manpages"
+    "${CMAKE_CURRENT_SOURCE_DIR}/../pentobi/unix/manpage.xsl"
+    pentobi-thumbnailer-manpage.docbook
+    DEPENDS
+    "${CMAKE_CURRENT_BINARY_DIR}/pentobi-thumbnailer-manpage.docbook"
+    ../pentobi/unix/manpage.xsl
+)
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pentobi-thumbnailer.6"
+    DESTINATION "${CMAKE_INSTALL_MANDIR}/man6")
+foreach(lang ${linguas})
+    add_custom_command(OUTPUT ${lang}/pentobi-thumbnailer-manpage.docbook
+        COMMAND ${CMAKE_COMMAND} -E make_directory ${lang}
+        COMMAND "${ITSTOOL}" -l ${lang} -m ${lang}.mo
+        -o ${lang}/pentobi-thumbnailer-manpage.docbook
+        -i "${CMAKE_CURRENT_SOURCE_DIR}/../pentobi/unix/manpage.its"
+        pentobi-thumbnailer-manpage.docbook
+        DEPENDS ${lang}.mo "${CMAKE_CURRENT_BINARY_DIR}/pentobi-thumbnailer-manpage.docbook"
+        )
+    add_custom_command(OUTPUT ${lang}/pentobi-thumbnailer.6
+        COMMAND "${XSLTPROC}" --nonet --novalid
+        --path "${DOCBOOKXSL_DIR}/manpages" -o ${lang}/
+        "${CMAKE_CURRENT_SOURCE_DIR}/../pentobi/unix/manpage.xsl"
+        ${lang}/pentobi-thumbnailer-manpage.docbook
+        DEPENDS
+        "${CMAKE_CURRENT_BINARY_DIR}/${lang}/pentobi-thumbnailer-manpage.docbook"
+        ../pentobi/unix/manpage.xsl
+        )
+    install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${lang}/pentobi-thumbnailer.6"
+        DESTINATION "${CMAKE_INSTALL_MANDIR}/${lang}/man6")
+endforeach()
diff --git a/pentobi_thumbnailer/Main.cpp b/pentobi_thumbnailer/Main.cpp
new file mode 100644 (file)
index 0000000..15267b1
--- /dev/null
@@ -0,0 +1,105 @@
+//-----------------------------------------------------------------------------
+/** @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 <QLocale>
+#include <QLibraryInfo>
+#include <QString>
+#include <QTranslator>
+#include "libboardgame_base/Log.h"
+#include "libpentobi_thumbnail/CreateThumbnail.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char* argv[])
+{
+    libboardgame_base::LogInitializer log_initializer;
+    QCoreApplication::setApplicationVersion(QStringLiteral(VERSION));
+    QCoreApplication app(argc, argv);
+
+    QTranslator qtTranslator;
+    qtTranslator.load(
+                "qt_" + QLocale::system().name(),
+                QLibraryInfo::location(QLibraryInfo::TranslationsPath));
+    QCoreApplication::installTranslator(&qtTranslator);
+
+    QTranslator translator;
+    translator.load(":i18n/" + QLocale::system().name());
+    QCoreApplication::installTranslator(&translator);
+
+    try
+    {
+        QCommandLineParser parser;
+        parser.setApplicationDescription(
+                    QCoreApplication::translate(
+                        "main",
+                        "thumbnailer for Blokus game records as used by Pentobi"));
+        QCommandLineOption optionSize(
+                    QStringList() << QStringLiteral("s")
+                    << QStringLiteral("size"),
+                    //: Description for command line option --size
+                    QCoreApplication::translate(
+                        "main",
+                        "Generate image with height and width <size>."),
+                    //: Value name for command line option --size
+                    QCoreApplication::translate("main", "size"),
+                    QStringLiteral("128"));
+        parser.addOption(optionSize);
+        parser.addHelpOption();
+        parser.addVersionOption();
+        parser.addPositionalArgument(
+                    //: Name of input file command line argument.
+                    QCoreApplication::translate("main", "input.blksgf"),
+                    QCoreApplication::translate(
+                        "main",
+                        //: Description of input file command line argument.
+                        "Blokus SGF input file."));
+        parser.addPositionalArgument(
+                    //: Name of output image file command line argument.
+                    QCoreApplication::translate("main", "output.png"),
+                    QCoreApplication::translate(
+                        "main",
+                        //: Description of output file command line argument.
+                        "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 QCoreApplication::translate("main", "Invalid image size");
+        if (args.size() > 2)
+            throw QCoreApplication::translate("main", "Too many arguments");
+        if (args.size() < 2)
+            throw QCoreApplication::translate(
+                    "main", "Need input and output file argument");
+        QImage image(size, size, QImage::Format_ARGB32);
+        if (! createThumbnail(args.at(0), size, size, image))
+            throw QCoreApplication::translate(
+                    "main", "Thumbnail creation failed");
+        QImageWriter writer(args.at(1), "png");
+        if (! writer.write(image))
+            throw writer.errorString();
+    }
+    catch (const QString& s)
+    {
+        cerr << s.toLocal8Bit().constData() << '\n';
+        return 1;
+    }
+    catch (const exception& e)
+    {
+        cerr << e.what() << '\n';
+        return 1;
+    }
+    return 0;
+}
+
+//-----------------------------------------------------------------------------
diff --git a/pentobi_thumbnailer/create-pot b/pentobi_thumbnailer/create-pot
new file mode 100755 (executable)
index 0000000..a832651
--- /dev/null
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+cd $(dirname "$0")
+itstool -i ../pentobi/unix/manpage.its pentobi-thumbnailer-manpage.docbook.in \
+  > pentobi-thumbnailer.pot
diff --git a/pentobi_thumbnailer/i18n/de.ts b/pentobi_thumbnailer/i18n/de.ts
new file mode 100644 (file)
index 0000000..8a74522
--- /dev/null
@@ -0,0 +1,53 @@
+<?xml version="1.0" ?><!DOCTYPE TS><TS language="de" version="2.1">
+<context>
+    <name>main</name>
+    <message>
+        <source>thumbnailer for Blokus game records as used by Pentobi</source>
+        <translation>Vorschaubildgenerator für Blokus-Spieldateien, wie vom Programm Pentobi verwendet</translation>
+    </message>
+    <message>
+        <source>Generate image with height and width &lt;size&gt;.</source>
+        <extracomment>Description for command line option --size</extracomment>
+        <translation>Bilder mit Höhe und Breite &lt;Größe&gt; generieren.</translation>
+    </message>
+    <message>
+        <source>size</source>
+        <extracomment>Value name for command line option --size</extracomment>
+        <translation>Größe</translation>
+    </message>
+    <message>
+        <source>input.blksgf</source>
+        <extracomment>Name of input file command line argument.</extracomment>
+        <translation>Eingabe.blksgf</translation>
+    </message>
+    <message>
+        <source>Blokus SGF input file.</source>
+        <translation>Blokus-SGF-Eingabedatei.</translation>
+    </message>
+    <message>
+        <source>output.png</source>
+        <extracomment>Name of output image file command line argument.</extracomment>
+        <translation>Ausgabe.png</translation>
+    </message>
+    <message>
+        <source>PNG image output file.</source>
+        <translation>PNG-Bild-Ausgabedatei.</translation>
+    </message>
+    <message>
+        <source>Invalid image size</source>
+        <translation>Ungültige Bildgröße</translation>
+    </message>
+    <message>
+        <source>Too many arguments</source>
+        <translation>Zu viele Argumente</translation>
+    </message>
+    <message>
+        <source>Need input and output file argument</source>
+        <translation>Benötige Eingabe- und Ausgabedateiargument</translation>
+    </message>
+    <message>
+        <source>Thumbnail creation failed</source>
+        <translation>Vorschaubildgenerierung fehlgeschlagen</translation>
+    </message>
+</context>
+</TS>
\ No newline at end of file
diff --git a/pentobi_thumbnailer/i18n/en.ts b/pentobi_thumbnailer/i18n/en.ts
new file mode 100644 (file)
index 0000000..82cb8fc
--- /dev/null
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+<context>
+    <name>main</name>
+    <message>
+        <source>thumbnailer for Blokus game records as used by Pentobi</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Generate image with height and width &lt;size&gt;.</source>
+        <extracomment>Description for command line option --size</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>size</source>
+        <extracomment>Value name for command line option --size</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>input.blksgf</source>
+        <extracomment>Name of input file command line argument.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Blokus SGF input file.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>output.png</source>
+        <extracomment>Name of output image file command line argument.</extracomment>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>PNG image output file.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Invalid image size</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Too many arguments</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Need input and output file argument</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Thumbnail creation failed</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+</TS>
diff --git a/pentobi_thumbnailer/i18n/es.ts b/pentobi_thumbnailer/i18n/es.ts
new file mode 100644 (file)
index 0000000..39d689a
--- /dev/null
@@ -0,0 +1,53 @@
+<?xml version="1.0" ?><!DOCTYPE TS><TS language="es" version="2.1">
+<context>
+    <name>main</name>
+    <message>
+        <source>thumbnailer for Blokus game records as used by Pentobi</source>
+        <translation>miniaturizador para registros de partidas de Blokus según su uso por Pentobi</translation>
+    </message>
+    <message>
+        <source>Generate image with height and width &lt;size&gt;.</source>
+        <extracomment>Description for command line option --size</extracomment>
+        <translation>Generar imagen con altura y anchura &lt;size&gt;.</translation>
+    </message>
+    <message>
+        <source>size</source>
+        <extracomment>Value name for command line option --size</extracomment>
+        <translation>tamaño</translation>
+    </message>
+    <message>
+        <source>input.blksgf</source>
+        <extracomment>Name of input file command line argument.</extracomment>
+        <translation>input.blksgf</translation>
+    </message>
+    <message>
+        <source>Blokus SGF input file.</source>
+        <translation>Archivo de entrada SGF de Blokus</translation>
+    </message>
+    <message>
+        <source>output.png</source>
+        <extracomment>Name of output image file command line argument.</extracomment>
+        <translation>output.png</translation>
+    </message>
+    <message>
+        <source>PNG image output file.</source>
+        <translation>Archivo de imagen de salida PNG.</translation>
+    </message>
+    <message>
+        <source>Invalid image size</source>
+        <translation>Tamaño de imagen no válido</translation>
+    </message>
+    <message>
+        <source>Too many arguments</source>
+        <translation>Demasiados argumentos</translation>
+    </message>
+    <message>
+        <source>Need input and output file argument</source>
+        <translation>Es necesario argumento de archivo de entrada y salida</translation>
+    </message>
+    <message>
+        <source>Thumbnail creation failed</source>
+        <translation>Se produjo un error en la creación de la miniatura</translation>
+    </message>
+</context>
+</TS>
\ No newline at end of file
diff --git a/pentobi_thumbnailer/i18n/ru.ts b/pentobi_thumbnailer/i18n/ru.ts
new file mode 100644 (file)
index 0000000..2eb24fa
--- /dev/null
@@ -0,0 +1,53 @@
+<?xml version="1.0" ?><!DOCTYPE TS><TS language="ru" version="2.1">
+<context>
+    <name>main</name>
+    <message>
+        <source>thumbnailer for Blokus game records as used by Pentobi</source>
+        <translation>эскиз для записей игр Блокус, используемый Pentobi</translation>
+    </message>
+    <message>
+        <source>Generate image with height and width &lt;size&gt;.</source>
+        <extracomment>Description for command line option --size</extracomment>
+        <translation>Создать изображение с высотой и шириной &lt;size&gt;.</translation>
+    </message>
+    <message>
+        <source>size</source>
+        <extracomment>Value name for command line option --size</extracomment>
+        <translation>размер</translation>
+    </message>
+    <message>
+        <source>input.blksgf</source>
+        <extracomment>Name of input file command line argument.</extracomment>
+        <translation>входной.blksgf</translation>
+    </message>
+    <message>
+        <source>Blokus SGF input file.</source>
+        <translation>Входной файл Blokus SGF.</translation>
+    </message>
+    <message>
+        <source>output.png</source>
+        <extracomment>Name of output image file command line argument.</extracomment>
+        <translation>выходной.png</translation>
+    </message>
+    <message>
+        <source>PNG image output file.</source>
+        <translation>Выходной файл изображения PNG.</translation>
+    </message>
+    <message>
+        <source>Invalid image size</source>
+        <translation>Неверный размер изображения</translation>
+    </message>
+    <message>
+        <source>Too many arguments</source>
+        <translation>Слишком много аргументов</translation>
+    </message>
+    <message>
+        <source>Need input and output file argument</source>
+        <translation>Нужен аргумент входного и выходного файлов</translation>
+    </message>
+    <message>
+        <source>Thumbnail creation failed</source>
+        <translation>Не удалось создать миниатюру</translation>
+    </message>
+</context>
+</TS>
\ No newline at end of file
diff --git a/pentobi_thumbnailer/i18n/translations.qrc b/pentobi_thumbnailer/i18n/translations.qrc
new file mode 100644 (file)
index 0000000..bc3b73f
--- /dev/null
@@ -0,0 +1,7 @@
+<RCC>
+<qresource prefix="/i18n">
+<file>de.qm</file>
+<file>es.qm</file>
+<file>ru.qm</file>
+</qresource>
+</RCC>
diff --git a/pentobi_thumbnailer/pentobi-thumbnailer-manpage.docbook.in b/pentobi_thumbnailer/pentobi-thumbnailer-manpage.docbook.in
new file mode 100644 (file)
index 0000000..baf81f9
--- /dev/null
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
+<article>
+<articleinfo><date>@PENTOBI_RELEASE_DATE@</date></articleinfo>
+
+<refentry>
+<refmeta>
+<refentrytitle>pentobi-thumbnailer</refentrytitle>
+<manvolnum>6</manvolnum>
+<refmiscinfo class="source">Pentobi</refmiscinfo>
+<refmiscinfo class="version">@PENTOBI_VERSION@</refmiscinfo>
+<refmiscinfo class="manual">Pentobi Command Reference</refmiscinfo>
+</refmeta>
+
+<refnamediv>
+<refname>pentobi-thumbnailer</refname>
+<refpurpose>
+thumbnailer for game records for the board game Blokus as used by the program
+Pentobi
+</refpurpose>
+</refnamediv>
+
+<refsynopsisdiv>
+<cmdsynopsis>
+<command>pentobi-thumbnailer</command>
+<arg>
+<group choice="plain">
+<arg choice="plain"><option>-s</option></arg>
+<arg choice="plain"><option>--size</option></arg>
+</group>
+<replaceable>n</replaceable>
+</arg>
+<arg choice="plain"><replaceable>inputfile</replaceable></arg>
+<arg choice="plain"><replaceable>outputfile</replaceable></arg>
+</cmdsynopsis>
+<cmdsynopsis>
+<command>pentobi-thumbnailer</command>
+<group choice="plain">
+<arg choice="plain"><option>-h</option></arg>
+<arg choice="plain"><option>--help</option></arg>
+</group>
+</cmdsynopsis>
+<cmdsynopsis>
+<command>pentobi-thumbnailer</command>
+<group choice="plain">
+<arg choice="plain"><option>-v</option></arg>
+<arg choice="plain"><option>--version</option></arg>
+</group>
+</cmdsynopsis>
+</refsynopsisdiv>
+
+<refsection>
+<title>Description</title>
+<para>
+<command>pentobi-thumbnailer</command> 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.
+</para>
+<para>
+The input file is a game file in Pentobi's SGF format as documented in
+Pentobi-SGF.md in the Pentobi source package. The output file is a thumbnail
+image in PNG format.
+</para>
+</refsection>
+
+<refsection>
+<title>Options</title>
+<variablelist>
+<varlistentry>
+<term><option>-h</option></term>
+<term><option>--help</option></term>
+<listitem>
+<para>
+Display help and exit.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><option>-s</option> <replaceable>n</replaceable></term>
+<term><option>--size</option> <replaceable>n</replaceable></term>
+<listitem>
+<para>
+The size of the thumbnail. The default is 128.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term><option>-v</option></term>
+<term><option>--version</option></term>
+<listitem>
+<para>
+Display version and exit.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+</refsection>
+
+<refsection>
+<title>Exit Status</title>
+<para>
+0 if the thumbnail generation succeeds, 1 on error.
+</para>
+</refsection>
+
+<refsection>
+<title>See Also</title>
+<para>
+<command>pentobi</command>
+</para>
+</refsection>
+
+</refentry>
+</article>
diff --git a/pentobi_thumbnailer/pentobi-thumbnailer.pot b/pentobi_thumbnailer/pentobi-thumbnailer.pot
new file mode 100644 (file)
index 0000000..f8a9ff7
--- /dev/null
@@ -0,0 +1,92 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2020-10-16 07:22+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Put one translator per line, in the form NAME <EMAIL>, YEAR1, YEAR2
+msgctxt "_"
+msgid "translator-credits"
+msgstr ""
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-thumbnailer-manpage.docbook.in:11
+msgid "Pentobi"
+msgstr ""
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-thumbnailer-manpage.docbook.in:13
+msgid "Pentobi Command Reference"
+msgstr ""
+
+#. (itstool) path: refnamediv/refpurpose
+#: pentobi-thumbnailer-manpage.docbook.in:18
+msgid "thumbnailer for game records for the board game Blokus as used by the program Pentobi"
+msgstr ""
+
+#. (itstool) path: refsynopsisdiv/cmdsynopsis
+#: pentobi-thumbnailer-manpage.docbook.in:25
+msgid "<_:command-1/> <arg> <group choice=\"plain\"> <arg choice=\"plain\"><_:option-2/></arg> <arg choice=\"plain\"><_:option-3/></arg> </group> <replaceable>n</replaceable> </arg> <arg choice=\"plain\"><replaceable>inputfile</replaceable></arg> <arg choice=\"plain\"><replaceable>outputfile</replaceable></arg>"
+msgstr ""
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:54
+msgid "Description"
+msgstr ""
+
+#. (itstool) path: refsection/para
+#: pentobi-thumbnailer-manpage.docbook.in:55
+msgid "<_:command-1/> 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."
+msgstr ""
+
+#. (itstool) path: refsection/para
+#: pentobi-thumbnailer-manpage.docbook.in:60
+msgid "The input file is a game file in Pentobi's SGF format as documented in Pentobi-SGF.md in the Pentobi source package. The output file is a thumbnail image in PNG format."
+msgstr ""
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:68
+msgid "Options"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-thumbnailer-manpage.docbook.in:74
+msgid "Display help and exit."
+msgstr ""
+
+#. (itstool) path: varlistentry/term
+#: pentobi-thumbnailer-manpage.docbook.in:80
+#: pentobi-thumbnailer-manpage.docbook.in:81
+msgid "<_:option-1/> <replaceable>n</replaceable>"
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-thumbnailer-manpage.docbook.in:83
+msgid "The size of the thumbnail. The default is 128."
+msgstr ""
+
+#. (itstool) path: listitem/para
+#: pentobi-thumbnailer-manpage.docbook.in:92
+msgid "Display version and exit."
+msgstr ""
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:101
+msgid "Exit Status"
+msgstr ""
+
+#. (itstool) path: refsection/para
+#: pentobi-thumbnailer-manpage.docbook.in:102
+msgid "0 if the thumbnail generation succeeds, 1 on error."
+msgstr ""
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:108
+msgid "See Also"
+msgstr ""
+
diff --git a/pentobi_thumbnailer/pentobi.thumbnailer.in b/pentobi_thumbnailer/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/pentobi_thumbnailer/po/LINGUAS b/pentobi_thumbnailer/po/LINGUAS
new file mode 100644 (file)
index 0000000..d0ea0cd
--- /dev/null
@@ -0,0 +1,3 @@
+de
+es
+ru
diff --git a/pentobi_thumbnailer/po/de.po b/pentobi_thumbnailer/po/de.po
new file mode 100644 (file)
index 0000000..7466b0a
--- /dev/null
@@ -0,0 +1,124 @@
+# 
+# Translators:
+# Markus Enzenberger <markus.enzenberger@gmail.com>, 2020
+# 
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2020-10-16 07:22+0200\n"
+"PO-Revision-Date: 2019-02-25 15:38+0000\n"
+"Last-Translator: Markus Enzenberger <markus.enzenberger@gmail.com>, 2020\n"
+"Language-Team: German (https://www.transifex.com/markus-enzenberger/teams/89074/de/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. Put one translator per line, in the form NAME <EMAIL>, YEAR1, YEAR2
+msgctxt "_"
+msgid "translator-credits"
+msgstr "Markus Enzenberger <enz@users.sourceforge.net>, 2019"
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-thumbnailer-manpage.docbook.in:11
+msgid "Pentobi"
+msgstr "Pentobi"
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-thumbnailer-manpage.docbook.in:13
+msgid "Pentobi Command Reference"
+msgstr "Pentobi Befehlsreferenz"
+
+#. (itstool) path: refnamediv/refpurpose
+#: pentobi-thumbnailer-manpage.docbook.in:18
+msgid ""
+"thumbnailer for game records for the board game Blokus as used by the "
+"program Pentobi"
+msgstr ""
+"Vorschaubildgenerator für Spieldateien des Brettspiels Blokus, wie vom "
+"Programm Pentobi verwendet"
+
+#. (itstool) path: refsynopsisdiv/cmdsynopsis
+#: pentobi-thumbnailer-manpage.docbook.in:25
+msgid ""
+"<_:command-1/> <arg> <group choice=\"plain\"> <arg "
+"choice=\"plain\"><_:option-2/></arg> <arg "
+"choice=\"plain\"><_:option-3/></arg> </group> <replaceable>n</replaceable> "
+"</arg> <arg choice=\"plain\"><replaceable>inputfile</replaceable></arg> <arg"
+" choice=\"plain\"><replaceable>outputfile</replaceable></arg>"
+msgstr ""
+"<_:command-1/> <arg> <group choice=\"plain\"> <arg "
+"choice=\"plain\"><_:option-2/></arg> <arg "
+"choice=\"plain\"><_:option-3/></arg> </group> <replaceable>n</replaceable> "
+"</arg> <arg choice=\"plain\"><replaceable>Eingabedatei</replaceable></arg> "
+"<arg choice=\"plain\"><replaceable>Ausgabedatei</replaceable></arg>"
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:54
+msgid "Description"
+msgstr "Beschreibung"
+
+#. (itstool) path: refsection/para
+#: pentobi-thumbnailer-manpage.docbook.in:55
+msgid ""
+"<_:command-1/> 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."
+msgstr ""
+"<_:command-1/> ist ein Teil des Programms Pentobi und dafür gedacht, "
+"Vorschaubilder für die Desktopumgebung Gnome zu Spieldateien zu generieren, "
+"die von Pentobi gespeichert werden."
+
+#. (itstool) path: refsection/para
+#: pentobi-thumbnailer-manpage.docbook.in:60
+msgid ""
+"The input file is a game file in Pentobi's SGF format as documented in "
+"Pentobi-SGF.md in the Pentobi source package. The output file is a thumbnail"
+" image in PNG format."
+msgstr ""
+"Die Eingabedatei ist in Pentobis SGF-Format, wie in Pentobi-SGF.md im "
+"Quelltextpaket von Pentobi dokumentiert. Die Ausgabedatei ist ein "
+"Vorschaubild im PNG-Format."
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:68
+msgid "Options"
+msgstr "Optionen"
+
+#. (itstool) path: listitem/para
+#: pentobi-thumbnailer-manpage.docbook.in:74
+msgid "Display help and exit."
+msgstr "Hilfe anzeigen und Programm beenden."
+
+#. (itstool) path: varlistentry/term
+#: pentobi-thumbnailer-manpage.docbook.in:80
+#: pentobi-thumbnailer-manpage.docbook.in:81
+msgid "<_:option-1/> <replaceable>n</replaceable>"
+msgstr "<_:option-1/> <replaceable>n</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-thumbnailer-manpage.docbook.in:83
+msgid "The size of the thumbnail. The default is 128."
+msgstr "Die Größe des Vorschaubildes. Voreingestellt ist 128."
+
+#. (itstool) path: listitem/para
+#: pentobi-thumbnailer-manpage.docbook.in:92
+msgid "Display version and exit."
+msgstr "Version anzeigen und Programm beenden."
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:101
+msgid "Exit Status"
+msgstr "Rückgabewert"
+
+#. (itstool) path: refsection/para
+#: pentobi-thumbnailer-manpage.docbook.in:102
+msgid "0 if the thumbnail generation succeeds, 1 on error."
+msgstr ""
+"0, wenn die Generierung des Vorschaubildes erfolgreich ist, 1 im Fehlerfall."
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:108
+msgid "See Also"
+msgstr "Siehe auch"
diff --git a/pentobi_thumbnailer/po/es.po b/pentobi_thumbnailer/po/es.po
new file mode 100644 (file)
index 0000000..faef5f1
--- /dev/null
@@ -0,0 +1,121 @@
+# 
+# Translators:
+# Francisco Zamorano <pacozamo@gmail.com>, 2019
+# 
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2020-10-16 07:22+0200\n"
+"PO-Revision-Date: 2019-02-25 15:38+0000\n"
+"Last-Translator: Francisco Zamorano <pacozamo@gmail.com>, 2019\n"
+"Language-Team: Spanish (https://www.transifex.com/markus-enzenberger/teams/89074/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. Put one translator per line, in the form NAME <EMAIL>, YEAR1, YEAR2
+msgctxt "_"
+msgid "translator-credits"
+msgstr "traductor-créditos"
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-thumbnailer-manpage.docbook.in:11
+msgid "Pentobi"
+msgstr "Pentobi"
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-thumbnailer-manpage.docbook.in:13
+msgid "Pentobi Command Reference"
+msgstr "Referencia de comandos de Pentobi"
+
+#. (itstool) path: refnamediv/refpurpose
+#: pentobi-thumbnailer-manpage.docbook.in:18
+msgid ""
+"thumbnailer for game records for the board game Blokus as used by the "
+"program Pentobi"
+msgstr ""
+"miniaturizador para registros de partidas para el juego de mesa Blokus según"
+" su uso por parte del programa Pentobi"
+
+#. (itstool) path: refsynopsisdiv/cmdsynopsis
+#: pentobi-thumbnailer-manpage.docbook.in:25
+msgid ""
+"<_:command-1/> <arg> <group choice=\"plain\"> <arg "
+"choice=\"plain\"><_:option-2/></arg> <arg "
+"choice=\"plain\"><_:option-3/></arg> </group> <replaceable>n</replaceable> "
+"</arg> <arg choice=\"plain\"><replaceable>inputfile</replaceable></arg> <arg"
+" choice=\"plain\"><replaceable>outputfile</replaceable></arg>"
+msgstr ""
+"<_:command-1/> <arg> <group choice=\"plain\"> <arg "
+"choice=\"plain\"><_:option-2/></arg> <arg "
+"choice=\"plain\"><_:option-3/></arg> </group> <replaceable>n</replaceable> "
+"</arg> <arg choice=\"plain\"><replaceable>inputfile</replaceable></arg> <arg"
+" choice=\"plain\"><replaceable>outputfile</replaceable></arg>"
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:54
+msgid "Description"
+msgstr "Descripción"
+
+#. (itstool) path: refsection/para
+#: pentobi-thumbnailer-manpage.docbook.in:55
+msgid ""
+"<_:command-1/> 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."
+msgstr ""
+"<_:command-1/> es parte del programa Pentobi y sirve como miniaturizador "
+"para el entorno de escritorio Gnome para generar vistas previas de los "
+"archivos de partida escritos por Pentobi."
+
+#. (itstool) path: refsection/para
+#: pentobi-thumbnailer-manpage.docbook.in:60
+msgid ""
+"The input file is a game file in Pentobi's SGF format as documented in "
+"Pentobi-SGF.md in the Pentobi source package. The output file is a thumbnail"
+" image in PNG format."
+msgstr ""
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:68
+msgid "Options"
+msgstr "Opciones"
+
+#. (itstool) path: listitem/para
+#: pentobi-thumbnailer-manpage.docbook.in:74
+msgid "Display help and exit."
+msgstr "Mostrar ayuda y salir."
+
+#. (itstool) path: varlistentry/term
+#: pentobi-thumbnailer-manpage.docbook.in:80
+#: pentobi-thumbnailer-manpage.docbook.in:81
+msgid "<_:option-1/> <replaceable>n</replaceable>"
+msgstr "<_:option-1/> <replaceable>n</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-thumbnailer-manpage.docbook.in:83
+msgid "The size of the thumbnail. The default is 128."
+msgstr "El tamaño de la miniatura. El que viene por defecto es 128."
+
+#. (itstool) path: listitem/para
+#: pentobi-thumbnailer-manpage.docbook.in:92
+msgid "Display version and exit."
+msgstr "Mostrar versión y salir."
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:101
+msgid "Exit Status"
+msgstr "Estado de salida"
+
+#. (itstool) path: refsection/para
+#: pentobi-thumbnailer-manpage.docbook.in:102
+msgid "0 if the thumbnail generation succeeds, 1 on error."
+msgstr ""
+"0 si la generación de miniaturas tiene éxito; 1 si se produce un error."
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:108
+msgid "See Also"
+msgstr "Consulte también"
diff --git a/pentobi_thumbnailer/po/ru.po b/pentobi_thumbnailer/po/ru.po
new file mode 100644 (file)
index 0000000..3d438d8
--- /dev/null
@@ -0,0 +1,121 @@
+# 
+# Translators:
+# Виктор Ерухин <official159ru@mail.ru>, 2020
+# 
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2020-10-16 07:22+0200\n"
+"PO-Revision-Date: 2019-02-25 15:38+0000\n"
+"Last-Translator: Виктор Ерухин <official159ru@mail.ru>, 2020\n"
+"Language-Team: Russian (https://www.transifex.com/markus-enzenberger/teams/89074/ru/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
+
+#. Put one translator per line, in the form NAME <EMAIL>, YEAR1, YEAR2
+msgctxt "_"
+msgid "translator-credits"
+msgstr "переводчик-вклад"
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-thumbnailer-manpage.docbook.in:11
+msgid "Pentobi"
+msgstr "Pentobi"
+
+#. (itstool) path: refmeta/refmiscinfo
+#: pentobi-thumbnailer-manpage.docbook.in:13
+msgid "Pentobi Command Reference"
+msgstr "Справочник команд Pentobi"
+
+#. (itstool) path: refnamediv/refpurpose
+#: pentobi-thumbnailer-manpage.docbook.in:18
+msgid ""
+"thumbnailer for game records for the board game Blokus as used by the "
+"program Pentobi"
+msgstr "эскиз для записей игр Блокус, используемый Pentobi"
+
+#. (itstool) path: refsynopsisdiv/cmdsynopsis
+#: pentobi-thumbnailer-manpage.docbook.in:25
+msgid ""
+"<_:command-1/> <arg> <group choice=\"plain\"> <arg "
+"choice=\"plain\"><_:option-2/></arg> <arg "
+"choice=\"plain\"><_:option-3/></arg> </group> <replaceable>n</replaceable> "
+"</arg> <arg choice=\"plain\"><replaceable>inputfile</replaceable></arg> <arg"
+" choice=\"plain\"><replaceable>outputfile</replaceable></arg>"
+msgstr ""
+"<_:command-1/> <arg> <group choice=\"plain\"> <arg "
+"choice=\"plain\"><_:option-2/></arg> <arg "
+"choice=\"plain\"><_:option-3/></arg> </group> <replaceable>n</replaceable> "
+"</arg> <arg choice=\"plain\"><replaceable>входной</replaceable></arg> <arg "
+"choice=\"plain\"><replaceable>выходной</replaceable></arg>"
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:54
+msgid "Description"
+msgstr "Описание"
+
+#. (itstool) path: refsection/para
+#: pentobi-thumbnailer-manpage.docbook.in:55
+msgid ""
+"<_:command-1/> 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."
+msgstr ""
+"<_:command-1/> является частью программы Pentobi и предназначен для "
+"использования в качестве эскиза в среде рабочего стола Gnome для создания "
+"превью игровых файлов, написанных Pentobi."
+
+#. (itstool) path: refsection/para
+#: pentobi-thumbnailer-manpage.docbook.in:60
+msgid ""
+"The input file is a game file in Pentobi's SGF format as documented in "
+"Pentobi-SGF.md in the Pentobi source package. The output file is a thumbnail"
+" image in PNG format."
+msgstr ""
+"Входной файл Pentobi представляет собой игровой файл в формате SGF, как "
+"описано в Pentobi-SGF.md в исходном пакете Pentobi. Выходной файл "
+"представляет собой уменьшенное изображение в формате PNG."
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:68
+msgid "Options"
+msgstr "Параметры"
+
+#. (itstool) path: listitem/para
+#: pentobi-thumbnailer-manpage.docbook.in:74
+msgid "Display help and exit."
+msgstr "Показать справку и выйти."
+
+#. (itstool) path: varlistentry/term
+#: pentobi-thumbnailer-manpage.docbook.in:80
+#: pentobi-thumbnailer-manpage.docbook.in:81
+msgid "<_:option-1/> <replaceable>n</replaceable>"
+msgstr "<_:option-1/> <replaceable>n</replaceable>"
+
+#. (itstool) path: listitem/para
+#: pentobi-thumbnailer-manpage.docbook.in:83
+msgid "The size of the thumbnail. The default is 128."
+msgstr "Размер эскиза. По умолчанию 128."
+
+#. (itstool) path: listitem/para
+#: pentobi-thumbnailer-manpage.docbook.in:92
+msgid "Display version and exit."
+msgstr "Показать версию и выйти."
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:101
+msgid "Exit Status"
+msgstr "Состояние завершения"
+
+#. (itstool) path: refsection/para
+#: pentobi-thumbnailer-manpage.docbook.in:102
+msgid "0 if the thumbnail generation succeeds, 1 on error."
+msgstr "0 в случае успешной генерации эскиза, 1 в случае ошибки."
+
+#. (itstool) path: refsection/title
+#: pentobi-thumbnailer-manpage.docbook.in:108
+msgid "See Also"
+msgstr "Смотрите также"
diff --git a/twogtp/Analyze.cpp b/twogtp/Analyze.cpp
new file mode 100644 (file)
index 0000000..3bb6b20
--- /dev/null
@@ -0,0 +1,135 @@
+//-----------------------------------------------------------------------------
+/** @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_base/FmtSaver.h"
+#include "libboardgame_base/Statistics.h"
+#include "libboardgame_base/StringUtil.h"
+
+using namespace std;
+using libboardgame_base::from_string;
+using libboardgame_base::split;
+using libboardgame_base::trim;
+using libboardgame_base::FmtSaver;
+using libboardgame_base::Statistics;
+using libboardgame_base::StatisticsExt;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void write_result(const Statistics<>& stat)
+{
+    FmtSaver saver(cout);
+    cout << fixed << setprecision(1) << stat.get_mean() * 100 << u8"±"
+         << 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
+                 << u8"±" << 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 << u8"±"
+             << 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/twogtp/Analyze.h b/twogtp/Analyze.h
new file mode 100644 (file)
index 0000000..4f4dedb
--- /dev/null
@@ -0,0 +1,18 @@
+//-----------------------------------------------------------------------------
+/** @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>
+
+//-----------------------------------------------------------------------------
+
+void analyze(const std::string& file);
+
+//-----------------------------------------------------------------------------
+
+#endif // TWOGTP_ANALYZE_H
diff --git a/twogtp/CMakeLists.txt b/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/twogtp/FdStream.cpp b/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/twogtp/FdStream.h b/twogtp/FdStream.h
new file mode 100644 (file)
index 0000000..e046b87
--- /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/twogtp/GtpConnection.cpp b/twogtp/GtpConnection.cpp
new file mode 100644 (file)
index 0000000..f5fa935
--- /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_base/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/twogtp/GtpConnection.h b/twogtp/GtpConnection.h
new file mode 100644 (file)
index 0000000..6e47a06
--- /dev/null
@@ -0,0 +1,54 @@
+//-----------------------------------------------------------------------------
+/** @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 <stdexcept>
+#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/twogtp/Main.cpp b/twogtp/Main.cpp
new file mode 100644 (file)
index 0000000..9b2ce68
--- /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_base/Log.h"
+#include "libboardgame_base/Options.h"
+#include "libpentobi_base/Variant.h"
+
+using namespace std;
+using libboardgame_base::Options;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char** argv)
+{
+    libboardgame_base::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_base::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/twogtp/Output.cpp b/twogtp/Output.cpp
new file mode 100644 (file)
index 0000000..eec6db6
--- /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_base/StringUtil.h"
+
+using libboardgame_base::from_string;
+using libboardgame_base::split;
+using libboardgame_base::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({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 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({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 lock(m_mutex);
+    m_output_tree.generate_move(is_player_black, bd, to_play, mv);
+    return ! mv.is_null();
+}
+
+unsigned Output::get_next()
+{
+    lock_guard lock(m_mutex);
+    unsigned n = m_next;
+    do
+       ++m_next;
+    while (m_games.count(m_next) != 0);
+    return n;
+}
+
+void Output::save()
+{
+    lock_guard 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/twogtp/Output.h b/twogtp/Output.h
new file mode 100644 (file)
index 0000000..dbcb785
--- /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_base/Timer.h"
+#include "libboardgame_base/WallTimeSource.h"
+
+using libboardgame_base::Timer;
+using libboardgame_base::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/twogtp/OutputTree.cpp b/twogtp/OutputTree.cpp
new file mode 100644 (file)
index 0000000..0d3ab83
--- /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_base/TreeReader.h"
+#include "libboardgame_base/TreeWriter.h"
+#include "libpentobi_base/BoardUtil.h"
+
+using libboardgame_base::ArrayList;
+using libboardgame_base::SgfNode;
+using libboardgame_base::TreeReader;
+using libboardgame_base::TreeWriter;
+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, float result)
+{
+    unsigned index = is_player_black ? 0 : 1;
+    array<unsigned short, 2> count;
+    array<float, 2> avg_result;
+    array<unsigned short, 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,
+                          float 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/twogtp/OutputTree.h b/twogtp/OutputTree.h
new file mode 100644 (file)
index 0000000..dddfeed
--- /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, float 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/twogtp/TwoGtp.cpp b/twogtp/TwoGtp.cpp
new file mode 100644 (file)
index 0000000..7c505f8
--- /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_base/Log.h"
+#include "libboardgame_base/Writer.h"
+#include "libpentobi_base/ScoreUtil.h"
+
+using libboardgame_base::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 || (m_bd.get_break_ties() && 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/twogtp/TwoGtp.h b/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