Upload 211 files
Browse filesThis view is limited to 50 files because it contains too many changes.  
							See raw diff
- .gitattributes +44 -0
 - katrain/CONTRIBUTIONS.md +79 -0
 - katrain/ENGINE.md +74 -0
 - katrain/INSTALL.md +177 -0
 - katrain/LICENSE +40 -0
 - katrain/README.md +253 -0
 - katrain/THEMES.md +86 -0
 - katrain/__main__.spec +38 -0
 - katrain/__pycache__/board_ai.cpython-310.pyc +0 -0
 - katrain/__pycache__/engine_ai.cpython-310.pyc +0 -0
 - katrain/__pycache__/hongik_ai.cpython-310.pyc +0 -0
 - katrain/fonts/Roboto-Black.ttf +3 -0
 - katrain/fonts/Roboto-BlackItalic.ttf +3 -0
 - katrain/fonts/Roboto-Bold.ttf +3 -0
 - katrain/fonts/Roboto-BoldItalic.ttf +3 -0
 - katrain/fonts/Roboto-Italic.ttf +3 -0
 - katrain/fonts/Roboto-Light.ttf +3 -0
 - katrain/fonts/Roboto-LightItalic.ttf +3 -0
 - katrain/fonts/Roboto-Medium.ttf +3 -0
 - katrain/fonts/Roboto-MediumItalic.ttf +3 -0
 - katrain/fonts/Roboto-Regular.ttf +3 -0
 - katrain/fonts/Roboto-Thin.ttf +3 -0
 - katrain/fonts/Roboto-ThinItalic.ttf +3 -0
 - katrain/i18n.py +125 -0
 - katrain/katrain.py +4 -0
 - katrain/katrain/__init__.py +0 -0
 - katrain/katrain/__main__.py +455 -0
 - katrain/katrain/__main__.spec +38 -0
 - katrain/katrain/__pycache__/__init__.cpython-310.pyc +0 -0
 - katrain/katrain/config.json +235 -0
 - katrain/katrain/core/__init__.py +0 -0
 - katrain/katrain/core/__pycache__/__init__.cpython-310.pyc +0 -0
 - katrain/katrain/core/__pycache__/ai.cpython-310.pyc +0 -0
 - katrain/katrain/core/__pycache__/base_katrain.cpython-310.pyc +0 -0
 - katrain/katrain/core/__pycache__/constants.cpython-310.pyc +0 -0
 - katrain/katrain/core/__pycache__/game.cpython-310.pyc +0 -0
 - katrain/katrain/core/__pycache__/game_node.cpython-310.pyc +0 -0
 - katrain/katrain/core/__pycache__/lang.cpython-310.pyc +0 -0
 - katrain/katrain/core/__pycache__/sgf_parser.cpython-310.pyc +0 -0
 - katrain/katrain/core/__pycache__/utils.cpython-310.pyc +0 -0
 - katrain/katrain/core/ai.py +516 -0
 - katrain/katrain/core/base_katrain.py +96 -0
 - katrain/katrain/core/constants.py +272 -0
 - katrain/katrain/core/contribute_engine.py +302 -0
 - katrain/katrain/core/game.py +818 -0
 - katrain/katrain/core/game_node.py +466 -0
 - katrain/katrain/core/lang.py +89 -0
 - katrain/katrain/core/sgf_parser.py +714 -0
 - katrain/katrain/core/tsumego_frame.py +289 -0
 - katrain/katrain/core/utils.py +99 -0
 
    	
        .gitattributes
    CHANGED
    
    | 
         @@ -33,3 +33,47 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text 
     | 
|
| 33 | 
         
             
            *.zip filter=lfs diff=lfs merge=lfs -text
         
     | 
| 34 | 
         
             
            *.zst filter=lfs diff=lfs merge=lfs -text
         
     | 
| 35 | 
         
             
            *tfevents* filter=lfs diff=lfs merge=lfs -text
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 33 | 
         
             
            *.zip filter=lfs diff=lfs merge=lfs -text
         
     | 
| 34 | 
         
             
            *.zst filter=lfs diff=lfs merge=lfs -text
         
     | 
| 35 | 
         
             
            *tfevents* filter=lfs diff=lfs merge=lfs -text
         
     | 
| 36 | 
         
            +
            katrain/fonts/Roboto-Black.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 37 | 
         
            +
            katrain/fonts/Roboto-BlackItalic.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 38 | 
         
            +
            katrain/fonts/Roboto-Bold.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 39 | 
         
            +
            katrain/fonts/Roboto-BoldItalic.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 40 | 
         
            +
            katrain/fonts/Roboto-Italic.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 41 | 
         
            +
            katrain/fonts/Roboto-Light.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 42 | 
         
            +
            katrain/fonts/Roboto-LightItalic.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 43 | 
         
            +
            katrain/fonts/Roboto-Medium.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 44 | 
         
            +
            katrain/fonts/Roboto-MediumItalic.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 45 | 
         
            +
            katrain/fonts/Roboto-Regular.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 46 | 
         
            +
            katrain/fonts/Roboto-Thin.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 47 | 
         
            +
            katrain/fonts/Roboto-ThinItalic.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 48 | 
         
            +
            katrain/katrain/fonts/materialdesignicons-webfont.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 49 | 
         
            +
            katrain/katrain/fonts/NotoSans-Regular.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 50 | 
         
            +
            katrain/katrain/fonts/NotoSansCJKsc-Regular.otf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 51 | 
         
            +
            katrain/katrain/fonts/NotoSansJP-Regular.otf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 52 | 
         
            +
            katrain/katrain/fonts/Roboto-Black.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 53 | 
         
            +
            katrain/katrain/fonts/Roboto-BlackItalic.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 54 | 
         
            +
            katrain/katrain/fonts/Roboto-Bold.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 55 | 
         
            +
            katrain/katrain/fonts/Roboto-BoldItalic.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 56 | 
         
            +
            katrain/katrain/fonts/Roboto-Italic.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 57 | 
         
            +
            katrain/katrain/fonts/Roboto-Light.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 58 | 
         
            +
            katrain/katrain/fonts/Roboto-LightItalic.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 59 | 
         
            +
            katrain/katrain/fonts/Roboto-Medium.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 60 | 
         
            +
            katrain/katrain/fonts/Roboto-MediumItalic.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 61 | 
         
            +
            katrain/katrain/fonts/Roboto-Regular.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 62 | 
         
            +
            katrain/katrain/fonts/Roboto-Thin.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 63 | 
         
            +
            katrain/katrain/fonts/Roboto-ThinItalic.ttf filter=lfs diff=lfs merge=lfs -text
         
     | 
| 64 | 
         
            +
            katrain/katrain/img/B_stone.png filter=lfs diff=lfs merge=lfs -text
         
     | 
| 65 | 
         
            +
            katrain/katrain/img/board.png filter=lfs diff=lfs merge=lfs -text
         
     | 
| 66 | 
         
            +
            katrain/katrain/img/icon.ico filter=lfs diff=lfs merge=lfs -text
         
     | 
| 67 | 
         
            +
            katrain/katrain/img/icon.ico__ filter=lfs diff=lfs merge=lfs -text
         
     | 
| 68 | 
         
            +
            katrain/katrain/img/inner.png filter=lfs diff=lfs merge=lfs -text
         
     | 
| 69 | 
         
            +
            katrain/katrain/img/W_stone.png filter=lfs diff=lfs merge=lfs -text
         
     | 
| 70 | 
         
            +
            katrain/katrain/sounds/boing.wav filter=lfs diff=lfs merge=lfs -text
         
     | 
| 71 | 
         
            +
            katrain/katrain/sounds/countdownbeep.wav filter=lfs diff=lfs merge=lfs -text
         
     | 
| 72 | 
         
            +
            katrain/themes/blended-all.png filter=lfs diff=lfs merge=lfs -text
         
     | 
| 73 | 
         
            +
            katrain/themes/blended-weak.png filter=lfs diff=lfs merge=lfs -text
         
     | 
| 74 | 
         
            +
            katrain/themes/blocks-none.png filter=lfs diff=lfs merge=lfs -text
         
     | 
| 75 | 
         
            +
            katrain/themes/eric-lizzie.png filter=lfs diff=lfs merge=lfs -text
         
     | 
| 76 | 
         
            +
            katrain/themes/koast.png filter=lfs diff=lfs merge=lfs -text
         
     | 
| 77 | 
         
            +
            katrain/themes/marks-weak.png filter=lfs diff=lfs merge=lfs -text
         
     | 
| 78 | 
         
            +
            katrain/themes/shaded-all.png filter=lfs diff=lfs merge=lfs -text
         
     | 
| 79 | 
         
            +
            katrain/themes/shaded-no-alpha.png filter=lfs diff=lfs merge=lfs -text
         
     | 
    	
        katrain/CONTRIBUTIONS.md
    ADDED
    
    | 
         @@ -0,0 +1,79 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            # Contributing 
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
            If you are a new contributor wanting to make a larger contribution,
         
     | 
| 4 | 
         
            +
             please first discuss the change you wish to make via
         
     | 
| 5 | 
         
            +
             an issue, reddit or discord before making a pull request.
         
     | 
| 6 | 
         
            +
             
     | 
| 7 | 
         
            +
            ## Python contributions
         
     | 
| 8 | 
         
            +
             
     | 
| 9 | 
         
            +
            Python code is formatted using [black](https://github.com/psf/black) with the settings `-l 120`.
         
     | 
| 10 | 
         
            +
            This is not enforced, and contributions with incorrect formatting will be accepted, but formatting this way is appreciated.
         
     | 
| 11 | 
         
            +
             
     | 
| 12 | 
         
            +
            ## Translations
         
     | 
| 13 | 
         
            +
             
     | 
| 14 | 
         
            +
            ### Contributing to an existing translation
         
     | 
| 15 | 
         
            +
             
     | 
| 16 | 
         
            +
            * Go [here](https://github.com/sanderland/katrain/blob/master/katrain/i18n/locales/) and locate the `.po` file for your language. 
         
     | 
| 17 | 
         
            +
              * Alternatively, find the same file in the branch for the next version. 
         
     | 
| 18 | 
         
            +
            * Correct the relevant `msgstr` entries.
         
     | 
| 19 | 
         
            +
             
     | 
| 20 | 
         
            +
            ### Adding a translation
         
     | 
| 21 | 
         
            +
             
     | 
| 22 | 
         
            +
            Adding a translation requires making a new `.po` file with entries for that languages.
         
     | 
| 23 | 
         
            +
             
     | 
| 24 | 
         
            +
            * Copy the [English .po file](https://github.com/sanderland/katrain/blob/master/katrain/i18n/locales/en/LC_MESSAGES/katrain.po)
         
     | 
| 25 | 
         
            +
            * Change all the `msgstr` entries to your target language.
         
     | 
| 26 | 
         
            +
                * Note that anything between `{}` should be left as-is.
         
     | 
| 27 | 
         
            +
                * The information at the top of the file should also not be translated.
         
     | 
| 28 | 
         
            +
             
     | 
| 29 | 
         
            +
            You can send me the resulting `.po` file, and I will integrate it into the program.
         
     | 
| 30 | 
         
            +
             
     | 
| 31 | 
         
            +
            # Contributors 
         
     | 
| 32 | 
         
            +
             
     | 
| 33 | 
         
            +
            ## Primary author and project maintainer:
         
     | 
| 34 | 
         
            +
             
     | 
| 35 | 
         
            +
            [Sander Land](https://github.com/sanderland/)
         
     | 
| 36 | 
         
            +
             
     | 
| 37 | 
         
            +
            ## Contributors
         
     | 
| 38 | 
         
            +
             
     | 
| 39 | 
         
            +
            Many thanks to these additional authors:
         
     | 
| 40 | 
         
            +
             
     | 
| 41 | 
         
            +
            * Matthew Allred ("Kameone") for design of the v1.1 UI, macOS installation instructions, and working on promotion and YouTube videos.
         
     | 
| 42 | 
         
            +
            * "bale-go" for development and continued work on the 'calibrated rank' AI and rank estimation algorithm.
         
     | 
| 43 | 
         
            +
            * "Dontbtme" for detailed feedback and early testing of v1.0+.
         
     | 
| 44 | 
         
            +
            * "nowoowoo" for a fix to the parser for SGF files with extra line breaks.
         
     | 
| 45 | 
         
            +
            * "nimets123" for the timer sound effects and board/stone graphics.
         
     | 
| 46 | 
         
            +
            * Jordan Seaward for the stone sound effects.
         
     | 
| 47 | 
         
            +
            * "fohristiwhirl" for the Gibo and NGF formats parsing code.
         
     | 
| 48 | 
         
            +
            * "kaorahi" for bug fixes, SGF parser improvements, and tsumego frame code.
         
     | 
| 49 | 
         
            +
            * "ajkenny84" for the red-green colourblind theme.
         
     | 
| 50 | 
         
            +
            * Lukasz Wierzbowski for the ability to paste urls for sgfs and helping fix alt-gr issues.
         
     | 
| 51 | 
         
            +
            * Carton He for contributions to sgf parsing and handling.
         
     | 
| 52 | 
         
            +
            * "blamarche" for adding the board coordinates toggle.
         
     | 
| 53 | 
         
            +
            * "pdeblanc" for adding the ancient chinese scoring option, fixing a bug in query termination, and high precision score display.
         
     | 
| 54 | 
         
            +
            * "LiamHz" for adding the 'back to main branch' keyboard shortcut.
         
     | 
| 55 | 
         
            +
            * "xiaoyifang" for adding the reset analysis option, feature to save options on the loading screen, and scrolling through variations.
         
     | 
| 56 | 
         
            +
            * "electricRGB" for help with adding configurable keyboard shortcuts.
         
     | 
| 57 | 
         
            +
            * "milescrawford" for work on restyling the territory estimate.
         
     | 
| 58 | 
         
            +
            * "Funkenschlag1" for capturing stones sound and implementation, and board rotation.
         
     | 
| 59 | 
         
            +
            * "waltheri" for one of the wooden board textures.
         
     | 
| 60 | 
         
            +
            * Jacob Minsky ("jacobm-tech") for various contributions including analysis move range and improvements to territory display.
         
     | 
| 61 | 
         
            +
             
     | 
| 62 | 
         
            +
            ## Translators
         
     | 
| 63 | 
         
            +
             
     | 
| 64 | 
         
            +
            Many thanks to the following contributors for translations.
         
     | 
| 65 | 
         
            +
             
     | 
| 66 | 
         
            +
            * French: "Dontbtme" with contributions from "wonderingabout"
         
     | 
| 67 | 
         
            +
            * Korean: "isty2e"
         
     | 
| 68 | 
         
            +
            * German: "nimets123", "trohde", "Harleqin" and "Sovereign"
         
     | 
| 69 | 
         
            +
            * Spanish: Sergio Villegas ("serpiente") with contributions from the Spanish OGS community
         
     | 
| 70 | 
         
            +
            * Russian: Dmitry Ivankov and Alexander Kiselev
         
     | 
| 71 | 
         
            +
            * Simplified Chinese: Qing Mu with contributions from "Medwin" and Viktor Lin
         
     | 
| 72 | 
         
            +
            * Japanese: "kaorahi"
         
     | 
| 73 | 
         
            +
            * Traditional Chinese: "Tony-Liou" with contributions from Ching-yu Lin
         
     | 
| 74 | 
         
            +
             
     | 
| 75 | 
         
            +
            ## Additional thanks to
         
     | 
| 76 | 
         
            +
             
     | 
| 77 | 
         
            +
            * David Wu ("lightvector") for creating KataGo and providing assistance with making the most of KataGo's amazing capabilities.
         
     | 
| 78 | 
         
            +
            * "세븐틴" for including KaTrain in the Baduk Megapack and making explanatory YouTube videos in Korean.
         
     | 
| 79 | 
         
            +
             
     | 
    	
        katrain/ENGINE.md
    ADDED
    
    | 
         @@ -0,0 +1,74 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            # KataGo troubleshooting
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
            This page lists common ways in which the provided KataGo fails to work out of the box, and how to resolve these issues.
         
     | 
| 4 | 
         
            +
            If you find your problem is not in here, you can ask on the [Leela Zero & Friends Discord](http://discord.gg/AjTPFpN) (use the #gui channel),
         
     | 
| 5 | 
         
            +
             providing detailed information about your error.  
         
     | 
| 6 | 
         
            +
             
     | 
| 7 | 
         
            +
             
     | 
| 8 | 
         
            +
            * [General](#General)
         
     | 
| 9 | 
         
            +
                * [GPU vs CPU](#CPU)
         
     | 
| 10 | 
         
            +
            * [Windows specific help](#Windows)
         
     | 
| 11 | 
         
            +
            * [MacOS specific help](#Mac)
         
     | 
| 12 | 
         
            +
            * [Linux specific help](#Linux)
         
     | 
| 13 | 
         
            +
             
     | 
| 14 | 
         
            +
             
     | 
| 15 | 
         
            +
             
     | 
| 16 | 
         
            +
            ## <a name="General"></a> General
         
     | 
| 17 | 
         
            +
             
     | 
| 18 | 
         
            +
            ###  <a name="CPU"></a> GPU vs CPU
         
     | 
| 19 | 
         
            +
             
     | 
| 20 | 
         
            +
            The standard executables assume you have a compatible graphics card (GPU). 
         
     | 
| 21 | 
         
            +
            If you don't, KataGo will fail to start in ways that are difficult for KaTrain to pick up.  
         
     | 
| 22 | 
         
            +
             
     | 
| 23 | 
         
            +
            On Windows and Linux, you should be able to resolve this by:
         
     | 
| 24 | 
         
            +
             
     | 
| 25 | 
         
            +
            * Going to general and engine settings (F8)
         
     | 
| 26 | 
         
            +
            * Click 'download katago versions' and wait for downloads to finish.
         
     | 
| 27 | 
         
            +
            * Select a CPU based KataGo version (named 'Eigen' after the library it uses).
         
     | 
| 28 | 
         
            +
             
     | 
| 29 | 
         
            +
            Keep in mind that a CPU based engine can be significantly slower, and you may want to set your maximum number of
         
     | 
| 30 | 
         
            +
            visits to a lower number to compensate for this.
         
     | 
| 31 | 
         
            +
             
     | 
| 32 | 
         
            +
            ###  <a name="Models"></a> KataGo model versions
         
     | 
| 33 | 
         
            +
             
     | 
| 34 | 
         
            +
            KataGo models have changed over time, and selecting an older executable with a newer model can lead to errors.
         
     | 
| 35 | 
         
            +
            Of the provided binaries, this is typically the case for the 1.6.1 'bigger boards' binary, which should
         
     | 
| 36 | 
         
            +
             only be used with the standard 15/20/30/40 block models, and not the newer distributed training models.
         
     | 
| 37 | 
         
            +
             
     | 
| 38 | 
         
            +
             
     | 
| 39 | 
         
            +
            ## <a name="Mac"></a><img src="https://upload.wikimedia.org/wikipedia/commons/8/8a/Apple_Logo.svg" alt="macOs" height="35"/> For macOS users
         
     | 
| 40 | 
         
            +
             
     | 
| 41 | 
         
            +
            ### Running from source
         
     | 
| 42 | 
         
            +
             
     | 
| 43 | 
         
            +
            Make sure you `brew install katago` or set the engine path to your own KataGo binary, as there is no executable included.
         
     | 
| 44 | 
         
            +
             
     | 
| 45 | 
         
            +
            ### New Macs with M1 architecture
         
     | 
| 46 | 
         
            +
             
     | 
| 47 | 
         
            +
            Make sure you `brew install katago` as the provided executable does not work on rosetta.
         
     | 
| 48 | 
         
            +
             
     | 
| 49 | 
         
            +
            ### Getting more information about errors
         
     | 
| 50 | 
         
            +
             
     | 
| 51 | 
         
            +
            On macOS, the .app distributable will not show a console, so you will need install using `pip` to see the console window.
         
     | 
| 52 | 
         
            +
             
     | 
| 53 | 
         
            +
            ##  <a name="Windows"></a><img src="https://upload.wikimedia.org/wikipedia/commons/5/5f/Windows_logo_-_2012.svg" alt="Windows" height="35"/> For Windows users
         
     | 
| 54 | 
         
            +
             
     | 
| 55 | 
         
            +
            ### Getting more information about errors
         
     | 
| 56 | 
         
            +
             
     | 
| 57 | 
         
            +
            Run DebugKaTrain.exe, which is released in the .zip file distributable in releases. This will show a console window
         
     | 
| 58 | 
         
            +
             which typically tells you more.
         
     | 
| 59 | 
         
            +
             
     | 
| 60 | 
         
            +
             
     | 
| 61 | 
         
            +
            ## <a name="Linux"></a><img src="https://upload.wikimedia.org/wikipedia/commons/a/ab/Linux_Logo_in_Linux_Libertine_Font.svg" alt="Linux" height="35"/> For Linux users
         
     | 
| 62 | 
         
            +
             
     | 
| 63 | 
         
            +
            ### libzip compatibility
         
     | 
| 64 | 
         
            +
             
     | 
| 65 | 
         
            +
            The most common KataGo issue relates to incompatible library versions, leading to an "Error 127".
         
     | 
| 66 | 
         
            +
             
     | 
| 67 | 
         
            +
            * A good alternative is to go [here](https://github.com/lightvector/KataGo) and compile KataGo yourself.
         
     | 
| 68 | 
         
            +
            * Installing dependencies mentioned [here](INSTALL.md#LinuxTrouble) may also resolve certain issues with KataGo or the gui.
         
     | 
| 69 | 
         
            +
             
     | 
| 70 | 
         
            +
             
     | 
| 71 | 
         
            +
            ### Getting more information about errors
         
     | 
| 72 | 
         
            +
             
     | 
| 73 | 
         
            +
            * Check the terminal output around startup time.
         
     | 
| 74 | 
         
            +
            * Start KataGo by itself using `katrain/KataGo/katago` when running from source and check output.
         
     | 
    	
        katrain/INSTALL.md
    ADDED
    
    | 
         @@ -0,0 +1,177 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            # KaTrain Installation
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
            * [Quick install guide for MacOS](#MacQuick)
         
     | 
| 4 | 
         
            +
                * [Troubleshooting and installation from sources](#MacSources)
         
     | 
| 5 | 
         
            +
            * [Quick install guide for Windows](#WindowsQuick)
         
     | 
| 6 | 
         
            +
                * [Troubleshooting and installation from sources](#WindowsSources)
         
     | 
| 7 | 
         
            +
            * [Quick install guide for Linux](#LinuxQuick)
         
     | 
| 8 | 
         
            +
                * [Troubleshooting and installation from sources](#LinuxSources)
         
     | 
| 9 | 
         
            +
            * [Configuring Multiple GPUS](#GPU)
         
     | 
| 10 | 
         
            +
            * [Troubleshooting KataGo](#KataGo)
         
     | 
| 11 | 
         
            +
             
     | 
| 12 | 
         
            +
            ## <img src="https://upload.wikimedia.org/wikipedia/commons/8/8a/Apple_Logo.svg" alt="macOs" height="35"/> Installation for macOS users
         
     | 
| 13 | 
         
            +
             
     | 
| 14 | 
         
            +
            ### <a name="MacQuick"></a>Quick install guide
         
     | 
| 15 | 
         
            +
             
     | 
| 16 | 
         
            +
            The easiest way to install is probably [brew](https://brew.sh/). Simply run `brew install katrain` and it will download and install the latest pre-built .app, and also install katago if needed.
         
     | 
| 17 | 
         
            +
             
     | 
| 18 | 
         
            +
            You can also find downloadable .app files for macOS [here](https://github.com/sanderland/katrain/releases). 
         
     | 
| 19 | 
         
            +
            Simply download, unzip the file, mount the .dmg and drag the .app file to your application folder, everything is included.
         
     | 
| 20 | 
         
            +
            The first time launching the application you may need to [control-click in finder to give permission for the 'unidentified' app to launch](https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/mac). This is simply a result of Apple charging $99/year to developers to be 'identified'.
         
     | 
| 21 | 
         
            +
             
     | 
| 22 | 
         
            +
            Users with the last generation M1 macs with different architecture should then `brew install katago` in addition to this. KaTrain will automatically detect this KataGo binary.
         
     | 
| 23 | 
         
            +
             
     | 
| 24 | 
         
            +
            ### <a name="MacCommand"></a>Command line install guide
         
     | 
| 25 | 
         
            +
             
     | 
| 26 | 
         
            +
            [Open a terminal](https://support.apple.com/guide/terminal/open-or-quit-terminal-apd5265185d-f365-44cb-8b09-71a064a42125/mac) and enter the following commands:
         
     | 
| 27 | 
         
            +
            ```bash
         
     | 
| 28 | 
         
            +
            /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
         
     | 
| 29 | 
         
            +
            brew install python3
         
     | 
| 30 | 
         
            +
            brew install katago
         
     | 
| 31 | 
         
            +
            pip3 install katrain
         
     | 
| 32 | 
         
            +
            ```
         
     | 
| 33 | 
         
            +
             
     | 
| 34 | 
         
            +
            If you are using a M1 Mac, at the point of writing, the latest stable release of Kivy (2.0) does not support the new architecture, so we have to use a development snapshot and build it from source:
         
     | 
| 35 | 
         
            +
             
     | 
| 36 | 
         
            +
            ```bash
         
     | 
| 37 | 
         
            +
            /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
         
     | 
| 38 | 
         
            +
            brew install python3
         
     | 
| 39 | 
         
            +
            brew install katago
         
     | 
| 40 | 
         
            +
             
     | 
| 41 | 
         
            +
            # install dependencies: https://kivy.org/doc/stable/installation/installation-osx.html#install-source-osx
         
     | 
| 42 | 
         
            +
            brew install pkg-config sdl2 sdl2_image sdl2_ttf sdl2_mixer gstreamer ffmpeg
         
     | 
| 43 | 
         
            +
             
     | 
| 44 | 
         
            +
            # install Kivy from source: https://kivy.org/doc/stable/gettingstarted/installation.html#kivy-source-install
         
     | 
| 45 | 
         
            +
            pip3 install "kivy[base] @ https://github.com/kivy/kivy/archive/master.zip" --no-binary kivy
         
     | 
| 46 | 
         
            +
             
     | 
| 47 | 
         
            +
            pip3 install katrain
         
     | 
| 48 | 
         
            +
            ```
         
     | 
| 49 | 
         
            +
             
     | 
| 50 | 
         
            +
            Now you can start KaTrain by simply typing `katrain` in a terminal.
         
     | 
| 51 | 
         
            +
             
     | 
| 52 | 
         
            +
            These commands install [Homebrew](https://brew.sh), which simplifies installing packages,
         
     | 
| 53 | 
         
            +
             followed by the programming language Python, the KataGo AI, and KaTrain itself.
         
     | 
| 54 | 
         
            +
             
         
     | 
| 55 | 
         
            +
            To upgrade to a newer version, simply run `pip3 install -U katrain`
         
     | 
| 56 | 
         
            +
             
     | 
| 57 | 
         
            +
            ### <a name="MacSources"></a>Troubleshooting and Installation from sources
         
     | 
| 58 | 
         
            +
             
     | 
| 59 | 
         
            +
            Installation from sources is essentially the same as for Linux, see [here](#LinuxSources),
         
     | 
| 60 | 
         
            +
             note that you will still need to install your own KataGo, using brew or otherwise. 
         
     | 
| 61 | 
         
            +
             
     | 
| 62 | 
         
            +
            If you encounter SSL errors on downloading model files, you may need to follow [these](https://stackoverflow.com/questions/52805115/certificate-verify-failed-unable-to-get-local-issuer-certificate) instructions to fix your certificates.
         
     | 
| 63 | 
         
            +
             
     | 
| 64 | 
         
            +
            ## <img src="https://upload.wikimedia.org/wikipedia/commons/5/5f/Windows_logo_-_2012.svg" alt="Windows" height="35"/> Installation for Windows users
         
     | 
| 65 | 
         
            +
             
     | 
| 66 | 
         
            +
            ### <a name="WindowsQuick"></a>Quick install guide
         
     | 
| 67 | 
         
            +
             
     | 
| 68 | 
         
            +
            You can find downloadable .exe files for windows [here](https://github.com/sanderland/katrain/releases). 
         
     | 
| 69 | 
         
            +
            Simply download and run, everything is included.
         
     | 
| 70 | 
         
            +
             
     | 
| 71 | 
         
            +
            ### <a name="WindowsSources"></a>Installation from sources
         
     | 
| 72 | 
         
            +
             
     | 
| 73 | 
         
            +
            * Download the repository by clicking the green *Clone or download* on this page and *Download zip*. Extract the contents.
         
     | 
| 74 | 
         
            +
            * Make sure you have a python installation, I will assume Anaconda (Python 3.7/3.8), available [here](https://www.anaconda.com/products/individual#download-section).
         
     | 
| 75 | 
         
            +
            * Open 'Anaconda prompt' from the start menu and navigate to where you extracted the zip file using the `cd <folder>` command.
         
     | 
| 76 | 
         
            +
            * Execute the command `pip3 install .`
         
     | 
| 77 | 
         
            +
            * Start the app by running `katrain` in the command prompt. 
         
     | 
| 78 | 
         
            +
             
     | 
| 79 | 
         
            +
            ## <img src="https://upload.wikimedia.org/wikipedia/commons/a/ab/Linux_Logo_in_Linux_Libertine_Font.svg" alt="Linux" height="35"/> Installation for Linux users
         
     | 
| 80 | 
         
            +
             
     | 
| 81 | 
         
            +
            ### <a name="LinuxQuick"></a>Quick install guide
         
     | 
| 82 | 
         
            +
             
     | 
| 83 | 
         
            +
            If you have a working Python 3.6-3.8 available, you should be able to simply:
         
     | 
| 84 | 
         
            +
             
     | 
| 85 | 
         
            +
            * Run `pip3 install -U katrain` to install or upgrade.
         
     | 
| 86 | 
         
            +
            * Run the program by executing `katrain` in a terminal.
         
     | 
| 87 | 
         
            +
             
     | 
| 88 | 
         
            +
            ### <a name="LinuxSources"></a>Installation from sources 
         
     | 
| 89 | 
         
            +
             
     | 
| 90 | 
         
            +
            This section describes how to install KaTrain from sources,
         
     | 
| 91 | 
         
            +
             in case you want to run it in a local directory or have more control over the process. 
         
     | 
| 92 | 
         
            +
            It assumes you have a working Python 3.6+ installation.
         
     | 
| 93 | 
         
            +
             
     | 
| 94 | 
         
            +
            * Open a terminal.
         
     | 
| 95 | 
         
            +
            * Run the command `git clone https://github.com/sanderland/katrain.git` to download the repository and 
         
     | 
| 96 | 
         
            +
              change directory using `cd katrain`
         
     | 
| 97 | 
         
            +
            * Run the command `pip3 install .` to install the package globally, or use `--user` to install locally.
         
     | 
| 98 | 
         
            +
            * Run the program by typing `katrain` in the terminal.
         
     | 
| 99 | 
         
            +
                * If you prefer not to install, run without installing using `python3 -m katrain` after installing the 
         
     | 
| 100 | 
         
            +
                dependencies from `requirements.txt`.
         
     | 
| 101 | 
         
            +
             
     | 
| 102 | 
         
            +
            A binary for KataGo is included, but if you have compiled your own, press F8 to open general settings and change the 
         
     | 
| 103 | 
         
            +
             KataGo executable path to the relevant KataGo v1.4+ binary.
         
     | 
| 104 | 
         
            +
             
     | 
| 105 | 
         
            +
            ### <a name="LinuxTrouble"></a>Troubleshooting and advanced installation from sources
         
     | 
| 106 | 
         
            +
             
     | 
| 107 | 
         
            +
            You can try to manually install dependencies to resolve some issues relating to missing dependencies,
         
     | 
| 108 | 
         
            +
             e.g. the binary 'wheel' is not provided, KataGo is not starting, or sounds are not working.
         
     | 
| 109 | 
         
            +
            You can also follow these instructions if you don't want to install KaTrain, and just run it locally.
         
     | 
| 110 | 
         
            +
             
     | 
| 111 | 
         
            +
            First install the following packages, which are either required for building Kivy, 
         
     | 
| 112 | 
         
            +
             or may help resolve missing dependencies for Kivy or KataGo.
         
     | 
| 113 | 
         
            +
            ```bash
         
     | 
| 114 | 
         
            +
            sudo apt-get install python3-pip build-essential git python3 python3-dev ffmpeg libsdl2-dev libsdl2-image-dev\
         
     | 
| 115 | 
         
            +
                libsdl2-mixer-dev libsdl2-ttf-dev libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev zlib1g-dev\
         
     | 
| 116 | 
         
            +
                libgstreamer1.0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good libpulse\
         
     | 
| 117 | 
         
            +
                pkg-config libgl-dev opencl-headers ocl-icd-opencl-dev libzip-dev
         
     | 
| 118 | 
         
            +
            ```
         
     | 
| 119 | 
         
            +
            Then, try installing python package dependencies using:
         
     | 
| 120 | 
         
            +
            ```bash
         
     | 
| 121 | 
         
            +
            pip3 install -r requirements.txt
         
     | 
| 122 | 
         
            +
            pip3 install screeninfo # Skip on MacOS, not working
         
     | 
| 123 | 
         
            +
            ```
         
     | 
| 124 | 
         
            +
            In case the sound is not working, or there is no available wheel for your OS or Python version, try building kivy locally using:
         
     | 
| 125 | 
         
            +
            ```bash
         
     | 
| 126 | 
         
            +
            pip3 uninstall kivy
         
     | 
| 127 | 
         
            +
            pip3 install kivy --no-binary kivy
         
     | 
| 128 | 
         
            +
            ```
         
     | 
| 129 | 
         
            +
             
     | 
| 130 | 
         
            +
            You can now start KaTrain by running `python3 -m katrain`
         
     | 
| 131 | 
         
            +
             
     | 
| 132 | 
         
            +
            In case KataGo does not start, an alternative is to go [here](https://github.com/lightvector/KataGo) and compile KataGo yourself.
         
     | 
| 133 | 
         
            +
             
     | 
| 134 | 
         
            +
             
     | 
| 135 | 
         
            +
             
     | 
| 136 | 
         
            +
            ## <a name="GPU"></a> Configuring the GPU(s) KataGo uses
         
     | 
| 137 | 
         
            +
             
     | 
| 138 | 
         
            +
            In most cases KataGo detects your configuration correctly, automatically searching for OpenCL devices and select the highest scoring device. 
         
     | 
| 139 | 
         
            +
            However, if you have multiple GPUs or want to force a specific device you will need to edit the 'analysis_config.cfg' file in the KataGo folder.
         
     | 
| 140 | 
         
            +
             
     | 
| 141 | 
         
            +
            To see what devices are available and which one KataGo is using. Look for the following lines in the terminal after starting KaTrain:
         
     | 
| 142 | 
         
            +
            ```
         
     | 
| 143 | 
         
            +
                Found 3 device(s) on platform 0 with type CPU or GPU or Accelerator
         
     | 
| 144 | 
         
            +
                Found OpenCL Device 0: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz (Intel) (score 102)
         
     | 
| 145 | 
         
            +
                Found OpenCL Device 1: Intel(R) UHD Graphics 630 (Intel Inc.) (score 6000102)
         
     | 
| 146 | 
         
            +
                Found OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) (score 11000102)
         
     | 
| 147 | 
         
            +
                Using OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) OpenCL 1.2
         
     | 
| 148 | 
         
            +
            ```
         
     | 
| 149 | 
         
            +
             
     | 
| 150 | 
         
            +
            The above devices were found on a 2019 MacBook Pro with both an on-motherboard graphics chip, and a separate AMD Radeon Pro video card.
         
     | 
| 151 | 
         
            +
            As you can see it scores about twice as high as the Intel UHD chip and KataGo has selected
         
     | 
| 152 | 
         
            +
             it as it's sole device. You can configure KataGo to use *both* the AMD and the Intel devices to get the best performance out of the system.
         
     | 
| 153 | 
         
            +
             
     | 
| 154 | 
         
            +
            * Open the 'analysis_config.cfg' file in the `katrain/KataGo` folder in your python packages, or local sources.
         
     | 
| 155 | 
         
            +
              If you can't find it, turn on `debug_level=1` in general settings and look for the command that is used to start KataGo.
         
     | 
| 156 | 
         
            +
            * Search for `numNNServerThreadsPerModel` (~line 108), uncomment the line by deleting the # and set the value to 2. The line should read `numNNServerThreadsPerModel = 2`.
         
     | 
| 157 | 
         
            +
            * Search for `openclDeviceToUseThread` (~line 164), uncomment by deleting the # and set the values to the device ID numbers identified in the terminal.
         
     | 
| 158 | 
         
            +
              From the example above, we would want to use devices 1 and 2, for the Intel and AMD GPUs, but not device 0 (the CPU). In our case, the lines should read:
         
     | 
| 159 | 
         
            +
            ```
         
     | 
| 160 | 
         
            +
            openclDeviceToUseThread0 = 1
         
     | 
| 161 | 
         
            +
            openclDeviceToUseThread1 = 2
         
     | 
| 162 | 
         
            +
            ```
         
     | 
| 163 | 
         
            +
            * Run `katrain` and confirm that KataGo is now using both devices, by 
         
     | 
| 164 | 
         
            +
             checking the output from the terminal, which should indicate two devices being used. For example:
         
     | 
| 165 | 
         
            +
            ```
         
     | 
| 166 | 
         
            +
                Found 3 device(s) on platform 0 with type CPU or GPU or Accelerator
         
     | 
| 167 | 
         
            +
                Found OpenCL Device 0: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz (Intel) (score 102)
         
     | 
| 168 | 
         
            +
                Found OpenCL Device 1: Intel(R) UHD Graphics 630 (Intel Inc.) (score 6000102)
         
     | 
| 169 | 
         
            +
                Found OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) (score 11000102)
         
     | 
| 170 | 
         
            +
                Using OpenCL Device 1: Intel(R) UHD Graphics 630 (Intel Inc.) OpenCL 1.2
         
     | 
| 171 | 
         
            +
                Using OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) OpenCL 1.2
         
     | 
| 172 | 
         
            +
            ```
         
     | 
| 173 | 
         
            +
             
     | 
| 174 | 
         
            +
             
     | 
| 175 | 
         
            +
            ## <a name="KataGo"></a> Troubleshooting and advanced KataGo settings
         
     | 
| 176 | 
         
            +
             
     | 
| 177 | 
         
            +
            See [here](ENGINE.md) for an overview of how to resolve various issues with KataGo.
         
     | 
    	
        katrain/LICENSE
    ADDED
    
    | 
         @@ -0,0 +1,40 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            This repository includes:
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
            1. Binaries for 'KataGo', which is Copyright David J Wu et al.
         
     | 
| 4 | 
         
            +
            For on related licenses for these binaries and libraries see https://github.com/lightvector/KataGo
         
     | 
| 5 | 
         
            +
             
     | 
| 6 | 
         
            +
            2. Icons from www.flaticon.com, used with permission with the following attributions:
         
     | 
| 7 | 
         
            +
            - Equalize icon and Thrash Icon: derived from work by bqlqn from www.flaticon.com
         
     | 
| 8 | 
         
            +
            - Other Menu icons, Finish, Collaboration and Flag icons: derived from work by Freepik from www.flaticon.com
         
     | 
| 9 | 
         
            +
            - Collapse branch icon: derived from work by Kirill Kazachek from www.flaticon.com
         
     | 
| 10 | 
         
            +
            - Prune icon: derived from work by Pixelmeetup from www.flaticon.com
         
     | 
| 11 | 
         
            +
            - Reset icon: derived from work by Pixel Perfect from www.flaticon.com
         
     | 
| 12 | 
         
            +
            - Rotate icon:  derived from work by Frey Wazza from www.flaticon.com
         
     | 
| 13 | 
         
            +
             
     | 
| 14 | 
         
            +
            3. The True Type Font DIGITAL-7 version 1.02 by Sizenko Alexander, which is free for non-commercial use.
         
     | 
| 15 | 
         
            +
             
     | 
| 16 | 
         
            +
            4. The Noto Sans fonts from google which are covered by the SIL open font license v1.1 included in the katrain/fonts directory.
         
     | 
| 17 | 
         
            +
             
     | 
| 18 | 
         
            +
            -----------------------------------------------------------------------------------------
         
     | 
| 19 | 
         
            +
            Aside from the above, the license for all other content in this repository is as follows:
         
     | 
| 20 | 
         
            +
            -----------------------------------------------------------------------------------------
         
     | 
| 21 | 
         
            +
             
     | 
| 22 | 
         
            +
            Copyright 2020 Sander Land and/or other authors of the content in this repository.
         
     | 
| 23 | 
         
            +
            (See 'CONTRIBUTIONS.md' file for a list of authors as well as other indirect contributors).
         
     | 
| 24 | 
         
            +
             
     | 
| 25 | 
         
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
         
     | 
| 26 | 
         
            +
            associated documentation files (the "Software"), to deal in the Software without restriction,
         
     | 
| 27 | 
         
            +
            including without limitation the rights to use, copy, modify, merge, publish, distribute,
         
     | 
| 28 | 
         
            +
            sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
         
     | 
| 29 | 
         
            +
            furnished to do so, subject to the following conditions:
         
     | 
| 30 | 
         
            +
             
     | 
| 31 | 
         
            +
            The above copyright notice and this permission notice shall be included in all copies or
         
     | 
| 32 | 
         
            +
            substantial portions of the Software.
         
     | 
| 33 | 
         
            +
             
     | 
| 34 | 
         
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
         
     | 
| 35 | 
         
            +
            NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
         
     | 
| 36 | 
         
            +
            NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
         
     | 
| 37 | 
         
            +
            DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         
     | 
| 38 | 
         
            +
            OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         
     | 
| 39 | 
         
            +
             
     | 
| 40 | 
         
            +
            -----------------------------------------------------------------------------------------
         
     | 
    	
        katrain/README.md
    ADDED
    
    | 
         @@ -0,0 +1,253 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            # <a name="manual"></a> KaTrain
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
            [](http://github.com/sanderland/katrain/releases)
         
     | 
| 4 | 
         
            +
            [](http://en.wikipedia.org/wiki/MIT_License)
         
     | 
| 5 | 
         
            +
            [](http://github.com/sanderland/katrain/releases)
         
     | 
| 6 | 
         
            +
            [](http://pepy.tech/project/katrain)
         
     | 
| 7 | 
         
            +
            [](http://discord.com/channels/417022162348802048/629446365688365067)
         
     | 
| 8 | 
         
            +
             
     | 
| 9 | 
         
            +
            KaTrain is a tool for analyzing games and playing go with AI feedback from KataGo:
         
     | 
| 10 | 
         
            +
             
     | 
| 11 | 
         
            +
            * Review your games to find the moves that were most costly in terms of points lost.
         
     | 
| 12 | 
         
            +
            * Play against AI and get immediate feedback on mistakes with option to retry.
         
     | 
| 13 | 
         
            +
            * Play against a wide range of weakened versions of AI with various styles.
         
     | 
| 14 | 
         
            +
            * Automatically generate focused SGF reviews which show your biggest mistakes.
         
     | 
| 15 | 
         
            +
             
     | 
| 16 | 
         
            +
            ## Manual
         
     | 
| 17 | 
         
            +
             
     | 
| 18 | 
         
            +
            <table>
         
     | 
| 19 | 
         
            +
            <td>
         
     | 
| 20 | 
         
            +
             
     | 
| 21 | 
         
            +
            * [Previews and YouTube tutorials](#preview)
         
     | 
| 22 | 
         
            +
            * [Installation](#install)
         
     | 
| 23 | 
         
            +
            * [Manual](#ai)
         
     | 
| 24 | 
         
            +
                * [Configuring KataGo](#kata)
         
     | 
| 25 | 
         
            +
                * [Play against AI](#ai)
         
     | 
| 26 | 
         
            +
                * [Analyzing your Games](#analysis)
         
     | 
| 27 | 
         
            +
                * [Keyboard shortcuts](#keyboard)
         
     | 
| 28 | 
         
            +
                * [Distributed training](#distributed)
         
     | 
| 29 | 
         
            +
                * [Themes](#themes)
         
     | 
| 30 | 
         
            +
            * [FAQ and Troubleshooting](#faq)
         
     | 
| 31 | 
         
            +
            * [Contributing](#support)
         
     | 
| 32 | 
         
            +
             
     | 
| 33 | 
         
            +
             
     | 
| 34 | 
         
            +
            <td>
         
     | 
| 35 | 
         
            +
             
     | 
| 36 | 
         
            +
            <a href="http://github.com/sanderland/katrain/blob/master/README.md"><img alt="English" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-uk.png" width=50></a>
         
     | 
| 37 | 
         
            +
            <a href="http://translate.google.com/translate?sl=en&tl=de&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="German" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-de.png" width=50></a>
         
     | 
| 38 | 
         
            +
            <a href="http://translate.google.com/translate?sl=en&tl=fr&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="French" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-fr.png" width=50></a>
         
     | 
| 39 | 
         
            +
            <a href="http://translate.google.com/translate?sl=en&tl=ru&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Russian" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-ru.png" width=50></a>
         
     | 
| 40 | 
         
            +
            <a href="http://translate.google.com/translate?sl=en&tl=tr&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Turkish" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-tr.png" width=50></a>
         
     | 
| 41 | 
         
            +
            <br/>
         
     | 
| 42 | 
         
            +
             
     | 
| 43 | 
         
            +
            <a href="http://translate.google.com/translate?sl=en&tl=zh-CN&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Simplified Chinese" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-cn.png" width=50></a>
         
     | 
| 44 | 
         
            +
            <a href="http://translate.google.com/translate?sl=en&tl=zh-TW&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Traditional Chinese" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-tw.png" width=50></a>
         
     | 
| 45 | 
         
            +
            <a href="http://translate.google.com/translate?sl=en&tl=ko&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Korean" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-ko.png" width=50></a>
         
     | 
| 46 | 
         
            +
            <a href="http://translate.google.com/translate?sl=en&tl=ja&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Japanese" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-jp.png" width=50></a>
         
     | 
| 47 | 
         
            +
             
     | 
| 48 | 
         
            +
            </td>
         
     | 
| 49 | 
         
            +
            </table>
         
     | 
| 50 | 
         
            +
             
     | 
| 51 | 
         
            +
            ## <a name="preview"></a>  Preview and Youtube Videos
         
     | 
| 52 | 
         
            +
             
     | 
| 53 | 
         
            +
            <img alt="screenshot" src="https://raw.githubusercontent.com/sanderland/katrain/master/screenshots/analysis.png" width="550">
         
     | 
| 54 | 
         
            +
             
     | 
| 55 | 
         
            +
            | **Local Joseki Analysis**                  | **Analysis Tutorial**                                                                              | **Teaching Game Tutorial**                                                                                   |
         
     | 
| 56 | 
         
            +
            |:-----------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------:|
         
     | 
| 57 | 
         
            +
            | [](https://www.youtube.com/watch?v=tXniX57KtKk) | [](http://www.youtube.com/watch?v=qjxkcKgrsbU) | [](http://www.youtube.com/watch?v=wFl4Bab_eGM)   |
         
     | 
| 58 | 
         
            +
             
     | 
| 59 | 
         
            +
             
     | 
| 60 | 
         
            +
             
     | 
| 61 | 
         
            +
            ## <a name="install"></a> Installation
         
     | 
| 62 | 
         
            +
            * See the [releases page](http://github.com/sanderland/katrain/releases) for downloadable executables for Windows and macOS.
         
     | 
| 63 | 
         
            +
            * Alternatively use `pip3 install -U katrain` to install the latest version from PyPI on any 64-bit OS.
         
     | 
| 64 | 
         
            +
            * On macOS, you can also use `brew install katrain` to install the app.
         
     | 
| 65 | 
         
            +
            * [This page](https://github.com/sanderland/katrain/blob/master/INSTALL.md) has detailed instructions for Window, Linux and macOS,
         
     | 
| 66 | 
         
            +
              as well as troubleshooting and setting up KataGo to use multiple GPUs.
         
     | 
| 67 | 
         
            +
             
     | 
| 68 | 
         
            +
            ## <a name="kata"></a>  Configuring KataGo
         
     | 
| 69 | 
         
            +
             
     | 
| 70 | 
         
            +
            KaTrain comes pre-packaged with a working KataGo (OpenCL version) for Windows, Linux, and pre-M1 Mac operating systems, and the rather old 15 block model.
         
     | 
| 71 | 
         
            +
             
     | 
| 72 | 
         
            +
            To change the model, open 'General and Engine settings' in the application and 'Download models'. You can then select the model you want from the dropdown menu.
         
     | 
| 73 | 
         
            +
             
     | 
| 74 | 
         
            +
            To change the katago binary, e.g. to the Eigen/CPU version if you don't have a GPU, click 'Download KataGo versions'.
         
     | 
| 75 | 
         
            +
              You can then select the KataGo binary from the dropdown menu.
         
     | 
| 76 | 
         
            +
            There are also CUDA and TensorRT versions available on [the KataGo release site](https://github.com/lightvector/KataGo/releases). Particularly the latter may offer much better performance on NVIDIA GPUs, but will be harder to 
         
     | 
| 77 | 
         
            +
            set up: [see here for more details](https://github.com/lightvector/KataGo#opencl-vs-cuda-vs-tensorrt-vs-eigen).
         
     | 
| 78 | 
         
            +
             
     | 
| 79 | 
         
            +
            Finally, you can override the entire command used to start the analysis engine, which 
         
     | 
| 80 | 
         
            +
             can be useful for connecting to a remote server. Do keep in mind that KaTrain uses the *analysis engine*
         
     | 
| 81 | 
         
            +
             of KataGo, and not the GTP engine.
         
     | 
| 82 | 
         
            +
             
     | 
| 83 | 
         
            +
             
     | 
| 84 | 
         
            +
            ## <a name="ai"></a> Play against AI
         
     | 
| 85 | 
         
            +
             
     | 
| 86 | 
         
            +
            * Select the players in the main menu, or under 'New Game'.
         
     | 
| 87 | 
         
            +
            * In a teaching game, KaTrain will analyze your moves and automatically undo those that are sufficiently bad.
         
     | 
| 88 | 
         
            +
            * When playing against AI, note that the "Undo" button will undo both the AI's last move and yours.
         
     | 
| 89 | 
         
            +
             
     | 
| 90 | 
         
            +
            ### Instant feedback
         
     | 
| 91 | 
         
            +
             
     | 
| 92 | 
         
            +
            The dots on the move indicate how many points were lost by that move.
         
     | 
| 93 | 
         
            +
             
     | 
| 94 | 
         
            +
            * The colour indicates the size of the mistake according to KataGo
         
     | 
| 95 | 
         
            +
            * The size indicates if the mistake was actually punished. Going from fully punished at maximal size,
         
     | 
| 96 | 
         
            +
              to no actual effect on the score at minimal size.
         
     | 
| 97 | 
         
            +
             
     | 
| 98 | 
         
            +
            In short, if you are a weaker player you should mostly focus on large dots that are red or purple,
         
     | 
| 99 | 
         
            +
            while stronger players can pay more attention to smaller mistakes. If you want to hide some colours
         
     | 
| 100 | 
         
            +
            on the board, or not output details for them in SGFs,you can do so under 'Configure Teacher'.
         
     | 
| 101 | 
         
            +
             
     | 
| 102 | 
         
            +
            ### AIs
         
     | 
| 103 | 
         
            +
             
     | 
| 104 | 
         
            +
            This section describes the available AIs.
         
     | 
| 105 | 
         
            +
             
     | 
| 106 | 
         
            +
            In the 'AI settings', settings which have been tested and calibrated are at the top and have a lighter color,
         
     | 
| 107 | 
         
            +
            changing these will show an estimate of rank.
         
     | 
| 108 | 
         
            +
            This estimate should be reasonably accurate as long as you have not changed the other settings.
         
     | 
| 109 | 
         
            +
             
     | 
| 110 | 
         
            +
            * Recommended options for serious play include:
         
     | 
| 111 | 
         
            +
                * **KataGo** is full KataGo, above professional level. The analysis and feedback given is always based on this full strength KataGo AI.
         
     | 
| 112 | 
         
            +
                * **Calibrated Rank Bot** was calibrated on various bots (e.g. GnuGo and Pachi at different strength settings) to play a balanced
         
     | 
| 113 | 
         
            +
                 game from the opening to the endgame without making serious (DDK) blunders. Further discussion can be found
         
     | 
| 114 | 
         
            +
                  [here](http://github.com/sanderland/katrain/issues/44) and [here](http://github.com/sanderland/katrain/issues/74).
         
     | 
| 115 | 
         
            +
                * **Simple Style** Prefers moves that solidify both player's territory, leading to relatively simpler moves.
         
     | 
| 116 | 
         
            +
            * Legacy options which were developed earlier include: 
         
     | 
| 117 | 
         
            +
                * **ScoreLoss** is KataGo analyzing as usual, but
         
     | 
| 118 | 
         
            +
                  choosing from potential moves depending on the expected score loss, leading to a varied style with mostly small mistakes.
         
     | 
| 119 | 
         
            +
                * **Policy** uses the top move from the policy network (it's 'shape sense' without reading).
         
     | 
| 120 | 
         
            +
                * **Policy Weighted** picks a random move weighted by the policy, leading to a varied style with mostly small mistakes, and occasional blunders due to a lack of reading.
         
     | 
| 121 | 
         
            +
                * **Blinded Policy** picks a number of moves at random and play the best move among them, being effectively 'blind' to part of the board each turn. Calibrated rank is based on the same idea, and recommended over this option.
         
     | 
| 122 | 
         
            +
            * Options that are more on the 'fun and experimental' side include: 
         
     | 
| 123 | 
         
            +
                * Variants of **Blinded Policy**, which use the same basic strategy, but with a twist:
         
     | 
| 124 | 
         
            +
                   * **Local Style** will consider mostly moves close to the last move.
         
     | 
| 125 | 
         
            +
                   * **Tenuki Style** will consider mostly moves away from the last move.
         
     | 
| 126 | 
         
            +
                   * **Influential Style** will consider mostly 4th+ line moves, leading to a center-oriented style.
         
     | 
| 127 | 
         
            +
                   * **Territory Style** is biased in the opposite way, towards 1-3rd line moves.
         
     | 
| 128 | 
         
            +
                * **KataJigo** is KataGo attempting to win by 0.5 points, typically by responding to your mistakes with an immediate mistake of it's own.
         
     | 
| 129 | 
         
            +
                * **KataAntiMirror** is KataGo assuming you are playing mirror go and attempting to break out of it with profit as long as you are.
         
     | 
| 130 | 
         
            +
                
         
     | 
| 131 | 
         
            +
            The Engine based AIs (KataGo, ScoreLoss, KataJigo) are affected by both the model and choice of visits and maximum time,
         
     | 
| 132 | 
         
            +
             while the policy net based AIs are affected by the choice of model file, but work identically with 1 visit.
         
     | 
| 133 | 
         
            +
             
     | 
| 134 | 
         
            +
            Further technical details and discussion on some of these AIs can be found on [this](http://lifein19x19.com/viewtopic.php?f=10&t=17488&sid=b11e42c005bb6f4f48c83771e6a27eff) thread at the life in 19x19 forums.
         
     | 
| 135 | 
         
            +
             
     | 
| 136 | 
         
            +
            ## <a name="analysis"></a> Analysis
         
     | 
| 137 | 
         
            +
             
     | 
| 138 | 
         
            +
            Analysis options in KaTrain allow you to explore variations and request more in-depth analysis from the engine at any point in the game.
         
     | 
| 139 | 
         
            +
             
     | 
| 140 | 
         
            +
            Keyboard shortcuts are shown with **[key]**.
         
     | 
| 141 | 
         
            +
             
     | 
| 142 | 
         
            +
            * **[Tab]**: Switch between analysis and play modes.
         
     | 
| 143 | 
         
            +
              * AI moves, teaching mode and timers are suspended in analysis mode.
         
     | 
| 144 | 
         
            +
              * The state of the analysis options and right-hand side panels and options is saved independently for 'play' and 'analyze',
         
     | 
| 145 | 
         
            +
                allowing you to quickly switch between a more minimalistic 'play' mode and more complex 'analysis' mode.
         
     | 
| 146 | 
         
            +
             
     | 
| 147 | 
         
            +
            * The checkboxes at the top of the screen:
         
     | 
| 148 | 
         
            +
                * **[q]**: Child moves are shown. On by default, can turn it off to avoid obscuring other information or when 
         
     | 
| 149 | 
         
            +
                           wanting to guess the next move.
         
     | 
| 150 | 
         
            +
                * **[w]**: Show all dots: Toggles showing coloured evaluation 'dots' on the last few moves or not.
         
     | 
| 151 | 
         
            +
                   * You can configure the thresholds, along with how many of the last moves they are shown for under 'Teaching/Analysis Settings'.
         
     | 
| 152 | 
         
            +
                * **[e]**: Top moves: Show the next moves KataGo considered, colored by their expected point loss. 
         
     | 
| 153 | 
         
            +
                       Small/faint dots indicate high uncertainty and never show text (lower than your 'fast visits' setting). 
         
     | 
| 154 | 
         
            +
                       Hover over any of them to see the principal variation.
         
     | 
| 155 | 
         
            +
                * **[r]**: Policy moves: Show KataGo's policy network evaluation, i.e. where it thinks the best next move is purely from the position, 
         
     | 
| 156 | 
         
            +
                   and in the absence of any 'reading'. This turns off the 'top moves' setting as the overlap is often not useful.
         
     | 
| 157 | 
         
            +
                * **[t]**: Expected territory: Show expected ownership of each intersection.
         
     | 
| 158 | 
         
            +
             
     | 
| 159 | 
         
            +
            * The analysis options available under the 'Analysis' button are used for deeper evaluation of the position:
         
     | 
| 160 | 
         
            +
                * **[a]**: Deeper analysis: Re-evaluate the position using more visits, usually resulting in a more accurate evaluation.
         
     | 
| 161 | 
         
            +
                * **[s]**: Equalize visits: Re-evaluate all currently shown next moves with the same visits as the current top move. Useful to increase confidence in the suggestions with high uncertainty.
         
     | 
| 162 | 
         
            +
                * **[d]**: Analyze all moves: Evaluate all possible next moves. This can take a bit of time even though 'fast_visits' is used, but can be useful to see how many reasonable next moves are available.
         
     | 
| 163 | 
         
            +
                * **[f]**: Find alternatives: Increases analysis of current candidate moves to at least the 'fast visits' level, and request a new query that excludes all current candidate moves.    
         
     | 
| 164 | 
         
            +
                * **[g]**: Select area of interest: set an area and search only for moves in this box.
         
     | 
| 165 | 
         
            +
                           Good for solving tsumegos. Note that some results may appear outside the box due to establishing a baseline for the best move,
         
     | 
| 166 | 
         
            +
                           and the opponent can tenuki in variations.
         
     | 
| 167 | 
         
            +
                * **[h]**: Reset analysis. This reverts the analysis to what the engine returns after a normal query, removing any additional exploration.
         
     | 
| 168 | 
         
            +
                * **[i]**: Start insertion mode. Allows you to insert moves, to improve analysis when both players ignore an important exchange or life and death situation. Press again to stop inserting and copy the rest of the branch.
         
     | 
| 169 | 
         
            +
                * **[l]**: Play out the game until the end and add as a collapsed branch, to visualize the potential effect of mistakes. This is done in the background, and can be started at several nodes at once when comparing the results at different starting positions.      
         
     | 
| 170 | 
         
            +
                * **[spacebar]**: Turn continuous analysis on/off. This will continuously improve analysis of the current position, similar to Lizzie's 'pondering', but only when there are no other queries going on.
         
     | 
| 171 | 
         
            +
                  * **[shift+spacebar]**: As above, but does not turn 'top moves' hints on when it is off.
         
     | 
| 172 | 
         
            +
                * **[enter]** AI move. Makes the AI move for the current player regardless of current player selection.
         
     | 
| 173 | 
         
            +
                * **[F2]**: Deeper full game analysis. Analyze the entire game to a higher number of visits.
         
     | 
| 174 | 
         
            +
                * **[F3]**: Performance report. Show an overview of performance statistics for both players.  
         
     | 
| 175 | 
         
            +
                * **[F10]**: Tsumego Frame. After placing a life and death problem in a corner/side, use this to fill up the rest of the board to improve AI's ability in solving life and death problems.
         
     | 
| 176 | 
         
            +
                
         
     | 
| 177 | 
         
            +
              
         
     | 
| 178 | 
         
            +
            ## <a name="keyboard"></a> Keyboard and mouse shortcuts
         
     | 
| 179 | 
         
            +
             
     | 
| 180 | 
         
            +
            In addition to shortcuts mentioned above and those shown in the main menu:
         
     | 
| 181 | 
         
            +
             
     | 
| 182 | 
         
            +
            * **[Alt]**: Open the main menu.
         
     | 
| 183 | 
         
            +
            * **[~]** or **[ ` ]** or **[F12]**: Cycles through more minimalistic UI modes.
         
     | 
| 184 | 
         
            +
            * **[k]**: Toggle display of board coordinates.
         
     | 
| 185 | 
         
            +
            * **[p]**: Pass
         
     | 
| 186 | 
         
            +
            * **[pause]**: Pause/Resume timer
         
     | 
| 187 | 
         
            +
            * **[arrow left]** or **[z]**: Undo move. Hold shift for 10 moves at a time, or ctrl to skip to the start.
         
     | 
| 188 | 
         
            +
            * **[arrow right]** or **[x]**: Redo move. Hold shift for 10 moves at a time, or ctrl to skip to the end.
         
     | 
| 189 | 
         
            +
            * **[arrow up/down]** Switch branch, as would be expected from the move tree.
         
     | 
| 190 | 
         
            +
            * **[home/end]** Go to the beginning/end of the game.
         
     | 
| 191 | 
         
            +
            * **[pageup]** Make the currently selected node the main branch
         
     | 
| 192 | 
         
            +
            * **[Ctrl-delete]** Delete current node.
         
     | 
| 193 | 
         
            +
            * **[c]** Collapse/Uncollapse the branch from the current node to the previous branching point.
         
     | 
| 194 | 
         
            +
            * **[b]** Go back to the previous branching point.
         
     | 
| 195 | 
         
            +
            * **[Shift-b]** Go back the main branch.
         
     | 
| 196 | 
         
            +
            * **[n]** As in clicking the forward red arrow, go to one move before the next mistake (orange or worse) by a human player.
         
     | 
| 197 | 
         
            +
            * **[Shift-n]** As in clicking the backward red arrow, go to one move before the previous mistake.
         
     | 
| 198 | 
         
            +
            * **[scroll mouse]**:
         
     | 
| 199 | 
         
            +
              * When hovering the cursor over the right panel: Redo/Undo move.
         
     | 
| 200 | 
         
            +
              * When hovering over a candidate move: Scroll through principal variation.
         
     | 
| 201 | 
         
            +
            * **[middle/scroll wheel click]**: Add principal variation to the move tree. When scrolling, only moves up to the point you are viewing are added.
         
     | 
| 202 | 
         
            +
            * **[click on a move]**: See detailed statistics for a previous move, along with expected variation that was best instead of this move.
         
     | 
| 203 | 
         
            +
            * **[double-click on a move]**: Navigate directly to just before that point in the game.
         
     | 
| 204 | 
         
            +
            * **[Ctrl-V]**: Load SGF from the clipboard and do a 'fast' analysis of the game (with a high priority normal analysis for the last move).
         
     | 
| 205 | 
         
            +
            * **[Ctrl-C]**: Save SGF to clipboard.
         
     | 
| 206 | 
         
            +
            * **[Escape]**: Stop all analysis.
         
     | 
| 207 | 
         
            +
             
     | 
| 208 | 
         
            +
            ## <a name="distributed"></a>  Contributing to distributed training
         
     | 
| 209 | 
         
            +
            Starting in December 2020, KataGo started [distributed training](https://katagotraining.org/).
         
     | 
| 210 | 
         
            +
            This allows people to all help generate self-play games to increase KataGo's strength and train bigger models. 
         
     | 
| 211 | 
         
            +
             
     | 
| 212 | 
         
            +
            KaTrain 1.8.0+ makes it easy to contribute to distributed training: simply select the option from the main menu, register an account, and click run.
         
     | 
| 213 | 
         
            +
            During this mode you can do little more than watch games.
         
     | 
| 214 | 
         
            +
             
     | 
| 215 | 
         
            +
            Keep in mind that partial games are not uploaded,
         
     | 
| 216 | 
         
            +
             so it is best to plan to keep it running for at least an hour, if not several, for the most effective contribution.  
         
     | 
| 217 | 
         
            +
             
     | 
| 218 | 
         
            +
            A few keyboard shortcuts have special functions in this mode:
         
     | 
| 219 | 
         
            +
             
     | 
| 220 | 
         
            +
            * **[Spacebar]** Switch between manually navigating the current game, and automatically advancing it.
         
     | 
| 221 | 
         
            +
            * **[Escape]**: This sends the `quit` command to KataGo, which starts a slow shutdown, finishing partial games but not starting new ones. Only works on v1.11+.
         
     | 
| 222 | 
         
            +
            * **[Pause]**: Pauses/resumes contributions via the `pause` and `resume` commands introduced in KataGo v1.11. 
         
     | 
| 223 | 
         
            +
             
     | 
| 224 | 
         
            +
             
     | 
| 225 | 
         
            +
            ## <a name="themes"></a> Themes
         
     | 
| 226 | 
         
            +
             
     | 
| 227 | 
         
            +
            See [these instructions](THEMES.md) for how to modify the look of any graphics or colours, and creating or install themes.
         
     | 
| 228 | 
         
            +
               
         
     | 
| 229 | 
         
            +
            ## <a name="faq"></a> FAQ
         
     | 
| 230 | 
         
            +
             
     | 
| 231 | 
         
            +
            * The program is running too slowly. How can I speed it up?
         
     | 
| 232 | 
         
            +
              *  Adjust the number of visits or maximum time allowed in the settings.
         
     | 
| 233 | 
         
            +
            * KataGo crashes with "out of memory" errors, how can I prevent this?
         
     | 
| 234 | 
         
            +
              * Try using a lower number for `nnMaxBatchSize` in `KataGo/analysis_config.cfg`, and avoid using versions compiled with large board sizes.
         
     | 
| 235 | 
         
            +
              * If still encountering problems, please start KataGo by itself to check for any errors it gives.
         
     | 
| 236 | 
         
            +
              * Note that if you don't have a GPU, or your GPU does not support OpenCL, you should use the 'eigen' binaries which run on CPU only.
         
     | 
| 237 | 
         
            +
            * The font size is too small
         
     | 
| 238 | 
         
            +
              * On some ultra-high resolution monitors, dialogs and other elements with text can appear too small. Please see [these](https://github.com/sanderland/katrain/issues/359#issuecomment-784096271) instructions to adjust them.
         
     | 
| 239 | 
         
            +
            * The app crashes with an error about "unable to find any valuable cutbuffer provider"
         
     | 
| 240 | 
         
            +
              * Install xclip using `sudo apt-get install xclip`
         
     | 
| 241 | 
         
            +
             
     | 
| 242 | 
         
            +
             
     | 
| 243 | 
         
            +
            ## <a name="support"></a> Support / Contribute
         
     | 
| 244 | 
         
            +
             
     | 
| 245 | 
         
            +
            [](http://github.com/sanderland/katrain/issues)
         
     | 
| 246 | 
         
            +
            [](CONTRIBUTIONS.md)
         
     | 
| 247 | 
         
            +
             
     | 
| 248 | 
         
            +
             * Ideas, feedback, and contributions to code or translations are all very welcome.
         
     | 
| 249 | 
         
            +
                * For suggestions and planned improvements, see [open issues](http://github.com/sanderland/katrain/issues) on github to check if the functionality is already planned.
         
     | 
| 250 | 
         
            +
            * You can join the [Computer Go Community Discord (formerly Leela Zero & Friends)](http://discord.gg/AjTPFpN) (use the #gui channel) to get help, discuss improvements, or simply show your appreciation. Please do not use github issues to ask for technical help, this is only for bugs, suggestions and discussing contributions.
         
     | 
| 251 | 
         
            +
             
     | 
| 252 | 
         
            +
             
     | 
| 253 | 
         
            +
             
     | 
    	
        katrain/THEMES.md
    ADDED
    
    | 
         @@ -0,0 +1,86 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            # Themes
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
            Version 1.7 brings basic support for themes, and 1.9 extends it to include keyboard shortcuts and support for multiple theme files.
         
     | 
| 4 | 
         
            +
             
     | 
| 5 | 
         
            +
            ## Creating and editing themes
         
     | 
| 6 | 
         
            +
             
     | 
| 7 | 
         
            +
            * Look at the `Theme` class in [`katrain/gui/theme.py`](https://github.com/sanderland/katrain/blob/master/katrain/gui/theme.py).
         
     | 
| 8 | 
         
            +
            * Make a `theme<yourthemename>.json` file in your `<home dir>/.katrain` directory and specify any variables from the above class you want to override, e.g. 
         
     | 
| 9 | 
         
            +
             ```json
         
     | 
| 10 | 
         
            +
             {
         
     | 
| 11 | 
         
            +
              "BACKGROUND_COLOR": [1,0,0,1],
         
     | 
| 12 | 
         
            +
              "KEY_STOP_ANALYSIS": "f10",
         
     | 
| 13 | 
         
            +
              "MISTAKE_SOUNDS": ["jeff.wav","what.wav"]
         
     | 
| 14 | 
         
            +
            }
         
     | 
| 15 | 
         
            +
              ```
         
     | 
| 16 | 
         
            +
            * All resources (including icons, which can not be renamed for now) will be looked up in `<home dir>/.katrain` first, so files with identical names there can be used to override sounds and images.
         
     | 
| 17 | 
         
            +
            * If variables are specified in multiple theme files, the *latest* alphabetically takes precedence. That is, each later theme file overwrites the settings from any previous one.
         
     | 
| 18 | 
         
            +
             
     | 
| 19 | 
         
            +
            ## Expected territory options
         
     | 
| 20 | 
         
            +
             
     | 
| 21 | 
         
            +
            * KaTrain supports different styles of display of expected territory:
         
     | 
| 22 | 
         
            +
              * Blended style colors the board with an intensity proportional to the likelihood of a player controlling that territory at the end of the game.
         
     | 
| 23 | 
         
            +
              * Shaded style behaves the same as Blended, but uses square shades similar to
         
     | 
| 24 | 
         
            +
              the Katago paper.
         
     | 
| 25 | 
         
            +
              * In the Marks style, each point of the board is marked with a square of size which is proportional to ownership likelihood.
         
     | 
| 26 | 
         
            +
              * The Blocks style divides the whole board into black, white, and neutral territory, based on a likelihood threshold. This style is appropriate as a counting aid, but may be misleading before endgame if much of the territory is unsettled.
         
     | 
| 27 | 
         
            +
            * Marks can also appear on stones to indicate the likelihood of these stones living at the end of the game. Three styles are supported:
         
     | 
| 28 | 
         
            +
              * All stones can be marked, with the color of the mark indicating the expected ownership and the size of the mark indicating certainty.
         
     | 
| 29 | 
         
            +
              * Weak stones only - marks will appear only on stones which are over 50% likely to die before the end of the game.
         
     | 
| 30 | 
         
            +
              * No stone marks.
         
     | 
| 31 | 
         
            +
            * Stones can also be made transparent based on their strength.
         
     | 
| 32 | 
         
            +
             
     | 
| 33 | 
         
            +
            | <img src="./themes/blended-all.png"  width="400"/> <br> Blended style, all stones marked| <img src="./themes/shaded-all.png"   width="400"/> <br> Shaded style, all stones marked |
         
     | 
| 34 | 
         
            +
            | --- | ---|
         
     | 
| 35 | 
         
            +
            | <img src="./themes/blocks-none.png"  width="400"/> <br> Territory blocks, no stones marked | <img src="./themes/blended-weak.png" width="400"/> <br> Blended territory, weak stones marked |
         
     | 
| 36 | 
         
            +
            | <img src="./themes/marks-weak.png"   width="400"/>  <br> Marks on intersections, weak stones marked | <img src="./themes/shaded-no-alpha.png"   width="400"/>  <br> Shaded, no stone alpha |
         
     | 
| 37 | 
         
            +
                                         
         
     | 
| 38 | 
         
            +
             
     | 
| 39 | 
         
            +
            <sup>The game used in the screenshots is [Albert Yen vs. Eric Yoder](https://www.usgo.org/news/2022/03/members-edition-midwest-open-round-2-the-broken-ladder-game).</sup>
         
     | 
| 40 | 
         
            +
             
     | 
| 41 | 
         
            +
            The stone marks, transparency, and territory style are independent; the table above presents a collection of possible variants.
         
     | 
| 42 | 
         
            +
            The relevant variables are:
         
     | 
| 43 | 
         
            +
            ```
         
     | 
| 44 | 
         
            +
                TERRITORY_DISPLAY = "blended" | "shaded" | "marks" | "blocks"
         
     | 
| 45 | 
         
            +
                STONE_MARKS = "all" | "weak" | "none"
         
     | 
| 46 | 
         
            +
                OWNERSHIP_COLORS = {"B": [0.0, 0.0, 0.10, 0.75], "W": [0.92, 0.92, 1.0, 0.800]}
         
     | 
| 47 | 
         
            +
                BLOCKS_THRESHOLD = 0.6
         
     | 
| 48 | 
         
            +
                MARK_SIZE = 0.42  # as fraction of stone size
         
     | 
| 49 | 
         
            +
                STONE_MIN_ALPHA = 0.5
         
     | 
| 50 | 
         
            +
            ```
         
     | 
| 51 | 
         
            +
             
     | 
| 52 | 
         
            +
            The colors are specified as RGB values and a maximum alpha transparency.
         
     | 
| 53 | 
         
            +
             
     | 
| 54 | 
         
            +
            ## Installation
         
     | 
| 55 | 
         
            +
             
     | 
| 56 | 
         
            +
            * To install a theme, simply unzip the theme.zip to your .katrain folder. 
         
     | 
| 57 | 
         
            +
              * On Windows you can find it in C:\Users\you\\.katrain and on linux in ~/.katrain.
         
     | 
| 58 | 
         
            +
              * When in doubt, the general settings dialog will also show the location.
         
     | 
| 59 | 
         
            +
            * To uninstall a theme, remove theme.json and all relevant images from that folder.
         
     | 
| 60 | 
         
            +
             
     | 
| 61 | 
         
            +
            ## Available themes
         
     | 
| 62 | 
         
            +
             
     | 
| 63 | 
         
            +
            ### Alternate board/stones theme by "koast"
         
     | 
| 64 | 
         
            +
             
     | 
| 65 | 
         
            +
            [Download](https://github.com/sanderland/katrain/blob/master/themes/koast-theme.zip)
         
     | 
| 66 | 
         
            +
             
     | 
| 67 | 
         
            +
            
         
     | 
| 68 | 
         
            +
             
     | 
| 69 | 
         
            +
            ### Lizzie-like theme
         
     | 
| 70 | 
         
            +
             
     | 
| 71 | 
         
            +
            * Theme created by Eric W, includes modified board, stones
         
     | 
| 72 | 
         
            +
            * Images taken from [Lizzie](https://github.com/featurecat/lizzie/) by featurecat and contributors.
         
     | 
| 73 | 
         
            +
            * Hides hints for low visit/uncertain moves instead of showing small dots. 
         
     | 
| 74 | 
         
            +
             
     | 
| 75 | 
         
            +
            [Download](https://github.com/sanderland/katrain/blob/master/themes/eric-lizzie-look.zip)
         
     | 
| 76 | 
         
            +
             
     | 
| 77 | 
         
            +
            
         
     | 
| 78 | 
         
            +
             
     | 
| 79 | 
         
            +
             
     | 
| 80 | 
         
            +
            ### Jeff sounds
         
     | 
| 81 | 
         
            +
             
     | 
| 82 | 
         
            +
            * This theme makes Jeff comment `Ahhh?` and `What?!` when you make mistakes.
         
     | 
| 83 | 
         
            +
            * Sounds provided by Mikkgo.
         
     | 
| 84 | 
         
            +
             
     | 
| 85 | 
         
            +
            [Download](https://github.com/sanderland/katrain/blob/master/themes/jeff-sounds.zip)
         
     | 
| 86 | 
         
            +
             
     | 
    	
        katrain/__main__.spec
    ADDED
    
    | 
         @@ -0,0 +1,38 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            # -*- mode: python ; coding: utf-8 -*-
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
             
     | 
| 4 | 
         
            +
            a = Analysis(
         
     | 
| 5 | 
         
            +
                ['katrain\\__main__.py'],
         
     | 
| 6 | 
         
            +
                pathex=[],
         
     | 
| 7 | 
         
            +
                binaries=[],
         
     | 
| 8 | 
         
            +
                datas=[],
         
     | 
| 9 | 
         
            +
                hiddenimports=[],
         
     | 
| 10 | 
         
            +
                hookspath=[],
         
     | 
| 11 | 
         
            +
                hooksconfig={},
         
     | 
| 12 | 
         
            +
                runtime_hooks=[],
         
     | 
| 13 | 
         
            +
                excludes=[],
         
     | 
| 14 | 
         
            +
                noarchive=False,
         
     | 
| 15 | 
         
            +
                optimize=0,
         
     | 
| 16 | 
         
            +
            )
         
     | 
| 17 | 
         
            +
            pyz = PYZ(a.pure)
         
     | 
| 18 | 
         
            +
             
     | 
| 19 | 
         
            +
            exe = EXE(
         
     | 
| 20 | 
         
            +
                pyz,
         
     | 
| 21 | 
         
            +
                a.scripts,
         
     | 
| 22 | 
         
            +
                a.binaries,
         
     | 
| 23 | 
         
            +
                a.datas,
         
     | 
| 24 | 
         
            +
                [],
         
     | 
| 25 | 
         
            +
                name='__main__',
         
     | 
| 26 | 
         
            +
                debug=False,
         
     | 
| 27 | 
         
            +
                bootloader_ignore_signals=False,
         
     | 
| 28 | 
         
            +
                strip=False,
         
     | 
| 29 | 
         
            +
                upx=True,
         
     | 
| 30 | 
         
            +
                upx_exclude=[],
         
     | 
| 31 | 
         
            +
                runtime_tmpdir=None,
         
     | 
| 32 | 
         
            +
                console=False,
         
     | 
| 33 | 
         
            +
                disable_windowed_traceback=False,
         
     | 
| 34 | 
         
            +
                argv_emulation=False,
         
     | 
| 35 | 
         
            +
                target_arch=None,
         
     | 
| 36 | 
         
            +
                codesign_identity=None,
         
     | 
| 37 | 
         
            +
                entitlements_file=None,
         
     | 
| 38 | 
         
            +
            )
         
     | 
    	
        katrain/__pycache__/board_ai.cpython-310.pyc
    ADDED
    
    | 
         Binary file (9.52 kB). View file 
     | 
| 
         | 
    	
        katrain/__pycache__/engine_ai.cpython-310.pyc
    ADDED
    
    | 
         Binary file (23.9 kB). View file 
     | 
| 
         | 
    	
        katrain/__pycache__/hongik_ai.cpython-310.pyc
    ADDED
    
    | 
         Binary file (13.9 kB). View file 
     | 
| 
         | 
    	
        katrain/fonts/Roboto-Black.ttf
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            version https://git-lfs.github.com/spec/v1
         
     | 
| 2 | 
         
            +
            oid sha256:b6a38ddfb6b7d92a644da3a175cab3858438b3c791486aeeca2094a611430f27
         
     | 
| 3 | 
         
            +
            size 142472
         
     | 
    	
        katrain/fonts/Roboto-BlackItalic.ttf
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            version https://git-lfs.github.com/spec/v1
         
     | 
| 2 | 
         
            +
            oid sha256:e57070129b2845c7684675491c305fc9cd75d801a2812deb154f1077016cea54
         
     | 
| 3 | 
         
            +
            size 149644
         
     | 
    	
        katrain/fonts/Roboto-Bold.ttf
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            version https://git-lfs.github.com/spec/v1
         
     | 
| 2 | 
         
            +
            oid sha256:9287925cae90ac480804094ff0876832065e2db116470da1f524d79ed9c18b70
         
     | 
| 3 | 
         
            +
            size 135820
         
     | 
    	
        katrain/fonts/Roboto-BoldItalic.ttf
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            version https://git-lfs.github.com/spec/v1
         
     | 
| 2 | 
         
            +
            oid sha256:2d998c92d5478dafabe3902ec6521b7ca6a2d7dca9251607553962538ec22947
         
     | 
| 3 | 
         
            +
            size 144700
         
     | 
    	
        katrain/fonts/Roboto-Italic.ttf
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            version https://git-lfs.github.com/spec/v1
         
     | 
| 2 | 
         
            +
            oid sha256:d24529b2a332a23bc226a43a15f8c185c5af52502cca5e9dee7f9896bf7cd383
         
     | 
| 3 | 
         
            +
            size 148540
         
     | 
    	
        katrain/fonts/Roboto-Light.ttf
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            version https://git-lfs.github.com/spec/v1
         
     | 
| 2 | 
         
            +
            oid sha256:b17667ce7e13581db105777f986e141168231e88a8ef16d13e581c7c1525f14b
         
     | 
| 3 | 
         
            +
            size 140276
         
     | 
    	
        katrain/fonts/Roboto-LightItalic.ttf
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            version https://git-lfs.github.com/spec/v1
         
     | 
| 2 | 
         
            +
            oid sha256:4cadcfdd708e1aee7625c1e66cb80d2e44ba61e2e54d76bc60935fcfc1e5ed88
         
     | 
| 3 | 
         
            +
            size 145932
         
     | 
    	
        katrain/fonts/Roboto-Medium.ttf
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            version https://git-lfs.github.com/spec/v1
         
     | 
| 2 | 
         
            +
            oid sha256:d0c8f44a774b8490ceee29889cdabc72381fa35fb621619a78fd28211d90241c
         
     | 
| 3 | 
         
            +
            size 137308
         
     | 
    	
        katrain/fonts/Roboto-MediumItalic.ttf
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            version https://git-lfs.github.com/spec/v1
         
     | 
| 2 | 
         
            +
            oid sha256:fd3c714c2e39b1a5dbff6eb24157adfa3f277fa5293cafbf1a0074ad54b094d4
         
     | 
| 3 | 
         
            +
            size 147876
         
     | 
    	
        katrain/fonts/Roboto-Regular.ttf
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            version https://git-lfs.github.com/spec/v1
         
     | 
| 2 | 
         
            +
            oid sha256:dbd285b518e398832f6f4a736109c355ce25a49546bfce41bab256c9ef7e56eb
         
     | 
| 3 | 
         
            +
            size 146004
         
     | 
    	
        katrain/fonts/Roboto-Thin.ttf
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            version https://git-lfs.github.com/spec/v1
         
     | 
| 2 | 
         
            +
            oid sha256:11dbd8bf4f8c61d665f4f3157027b9643db2454d5d84daffbe6385d70e8bf131
         
     | 
| 3 | 
         
            +
            size 130044
         
     | 
    	
        katrain/fonts/Roboto-ThinItalic.ttf
    ADDED
    
    | 
         @@ -0,0 +1,3 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            version https://git-lfs.github.com/spec/v1
         
     | 
| 2 | 
         
            +
            oid sha256:e7f436499c79fa18381468afe4b80690a59c0bd635e72f63190023d11bf17a1d
         
     | 
| 3 | 
         
            +
            size 132376
         
     | 
    	
        katrain/i18n.py
    ADDED
    
    | 
         @@ -0,0 +1,125 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import copy
         
     | 
| 2 | 
         
            +
            import glob
         
     | 
| 3 | 
         
            +
            import os
         
     | 
| 4 | 
         
            +
            import re
         
     | 
| 5 | 
         
            +
            import sys
         
     | 
| 6 | 
         
            +
            from collections import defaultdict
         
     | 
| 7 | 
         
            +
             
     | 
| 8 | 
         
            +
            import polib
         
     | 
| 9 | 
         
            +
             
     | 
| 10 | 
         
            +
            localedir = "katrain/i18n/locales"
         
     | 
| 11 | 
         
            +
            locales = set(os.listdir(localedir))
         
     | 
| 12 | 
         
            +
            print("locales found:", locales)
         
     | 
| 13 | 
         
            +
             
     | 
| 14 | 
         
            +
            strings_to_langs = defaultdict(dict)
         
     | 
| 15 | 
         
            +
            strings_to_keys = defaultdict(dict)
         
     | 
| 16 | 
         
            +
            lang_to_strings = defaultdict(set)
         
     | 
| 17 | 
         
            +
             
     | 
| 18 | 
         
            +
            DEFAULT_LANG = "en"
         
     | 
| 19 | 
         
            +
            INACTIVE_LANGS = ["es"]
         
     | 
| 20 | 
         
            +
            errors = False
         
     | 
| 21 | 
         
            +
             
     | 
| 22 | 
         
            +
            po = {}
         
     | 
| 23 | 
         
            +
            pofile = {}
         
     | 
| 24 | 
         
            +
            todos = defaultdict(list)
         
     | 
| 25 | 
         
            +
             
     | 
| 26 | 
         
            +
            for lang in locales:
         
     | 
| 27 | 
         
            +
                if lang in INACTIVE_LANGS:
         
     | 
| 28 | 
         
            +
                    continue
         
     | 
| 29 | 
         
            +
                pofile[lang] = os.path.join(localedir, lang, "LC_MESSAGES", "katrain.po")
         
     | 
| 30 | 
         
            +
                po[lang] = polib.pofile(pofile[lang])
         
     | 
| 31 | 
         
            +
                for entry in po[lang].translated_entries():
         
     | 
| 32 | 
         
            +
                    if "TODO" in entry.comment and "DEPRECATED" not in entry.comment:
         
     | 
| 33 | 
         
            +
                        todos[lang].append(entry)
         
     | 
| 34 | 
         
            +
                    strings_to_langs[entry.msgid][lang] = entry
         
     | 
| 35 | 
         
            +
                    strings_to_keys[entry.msgid][lang] = set(re.findall("{.*?}", entry.msgstr))
         
     | 
| 36 | 
         
            +
                    if entry.msgid in lang_to_strings[lang]:
         
     | 
| 37 | 
         
            +
                        print("duplicate", entry.msgid, "in", lang, "--> deleting", entry.msgstr)
         
     | 
| 38 | 
         
            +
                        errors = True
         
     | 
| 39 | 
         
            +
                        po[lang].remove(entry)
         
     | 
| 40 | 
         
            +
                    else:
         
     | 
| 41 | 
         
            +
                        lang_to_strings[lang].add(entry.msgid)
         
     | 
| 42 | 
         
            +
                if todos[lang] and any("todo" in a for a in sys.argv):
         
     | 
| 43 | 
         
            +
                    print(f"========== {lang} has {len(todos[lang])} TODO entries ========== ")
         
     | 
| 44 | 
         
            +
                    for item in todos[lang]:
         
     | 
| 45 | 
         
            +
                        print(item)
         
     | 
| 46 | 
         
            +
             
     | 
| 47 | 
         
            +
             
     | 
| 48 | 
         
            +
            for lang in locales:
         
     | 
| 49 | 
         
            +
                if lang in INACTIVE_LANGS:
         
     | 
| 50 | 
         
            +
                    continue
         
     | 
| 51 | 
         
            +
                if lang != DEFAULT_LANG:
         
     | 
| 52 | 
         
            +
                    for msgid in lang_to_strings[lang]:
         
     | 
| 53 | 
         
            +
                        if (
         
     | 
| 54 | 
         
            +
                            DEFAULT_LANG in strings_to_keys[msgid]
         
     | 
| 55 | 
         
            +
                            and strings_to_keys[msgid][lang] != strings_to_keys[msgid][DEFAULT_LANG]
         
     | 
| 56 | 
         
            +
                        ):
         
     | 
| 57 | 
         
            +
                            print(
         
     | 
| 58 | 
         
            +
                                f"{msgid} has inconstent formatting keys for {lang}: ",
         
     | 
| 59 | 
         
            +
                                strings_to_keys[msgid][lang],
         
     | 
| 60 | 
         
            +
                                "is different from default",
         
     | 
| 61 | 
         
            +
                                strings_to_keys[msgid][DEFAULT_LANG],
         
     | 
| 62 | 
         
            +
                            )
         
     | 
| 63 | 
         
            +
                            errors = True
         
     | 
| 64 | 
         
            +
             
     | 
| 65 | 
         
            +
                for msgid in strings_to_langs.keys() - lang_to_strings[lang]:
         
     | 
| 66 | 
         
            +
                    if lang == DEFAULT_LANG:
         
     | 
| 67 | 
         
            +
                        print("Message id", msgid, "found as ", strings_to_langs[msgid], "but missing in default", DEFAULT_LANG)
         
     | 
| 68 | 
         
            +
                        errors = True
         
     | 
| 69 | 
         
            +
                    elif DEFAULT_LANG in strings_to_langs[msgid]:
         
     | 
| 70 | 
         
            +
                        copied_entry = copy.copy(strings_to_langs[msgid][DEFAULT_LANG])
         
     | 
| 71 | 
         
            +
                        print("Message id", msgid, "missing in ", lang, "-> Adding it from", DEFAULT_LANG)
         
     | 
| 72 | 
         
            +
                        if copied_entry.comment:
         
     | 
| 73 | 
         
            +
                            copied_entry.comment = f"TODO - {copied_entry.comment}"
         
     | 
| 74 | 
         
            +
                        else:
         
     | 
| 75 | 
         
            +
                            copied_entry.comment = "TODO"
         
     | 
| 76 | 
         
            +
                        po[lang].append(copied_entry)
         
     | 
| 77 | 
         
            +
                        errors = True
         
     | 
| 78 | 
         
            +
                    else:
         
     | 
| 79 | 
         
            +
                        print(f"MISSING IN DEFAULT AND {lang}", msgid)
         
     | 
| 80 | 
         
            +
                        errors = True
         
     | 
| 81 | 
         
            +
             
     | 
| 82 | 
         
            +
                for msgid, lang_entries in strings_to_langs.items():
         
     | 
| 83 | 
         
            +
                    if lang in lang_entries and "TODO" in lang_entries[lang].comment:
         
     | 
| 84 | 
         
            +
                        if any(e.msgstr == lang_entries[lang].msgstr for ll, e in lang_entries.items() if ll != lang):
         
     | 
| 85 | 
         
            +
                            if lang_entries.get(DEFAULT_LANG):
         
     | 
| 86 | 
         
            +
                                todo_comment = (
         
     | 
| 87 | 
         
            +
                                    f"TODO - {lang_entries[DEFAULT_LANG].comment}" if lang_entries[DEFAULT_LANG].comment else "TODO"
         
     | 
| 88 | 
         
            +
                                )  # update todo
         
     | 
| 89 | 
         
            +
                                if (
         
     | 
| 90 | 
         
            +
                                    lang_entries[lang].msgstr != lang_entries[DEFAULT_LANG].msgstr
         
     | 
| 91 | 
         
            +
                                    or lang_entries[lang].comment.replace("\n", " ") != todo_comment
         
     | 
| 92 | 
         
            +
                                ):
         
     | 
| 93 | 
         
            +
                                    print(
         
     | 
| 94 | 
         
            +
                                        [
         
     | 
| 95 | 
         
            +
                                            lang_entries[lang].msgstr,
         
     | 
| 96 | 
         
            +
                                            lang_entries[DEFAULT_LANG].msgstr,
         
     | 
| 97 | 
         
            +
                                            lang_entries[lang].comment,
         
     | 
| 98 | 
         
            +
                                            todo_comment,
         
     | 
| 99 | 
         
            +
                                        ]
         
     | 
| 100 | 
         
            +
                                    )
         
     | 
| 101 | 
         
            +
                                    lang_entries[lang].msgstr = lang_entries[DEFAULT_LANG].msgstr  # update
         
     | 
| 102 | 
         
            +
                                    lang_entries[lang].comment = todo_comment
         
     | 
| 103 | 
         
            +
                                    print(f"{lang}/{msgid} todo entry updated")
         
     | 
| 104 | 
         
            +
             
     | 
| 105 | 
         
            +
                po[lang].save(pofile[lang])
         
     | 
| 106 | 
         
            +
                mofile = pofile[lang].replace(".po", ".mo")
         
     | 
| 107 | 
         
            +
                po[lang].save_as_mofile(mofile)
         
     | 
| 108 | 
         
            +
                print("Fixed", pofile[lang], "and converted ->", mofile)
         
     | 
| 109 | 
         
            +
             
     | 
| 110 | 
         
            +
             
     | 
| 111 | 
         
            +
            for ext in ["py", "kv"]:
         
     | 
| 112 | 
         
            +
                lc = 0
         
     | 
| 113 | 
         
            +
                for file in glob.glob(f"katrain/*.{ext}") + glob.glob(f"katrain/**/*.{ext}"):
         
     | 
| 114 | 
         
            +
                    with open(file, "r") as f:
         
     | 
| 115 | 
         
            +
                        for i, line in enumerate(f.readlines()):
         
     | 
| 116 | 
         
            +
                            if line.strip():
         
     | 
| 117 | 
         
            +
                                lc += 1
         
     | 
| 118 | 
         
            +
                            matches = [m.strip() for m in re.findall(r"i18n._\((.*?)\)", line)]
         
     | 
| 119 | 
         
            +
                            for msgid in matches:
         
     | 
| 120 | 
         
            +
                                stripped_msgid = msgid.strip("\"'")
         
     | 
| 121 | 
         
            +
                                if stripped_msgid and msgid[0] in ['"', "'"] and stripped_msgid not in strings_to_langs:  # not code
         
     | 
| 122 | 
         
            +
                                    print(f"Missing {msgid} used in code at \t{file}:{i} \t'{line.strip()}'")
         
     | 
| 123 | 
         
            +
                                    errors += 1
         
     | 
| 124 | 
         
            +
                print(f"Checked {lc} lines of {ext} code for missing i18n entries.")
         
     | 
| 125 | 
         
            +
            sys.exit(int(errors))
         
     | 
    	
        katrain/katrain.py
    ADDED
    
    | 
         @@ -0,0 +1,4 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            # for backward compatibility
         
     | 
| 2 | 
         
            +
            from katrain.__main__ import run_app
         
     | 
| 3 | 
         
            +
             
     | 
| 4 | 
         
            +
            run_app()
         
     | 
    	
        katrain/katrain/__init__.py
    ADDED
    
    | 
         
            File without changes
         
     | 
    	
        katrain/katrain/__main__.py
    ADDED
    
    | 
         @@ -0,0 +1,455 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            # Main entry point for the HongikAI-KaTrain application.
         
     | 
| 2 | 
         
            +
            # It initializes the Kivy/KaTrain GUI and integrates the custom HongikAIEngine.
         
     | 
| 3 | 
         
            +
            #
         
     | 
| 4 | 
         
            +
            # Author: Gemini 2.5 Pro, Gemini 2.5 Flash
         
     | 
| 5 | 
         
            +
             
     | 
| 6 | 
         
            +
            import os
         
     | 
| 7 | 
         
            +
            import sys
         
     | 
| 8 | 
         
            +
            import signal
         
     | 
| 9 | 
         
            +
            import threading
         
     | 
| 10 | 
         
            +
            import time
         
     | 
| 11 | 
         
            +
            import traceback
         
     | 
| 12 | 
         
            +
            import random
         
     | 
| 13 | 
         
            +
            from queue import Queue
         
     | 
| 14 | 
         
            +
            import json
         
     | 
| 15 | 
         
            +
             
     | 
| 16 | 
         
            +
            project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
         
     | 
| 17 | 
         
            +
             
     | 
| 18 | 
         
            +
            if project_root not in sys.path:
         
     | 
| 19 | 
         
            +
                sys.path.insert(0, project_root)
         
     | 
| 20 | 
         
            +
             
     | 
| 21 | 
         
            +
            os.environ["KCFG_KIVY_LOG_LEVEL"] = os.environ.get("KCFG_KIVY_LOG_LEVEL", "warning")
         
     | 
| 22 | 
         
            +
            import kivy
         
     | 
| 23 | 
         
            +
            kivy.require("2.0.0")
         
     | 
| 24 | 
         
            +
            from kivy.app import App
         
     | 
| 25 | 
         
            +
            from kivy.base import ExceptionHandler, ExceptionManager
         
     | 
| 26 | 
         
            +
            from kivy.lang import Builder
         
     | 
| 27 | 
         
            +
            from kivy.resources import resource_add_path
         
     | 
| 28 | 
         
            +
            from kivy.uix.screenmanager import Screen
         
     | 
| 29 | 
         
            +
            from kivy.core.window import Window
         
     | 
| 30 | 
         
            +
            from kivy.properties import ObjectProperty, StringProperty, NumericProperty, BooleanProperty
         
     | 
| 31 | 
         
            +
            from kivy.clock import Clock
         
     | 
| 32 | 
         
            +
            from kivy.config import Config
         
     | 
| 33 | 
         
            +
            from kivymd.app import MDApp
         
     | 
| 34 | 
         
            +
             
     | 
| 35 | 
         
            +
            from katrain.core.utils import find_package_resource, PATHS
         
     | 
| 36 | 
         
            +
            from katrain.core.base_katrain import KaTrainBase
         
     | 
| 37 | 
         
            +
            from katrain.core.lang import DEFAULT_LANGUAGE, i18n
         
     | 
| 38 | 
         
            +
            from katrain.core.constants import *
         
     | 
| 39 | 
         
            +
            from katrain.core.game import Game, KaTrainSGF, IllegalMoveException
         
     | 
| 40 | 
         
            +
            from katrain.core.sgf_parser import Move, ParseError
         
     | 
| 41 | 
         
            +
            from katrain.gui.theme import Theme
         
     | 
| 42 | 
         
            +
             
     | 
| 43 | 
         
            +
            import pygame
         
     | 
| 44 | 
         
            +
             
     | 
| 45 | 
         
            +
            from hongik.board_ai import Board
         
     | 
| 46 | 
         
            +
            from hongik.engine_ai import HongikAIEngine
         
     | 
| 47 | 
         
            +
             
     | 
| 48 | 
         
            +
            from katrain.gui.kivyutils import *
         
     | 
| 49 | 
         
            +
            from katrain.gui.widgets import MoveTree
         
     | 
| 50 | 
         
            +
            from katrain.gui.badukpan import BadukPanWidget
         
     | 
| 51 | 
         
            +
            from katrain.gui.controlspanel import ControlsPanel
         
     | 
| 52 | 
         
            +
             
     | 
| 53 | 
         
            +
            if 'USER' not in PATHS:
         
     | 
| 54 | 
         
            +
                USER_DATA_PATH = os.path.expanduser(os.path.join("~", ".katrain"))
         
     | 
| 55 | 
         
            +
                os.makedirs(USER_DATA_PATH, exist_ok=True)
         
     | 
| 56 | 
         
            +
                PATHS['USER'] = USER_DATA_PATH
         
     | 
| 57 | 
         
            +
             
     | 
| 58 | 
         
            +
            ICON = find_package_resource("katrain/img/icon.ico")
         
     | 
| 59 | 
         
            +
            Config.set("kivy", "window_icon", ICON)
         
     | 
| 60 | 
         
            +
            Config.set("input", "mouse", "mouse,multitouch_on_demand")
         
     | 
| 61 | 
         
            +
            SOUNDS_DIR = find_package_resource("katrain/sounds")
         
     | 
| 62 | 
         
            +
             
     | 
| 63 | 
         
            +
            class KaTrainGui(Screen, KaTrainBase):
         
     | 
| 64 | 
         
            +
                """
         
     | 
| 65 | 
         
            +
                The main GUI class for the application. It inherits from Kivy's Screen and
         
     | 
| 66 | 
         
            +
                KaTrainBase, managing all visual components and user interactions.
         
     | 
| 67 | 
         
            +
                """
         
     | 
| 68 | 
         
            +
                zen = NumericProperty(0)
         
     | 
| 69 | 
         
            +
                controls = ObjectProperty(None); engine = ObjectProperty(None); game = ObjectProperty(None)
         
     | 
| 70 | 
         
            +
                board_gui = ObjectProperty(None); board_controls = ObjectProperty(None); play_mode = ObjectProperty(None)
         
     | 
| 71 | 
         
            +
                show_move_numbers = BooleanProperty(False)
         
     | 
| 72 | 
         
            +
                analysis_controls = ObjectProperty(None) 
         
     | 
| 73 | 
         
            +
             
     | 
| 74 | 
         
            +
                @property
         
     | 
| 75 | 
         
            +
                def play_analyze_mode(self):
         
     | 
| 76 | 
         
            +
                    return self.play_mode.mode
         
     | 
| 77 | 
         
            +
                
         
     | 
| 78 | 
         
            +
                def __init__(self, **kwargs):
         
     | 
| 79 | 
         
            +
                    """Initializes the GUI, linking it to the main app and setting up necessary variables."""
         
     | 
| 80 | 
         
            +
                    self.katrain_app = kwargs.get('katrain_app')
         
     | 
| 81 | 
         
            +
                    self.engine, self.message_queue, self.pondering = None, Queue(), False
         
     | 
| 82 | 
         
            +
                    self.contributing, self.animate_contributing = False, False
         
     | 
| 83 | 
         
            +
                    super().__init__(**kwargs)
         
     | 
| 84 | 
         
            +
             
     | 
| 85 | 
         
            +
                def config_set(self, section, option, value):
         
     | 
| 86 | 
         
            +
                    """Sets a configuration value and writes it to the config file."""
         
     | 
| 87 | 
         
            +
                    self.katrain_app.config.set(section, option, value)
         
     | 
| 88 | 
         
            +
                    self.katrain_app.config.write()
         
     | 
| 89 | 
         
            +
             
     | 
| 90 | 
         
            +
                def save_config(self, sections=None):
         
     | 
| 91 | 
         
            +
                    """Writes the current configuration to disk."""
         
     | 
| 92 | 
         
            +
                    self.katrain_app.config.write()
         
     | 
| 93 | 
         
            +
                    
         
     | 
| 94 | 
         
            +
                def play_sound(self):
         
     | 
| 95 | 
         
            +
                    """Randomly plays a stone placement sound from the sounds directory."""
         
     | 
| 96 | 
         
            +
                    try:
         
     | 
| 97 | 
         
            +
                        sound_files = [f for f in os.listdir(SOUNDS_DIR) if f.startswith('stone') and f.endswith(('.wav', '.ogg'))]
         
     | 
| 98 | 
         
            +
                        if sound_files:
         
     | 
| 99 | 
         
            +
                            sound_to_play = random.choice(sound_files)
         
     | 
| 100 | 
         
            +
                            pygame.mixer.Sound(os.path.join(SOUNDS_DIR, sound_to_play)).play()
         
     | 
| 101 | 
         
            +
                    except pygame.error as e:
         
     | 
| 102 | 
         
            +
                        print(f"Pygame sound playback error: {e}")
         
     | 
| 103 | 
         
            +
             
     | 
| 104 | 
         
            +
                def start(self):
         
     | 
| 105 | 
         
            +
                    """
         
     | 
| 106 | 
         
            +
                    Starts the main application logic, initializes the AI engine, starts the
         
     | 
| 107 | 
         
            +
                    message loop, and creates a new game.
         
     | 
| 108 | 
         
            +
                    """
         
     | 
| 109 | 
         
            +
                    if self.engine: return
         
     | 
| 110 | 
         
            +
                    self.board_gui.trainer_config = self.config("trainer")
         
     | 
| 111 | 
         
            +
                    self.engine = HongikAIEngine(self, self.config("engine"))
         
     | 
| 112 | 
         
            +
                    threading.Thread(target=self._message_loop_thread, daemon=True).start()
         
     | 
| 113 | 
         
            +
                    self._do_new_game()
         
     | 
| 114 | 
         
            +
                    Clock.schedule_interval(self.handle_animations, 0.1)
         
     | 
| 115 | 
         
            +
                    Window.request_keyboard(None, self, "").bind(on_key_down=self._on_keyboard_down)
         
     | 
| 116 | 
         
            +
                    
         
     | 
| 117 | 
         
            +
                def update_player(self, bw, **kwargs):
         
     | 
| 118 | 
         
            +
                    """Updates the information and type for a given player (Black or White)."""
         
     | 
| 119 | 
         
            +
                    player_type = kwargs.get('player_type')
         
     | 
| 120 | 
         
            +
                    if player_type == PLAYER_AI:
         
     | 
| 121 | 
         
            +
                        self.players_info[bw].player_type, self.players_info[bw].player_subtype = PLAYER_AI, "홍익 AI"
         
     | 
| 122 | 
         
            +
                        self.players_info[bw].sgf_rank = ""
         
     | 
| 123 | 
         
            +
                        self.players_info[bw].calculated_rank = ""
         
     | 
| 124 | 
         
            +
                    elif player_type == PLAYER_HUMAN:
         
     | 
| 125 | 
         
            +
                        self.players_info[bw].player_type, self.players_info[bw].player_subtype = PLAYER_HUMAN, "Human"
         
     | 
| 126 | 
         
            +
             
     | 
| 127 | 
         
            +
                    self.players_info[bw].periods_used = 0
         
     | 
| 128 | 
         
            +
                    self.players_info[bw].being_taught = False
         
     | 
| 129 | 
         
            +
                    self.players_info[bw].player = bw 
         
     | 
| 130 | 
         
            +
                    if self.game: self.players_info[bw].name = self.game.root.get_property("P" + bw)
         
     | 
| 131 | 
         
            +
                    if self.controls: self.controls.update_players(); self.update_state()
         
     | 
| 132 | 
         
            +
             
     | 
| 133 | 
         
            +
                def update_gui(self, cn, redraw_board=False):
         
     | 
| 134 | 
         
            +
                    """Updates all GUI elements with the latest game state information."""
         
     | 
| 135 | 
         
            +
                    if not self.game: return
         
     | 
| 136 | 
         
            +
                    prisoners = self.game.prisoner_count
         
     | 
| 137 | 
         
            +
                    self.controls.players["B"].captures, self.controls.players["W"].captures = prisoners.get("W", 0), prisoners.get("B", 0)
         
     | 
| 138 | 
         
            +
                    if not self.engine or not self.engine.is_idle(): self.board_controls.engine_status_col = Theme.ENGINE_BUSY_COLOR
         
     | 
| 139 | 
         
            +
                    else: self.board_controls.engine_status_col = Theme.ENGINE_READY_COLOR
         
     | 
| 140 | 
         
            +
                    if redraw_board: self.board_gui.draw_board()
         
     | 
| 141 | 
         
            +
                    self.board_gui.redraw_board_contents_trigger()
         
     | 
| 142 | 
         
            +
                    self.controls.update_evaluation(); self.controls.move_tree.current_node = self.game.current_node
         
     | 
| 143 | 
         
            +
             
     | 
| 144 | 
         
            +
                def update_state(self, redraw_board=False):
         
     | 
| 145 | 
         
            +
                    """A shortcut to send an 'update-state' message to the message queue."""
         
     | 
| 146 | 
         
            +
                    self("update-state", redraw_board=redraw_board)
         
     | 
| 147 | 
         
            +
             
     | 
| 148 | 
         
            +
                def _message_loop_thread(self):
         
     | 
| 149 | 
         
            +
                    """
         
     | 
| 150 | 
         
            +
                    The main message loop that runs in a separate thread, processing commands
         
     | 
| 151 | 
         
            +
                    from the message queue to avoid blocking the GUI.
         
     | 
| 152 | 
         
            +
                    """
         
     | 
| 153 | 
         
            +
                    while True:
         
     | 
| 154 | 
         
            +
                        game_id, msg, args, kwargs = self.message_queue.get()
         
     | 
| 155 | 
         
            +
                        try:
         
     | 
| 156 | 
         
            +
                            if self.game and game_id != self.game.game_id: continue
         
     | 
| 157 | 
         
            +
                            fn = getattr(self, f"_do_{msg.replace('-', '_')}")
         
     | 
| 158 | 
         
            +
                            fn(*args, **kwargs)
         
     | 
| 159 | 
         
            +
                            if msg != "update_state": self._do_update_state()
         
     | 
| 160 | 
         
            +
                        except Exception as exc:
         
     | 
| 161 | 
         
            +
                            self.log(f"Message loop exception: {exc}", OUTPUT_ERROR); traceback.print_exc()
         
     | 
| 162 | 
         
            +
             
     | 
| 163 | 
         
            +
                def __call__(self, message, *args, **kwargs):
         
     | 
| 164 | 
         
            +
                    """Adds a message to the thread-safe message queue for processing."""
         
     | 
| 165 | 
         
            +
                    if message.endswith("popup"): Clock.schedule_once(lambda _dt: getattr(self, f"_do_{message.replace('-', '_')}")(*args, **kwargs), -1)
         
     | 
| 166 | 
         
            +
                    else: self.message_queue.put([self.game.game_id if self.game else None, message, args, kwargs])
         
     | 
| 167 | 
         
            +
             
     | 
| 168 | 
         
            +
                def _do_update_state(self, redraw_board=False):
         
     | 
| 169 | 
         
            +
                    """
         
     | 
| 170 | 
         
            +
                    Handles the 'update-state' message, refreshing the GUI to reflect the
         
     | 
| 171 | 
         
            +
                    current game state, player turn, and engine status.
         
     | 
| 172 | 
         
            +
                    """
         
     | 
| 173 | 
         
            +
                    if not self.game or not self.game.current_node: return
         
     | 
| 174 | 
         
            +
             
     | 
| 175 | 
         
            +
                    if self.controls:
         
     | 
| 176 | 
         
            +
                        self.controls.update_players()
         
     | 
| 177 | 
         
            +
                        next_player_is = self.game.current_node.next_player
         
     | 
| 178 | 
         
            +
                        self.controls.active_player = self.game.current_node.next_player
         
     | 
| 179 | 
         
            +
                     
         
     | 
| 180 | 
         
            +
                        self.controls.players['B'].active = (next_player_is == 'B')
         
     | 
| 181 | 
         
            +
                        self.controls.players['W'].active = (next_player_is == 'W')
         
     | 
| 182 | 
         
            +
                    
         
     | 
| 183 | 
         
            +
                    is_game_active = self.game and not self.game.end_result      
         
     | 
| 184 | 
         
            +
                    is_game_over = not is_game_active
         
     | 
| 185 | 
         
            +
             
     | 
| 186 | 
         
            +
                    if self.board_gui.game_is_over != is_game_over:
         
     | 
| 187 | 
         
            +
                        self.board_gui.game_is_over = is_game_over
         
     | 
| 188 | 
         
            +
                        if is_game_over:
         
     | 
| 189 | 
         
            +
                            self.board_gui.game_over_message = "Game Over"
         
     | 
| 190 | 
         
            +
                    
         
     | 
| 191 | 
         
            +
                    is_ai_vs_ai = (self.players_info['B'].player_type == PLAYER_AI and self.players_info['W'].player_type == PLAYER_AI)
         
     | 
| 192 | 
         
            +
                    
         
     | 
| 193 | 
         
            +
                    if self.controls and self.controls.ids.get('undo_button'):
         
     | 
| 194 | 
         
            +
                        self.controls.ids.undo_button.disabled = not is_game_active or is_ai_vs_ai
         
     | 
| 195 | 
         
            +
                        self.controls.ids.resign_button.disabled = not is_game_active or is_ai_vs_ai
         
     | 
| 196 | 
         
            +
                    
         
     | 
| 197 | 
         
            +
                    if self.engine and self.pondering:
         
     | 
| 198 | 
         
            +
                        self.game.analyze_extra("ponder")
         
     | 
| 199 | 
         
            +
                    else:
         
     | 
| 200 | 
         
            +
                        self.engine.stop_pondering()
         
     | 
| 201 | 
         
            +
                        
         
     | 
| 202 | 
         
            +
                    Clock.schedule_once(lambda _dt: self.update_gui(self.game.current_node, redraw_board), -1)
         
     | 
| 203 | 
         
            +
                    self.engine._game_turn()
         
     | 
| 204 | 
         
            +
             
     | 
| 205 | 
         
            +
                def _do_play(self, coords):
         
     | 
| 206 | 
         
            +
                    """Handles a 'play' event, creating a Move object and playing it on the board."""
         
     | 
| 207 | 
         
            +
                    try:
         
     | 
| 208 | 
         
            +
                        move = Move(coords, player=self.game.current_node.next_player)
         
     | 
| 209 | 
         
            +
                        self.game.play(move)
         
     | 
| 210 | 
         
            +
                        self.update_state()
         
     | 
| 211 | 
         
            +
                        if not move.is_pass: self.play_sound()
         
     | 
| 212 | 
         
            +
                    except IllegalMoveException as e:
         
     | 
| 213 | 
         
            +
                        self.controls.set_status(f"Illegal move: {str(e)}", STATUS_ERROR)
         
     | 
| 214 | 
         
            +
             
     | 
| 215 | 
         
            +
                def _do_new_game(self, player_types=(PLAYER_HUMAN, PLAYER_HUMAN), move_tree=None, sgf_filename=None):
         
     | 
| 216 | 
         
            +
                    """Handles a 'new-game' event, setting up a new game with specified players."""
         
     | 
| 217 | 
         
            +
                    self.pondering = False
         
     | 
| 218 | 
         
            +
                    self.engine.sound_index = False
         
     | 
| 219 | 
         
            +
                    if self.engine: self.engine.stop_self_play_loop(); self.engine.on_new_game()
         
     | 
| 220 | 
         
            +
                    self.game = Game(self, self.engine, move_tree=move_tree, sgf_filename=sgf_filename)
         
     | 
| 221 | 
         
            +
                   
         
     | 
| 222 | 
         
            +
                    self.board_controls.ids.game_mode_reset_btn.state = 'down'
         
     | 
| 223 | 
         
            +
                    self.update_player('B', player_type=player_types[0])
         
     | 
| 224 | 
         
            +
                    self.update_player('W', player_type=player_types[1])
         
     | 
| 225 | 
         
            +
                    if self.controls and self.controls.graph:
         
     | 
| 226 | 
         
            +
                        self.controls.graph.initialize_from_game(self.game.root)
         
     | 
| 227 | 
         
            +
                    self.update_state(redraw_board=True)
         
     | 
| 228 | 
         
            +
             
     | 
| 229 | 
         
            +
                    try:
         
     | 
| 230 | 
         
            +
                        self.analysis_controls.hamburger.disabled = False 
         
     | 
| 231 | 
         
            +
                        self.analysis_controls.show_children.disabled =False
         
     | 
| 232 | 
         
            +
                        self.analysis_controls.hints.disabled =False
         
     | 
| 233 | 
         
            +
                        self.analysis_controls.policy.disabled =False            
         
     | 
| 234 | 
         
            +
                        self.controls.ids.undo.disabled = False
         
     | 
| 235 | 
         
            +
                        self.board_controls.ids.pass_btn.disabled = False
         
     | 
| 236 | 
         
            +
                        self.controls.ids.timer.ids.pause.disabled = False
         
     | 
| 237 | 
         
            +
                    except Exception as e:
         
     | 
| 238 | 
         
            +
                        self.log(f"Error enabling button: {e}", OUTPUT_ERROR)
         
     | 
| 239 | 
         
            +
             
     | 
| 240 | 
         
            +
                
         
     | 
| 241 | 
         
            +
                def _do_start_hongik_selfplay(self):
         
     | 
| 242 | 
         
            +
                    """Starts a new self-play game between two Hongik AI instances."""
         
     | 
| 243 | 
         
            +
                    self._do_new_game(player_types=(PLAYER_AI, PLAYER_AI))
         
     | 
| 244 | 
         
            +
                    self.engine.start_self_play_loop()
         
     | 
| 245 | 
         
            +
             
     | 
| 246 | 
         
            +
                    try:
         
     | 
| 247 | 
         
            +
                        self.analysis_controls.hamburger.disabled = True
         
     | 
| 248 | 
         
            +
                        self.analysis_controls.show_children.checkbox.active = False
         
     | 
| 249 | 
         
            +
                        self.analysis_controls.show_children.disabled =True
         
     | 
| 250 | 
         
            +
                        self.analysis_controls.hints.checkbox.active = False
         
     | 
| 251 | 
         
            +
                        self.analysis_controls.hints.disabled =True
         
     | 
| 252 | 
         
            +
                        self.analysis_controls.policy.checkbox.active = False
         
     | 
| 253 | 
         
            +
                        self.analysis_controls.policy.disabled =True
         
     | 
| 254 | 
         
            +
                        self.controls.ids.undo.disabled = True
         
     | 
| 255 | 
         
            +
                        self.board_controls.ids.pass_btn.disabled = True
         
     | 
| 256 | 
         
            +
                        self.controls.ids.timer.ids.pause.disabled = True
         
     | 
| 257 | 
         
            +
                    except Exception as e:
         
     | 
| 258 | 
         
            +
                        self.log(f"Error enabling hamburger button: {e}", OUTPUT_ERROR)
         
     | 
| 259 | 
         
            +
             
     | 
| 260 | 
         
            +
                def _do_start_hongik_vshuman(self):
         
     | 
| 261 | 
         
            +
                    """Starts a new game between a human player and Hongik AI."""
         
     | 
| 262 | 
         
            +
                    self._do_new_game(player_types=(PLAYER_HUMAN, PLAYER_AI))
         
     | 
| 263 | 
         
            +
                    try:
         
     | 
| 264 | 
         
            +
                        hamburger_button = self.analysis_controls.ids.get('hamburger')
         
     | 
| 265 | 
         
            +
                        if hamburger_button:
         
     | 
| 266 | 
         
            +
                            hamburger_button.disabled = True
         
     | 
| 267 | 
         
            +
                    except Exception as e:
         
     | 
| 268 | 
         
            +
                        self.log(f"Error enabling hamburger button: {e}", OUTPUT_ERROR)
         
     | 
| 269 | 
         
            +
                    
         
     | 
| 270 | 
         
            +
                def _do_undo(self, n_times=1):
         
     | 
| 271 | 
         
            +
                    """Handles an 'undo' event, going back a specified number of moves."""
         
     | 
| 272 | 
         
            +
                    try: n_times = int(n_times)
         
     | 
| 273 | 
         
            +
                    except (ValueError, TypeError): n_times = 1
         
     | 
| 274 | 
         
            +
                    self.game.undo(n_times); self.update_state()
         
     | 
| 275 | 
         
            +
                    
         
     | 
| 276 | 
         
            +
                def _do_resign(self):
         
     | 
| 277 | 
         
            +
                    """Handles a 'resign' event, ending the game and resetting the GUI."""
         
     | 
| 278 | 
         
            +
                    if self.game:
         
     | 
| 279 | 
         
            +
                        winner = 'W' if self.game.current_node.next_player == 'B' else 'B'
         
     | 
| 280 | 
         
            +
                        self.game.root.set_property("RE", f"{winner}+Resign")
         
     | 
| 281 | 
         
            +
                        try:
         
     | 
| 282 | 
         
            +
                            self_play_button = self.board_controls.ids.hongik_selfplay_btn
         
     | 
| 283 | 
         
            +
                            vs_human_button = self.board_controls.ids.hongik_vs_human_btn
         
     | 
| 284 | 
         
            +
                            self_play_button.state = 'normal'
         
     | 
| 285 | 
         
            +
                            vs_human_button.state = 'normal'
         
     | 
| 286 | 
         
            +
                        except Exception as e:
         
     | 
| 287 | 
         
            +
                            self.log(f"Failed to change button state: {e}", OUTPUT_ERROR)
         
     | 
| 288 | 
         
            +
                        self.game = Game(self, self.engine)
         
     | 
| 289 | 
         
            +
                        self._do_new_game()
         
     | 
| 290 | 
         
            +
                
         
     | 
| 291 | 
         
            +
                def load_sgf_file(self, file_path):
         
     | 
| 292 | 
         
            +
                    """Initiates loading of an SGF file in a separate thread."""
         
     | 
| 293 | 
         
            +
                    self.controls.set_status(f"Loading SGF file: {os.path.basename(file_path)}", STATUS_INFO)
         
     | 
| 294 | 
         
            +
                    threading.Thread(target=self._load_sgf_thread_target, args=(file_path,), daemon=True).start()
         
     | 
| 295 | 
         
            +
             
     | 
| 296 | 
         
            +
                def _load_sgf_thread_target(self, file_path):
         
     | 
| 297 | 
         
            +
                    """The target function for the SGF loading thread."""
         
     | 
| 298 | 
         
            +
                    try:
         
     | 
| 299 | 
         
            +
                        move_tree = KaTrainSGF.parse_file(os.path.abspath(file_path))
         
     | 
| 300 | 
         
            +
                        Clock.schedule_once(lambda dt: self._do_new_game(move_tree=move_tree, sgf_filename=file_path))
         
     | 
| 301 | 
         
            +
                    except Exception as e:
         
     | 
| 302 | 
         
            +
                        self.log(f"SGF file loading failed: {e}", OUTPUT_ERROR)
         
     | 
| 303 | 
         
            +
                        Clock.schedule_once(lambda dt: self.controls.set_status(f"SGF loading failed", STATUS_ERROR))
         
     | 
| 304 | 
         
            +
             
     | 
| 305 | 
         
            +
                def handle_animations(self, *_args): pass
         
     | 
| 306 | 
         
            +
                
         
     | 
| 307 | 
         
            +
                def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
         
     | 
| 308 | 
         
            +
                    """Handles keyboard shortcuts, such as toggling move numbers."""
         
     | 
| 309 | 
         
            +
                    if not self.game: return True
         
     | 
| 310 | 
         
            +
                    key = keycode[1]
         
     | 
| 311 | 
         
            +
                    if key == 'n':
         
     | 
| 312 | 
         
            +
                        self.show_move_numbers = not self.show_move_numbers
         
     | 
| 313 | 
         
            +
                        self.board_gui.redraw_board_contents_trigger()
         
     | 
| 314 | 
         
            +
                        return True
         
     | 
| 315 | 
         
            +
                    return False
         
     | 
| 316 | 
         
            +
                
         
     | 
| 317 | 
         
            +
                def _do_score(self, *_args):
         
     | 
| 318 | 
         
            +
                    """Handles a 'score' event, requesting the engine to score the current position."""
         
     | 
| 319 | 
         
            +
                    self.board_gui.game_over_message = "Scoring..."
         
     | 
| 320 | 
         
            +
                    self.controls.set_status("Scoring...", STATUS_INFO)
         
     | 
| 321 | 
         
            +
                    def score_callback(score_details):
         
     | 
| 322 | 
         
            +
                        if score_details:
         
     | 
| 323 | 
         
            +
                            winner = score_details['winner']
         
     | 
| 324 | 
         
            +
                            score = score_details['score']
         
     | 
| 325 | 
         
            +
                            self.game.set_result(f"{winner}+{abs(score)}")
         
     | 
| 326 | 
         
            +
                            self.update_state()
         
     | 
| 327 | 
         
            +
                        else:
         
     | 
| 328 | 
         
            +
                            self.controls.set_status("Failed to score the game.", STATUS_ERROR)
         
     | 
| 329 | 
         
            +
                    self.engine.request_score(self.game.current_node, score_callback)
         
     | 
| 330 | 
         
            +
                
         
     | 
| 331 | 
         
            +
                def _bind_widgets(self, dt):
         
     | 
| 332 | 
         
            +
                    """위젯이 모두 생성된 후, 이벤트를 파이썬에서 직접 바인딩합니다."""
         
     | 
| 333 | 
         
            +
                    if self.analysis_controls and self.analysis_controls.show_children:
         
     | 
| 334 | 
         
            +
                        self.analysis_controls.show_children.checkbox.bind(active=self._handle_show_children_toggle)
         
     | 
| 335 | 
         
            +
             
     | 
| 336 | 
         
            +
                    if self.nav_drawer_contents and 'player_type_spinner_W' in self.nav_drawer_contents.ids:
         
     | 
| 337 | 
         
            +
                        self.nav_drawer_contents.ids.player_type_spinner_W.disabled = True
         
     | 
| 338 | 
         
            +
                        print("W player type spinner successfully disabled via Python.") # 확인용 로그
         
     | 
| 339 | 
         
            +
             
     | 
| 340 | 
         
            +
                def on_nav_drawer_close(self):
         
     | 
| 341 | 
         
            +
                    """Handles the closing of the navigation drawer, forcing a redraw."""
         
     | 
| 342 | 
         
            +
                    self.update_state() 
         
     | 
| 343 | 
         
            +
                    if self.board_gui:
         
     | 
| 344 | 
         
            +
                        self.board_gui.draw_board()
         
     | 
| 345 | 
         
            +
                        self.board_gui.redraw_board_contents_trigger() 
         
     | 
| 346 | 
         
            +
                    self.canvas.ask_update() 
         
     | 
| 347 | 
         
            +
             
     | 
| 348 | 
         
            +
                def _do_contribute_popup(self,*_args):pass
         
     | 
| 349 | 
         
            +
                def _do_config_popup(self, *_args):pass    
         
     | 
| 350 | 
         
            +
                def _do_new_game_popup(self,*_args):pass    
         
     | 
| 351 | 
         
            +
                def _do_save_game(self,*_args):pass
         
     | 
| 352 | 
         
            +
                def _do_save_game_as_popup(self,*_args):pass
         
     | 
| 353 | 
         
            +
                def _do_analyze_sgf_popup(self,*_args):pass
         
     | 
| 354 | 
         
            +
                def _do_teacher_popup(self,*_args):pass
         
     | 
| 355 | 
         
            +
                def _do_ai_popup(self,*_args):pass
         
     | 
| 356 | 
         
            +
                def _do_timer_popup(self,*_args):pass
         
     | 
| 357 | 
         
            +
             
     | 
| 358 | 
         
            +
            class KaTrainApp(MDApp):
         
     | 
| 359 | 
         
            +
                """
         
     | 
| 360 | 
         
            +
                The main application class that inherits from KivyMD's MDApp. It builds the
         
     | 
| 361 | 
         
            +
                GUI, manages the configuration, and handles application lifecycle events.
         
     | 
| 362 | 
         
            +
                """
         
     | 
| 363 | 
         
            +
                gui = ObjectProperty(None)
         
     | 
| 364 | 
         
            +
                language = StringProperty(DEFAULT_LANGUAGE, allownone=True)
         
     | 
| 365 | 
         
            +
                
         
     | 
| 366 | 
         
            +
                def __init__(self, **kwargs):
         
     | 
| 367 | 
         
            +
                    super().__init__(**kwargs)
         
     | 
| 368 | 
         
            +
                    self._resize_event = None
         
     | 
| 369 | 
         
            +
             
     | 
| 370 | 
         
            +
                def build_config(self, config):
         
     | 
| 371 | 
         
            +
                    """Sets up the default configuration for the application."""
         
     | 
| 372 | 
         
            +
                    if 'SGF' not in PATHS:
         
     | 
| 373 | 
         
            +
                        PATHS['SGF'] = os.path.join(PATHS.get('USER', '.'), 'sgf')
         
     | 
| 374 | 
         
            +
                        os.makedirs(PATHS['SGF'], exist_ok=True)
         
     | 
| 375 | 
         
            +
                    config.setdefaults("general",{"lang": DEFAULT_LANGUAGE, "show_player_rank": True, "last_sgf_directory": PATHS["SGF"],})
         
     | 
| 376 | 
         
            +
                    config.setdefaults("engine", {"max_visits": "100"})
         
     | 
| 377 | 
         
            +
                    
         
     | 
| 378 | 
         
            +
                    threshold_str = "-1,0.5,1.5,3,5,7.5,10"
         
     | 
| 379 | 
         
            +
                    thresholds_as_floats = [float(v) for v in threshold_str.split(',')]
         
     | 
| 380 | 
         
            +
                    
         
     | 
| 381 | 
         
            +
                    config.setdefaults("trainer", {
         
     | 
| 382 | 
         
            +
                        "eval_thresholds": thresholds_as_floats,
         
     | 
| 383 | 
         
            +
                        "theme": "theme:normal" 
         
     | 
| 384 | 
         
            +
                    })
         
     | 
| 385 | 
         
            +
                    
         
     | 
| 386 | 
         
            +
                    config.setdefaults("uistate", {"size": "[1300, 1000]"})
         
     | 
| 387 | 
         
            +
             
     | 
| 388 | 
         
            +
                def build(self):
         
     | 
| 389 | 
         
            +
                    """Builds the application's widget tree and sets up window bindings."""
         
     | 
| 390 | 
         
            +
                    pygame.mixer.init()
         
     | 
| 391 | 
         
            +
                    self.icon, self.title = ICON, "홍익 AI - KaTrain"
         
     | 
| 392 | 
         
            +
                    self.theme_cls.theme_style, self.theme_cls.primary_palette = "Dark", "Gray"
         
     | 
| 393 | 
         
            +
                    for p in [os.path.join(PATHS["PACKAGE"], d) for d in ["fonts","sounds","img", "lang"]] + [os.path.abspath(PATHS["USER"])]:
         
     | 
| 394 | 
         
            +
                        resource_add_path(p)
         
     | 
| 395 | 
         
            +
                    Builder.load_file(find_package_resource("katrain/gui.kv"))
         
     | 
| 396 | 
         
            +
                    Builder.load_file(find_package_resource("katrain/popups.kv"))
         
     | 
| 397 | 
         
            +
                    Window.bind(on_request_close=self.on_request_close)
         
     | 
| 398 | 
         
            +
                    Window.bind(on_dropfile=lambda win, file: self.gui.load_sgf_file(file.decode("utf8")))
         
     | 
| 399 | 
         
            +
                    Window.bind(on_resize=self.on_resize)
         
     | 
| 400 | 
         
            +
                    self.gui = KaTrainGui(katrain_app=self, config=self.config)
         
     | 
| 401 | 
         
            +
                    Window.size = Window.system_size
         
     | 
| 402 | 
         
            +
                    return self.gui
         
     | 
| 403 | 
         
            +
                
         
     | 
| 404 | 
         
            +
                def on_resize(self, window, width, height):
         
     | 
| 405 | 
         
            +
                    """Controls the storm of resize events by debouncing them with a short delay."""
         
     | 
| 406 | 
         
            +
                    if self._resize_event:
         
     | 
| 407 | 
         
            +
                        self._resize_event.cancel()
         
     | 
| 408 | 
         
            +
                    self._resize_event = Clock.schedule_once(self._redraw_all, 0.15)
         
     | 
| 409 | 
         
            +
             
     | 
| 410 | 
         
            +
                def _redraw_all(self, dt):
         
     | 
| 411 | 
         
            +
                    """The actual function that redraws the entire screen after a resize."""
         
     | 
| 412 | 
         
            +
                    if self.gui:
         
     | 
| 413 | 
         
            +
                        self.gui.update_state(redraw_board=True)
         
     | 
| 414 | 
         
            +
                
         
     | 
| 415 | 
         
            +
                def on_start(self):
         
     | 
| 416 | 
         
            +
                    """Called when the application is starting."""
         
     | 
| 417 | 
         
            +
                    self.language = self.gui.config("general/lang") or DEFAULT_LANGUAGE
         
     | 
| 418 | 
         
            +
                    self.gui.start()
         
     | 
| 419 | 
         
            +
                    Window.show()        
         
     | 
| 420 | 
         
            +
             
     | 
| 421 | 
         
            +
                def on_language(self, _instance, language):
         
     | 
| 422 | 
         
            +
                    """Handles language changes."""
         
     | 
| 423 | 
         
            +
                    i18n.switch_lang(language)
         
     | 
| 424 | 
         
            +
                    self.gui.config_set("general", "lang", language)
         
     | 
| 425 | 
         
            +
                    
         
     | 
| 426 | 
         
            +
                def on_request_close(self, *_args, **_kwargs):
         
     | 
| 427 | 
         
            +
                    """Handles the window close event, saving the window size and shutting down the engine."""
         
     | 
| 428 | 
         
            +
                    if getattr(self, "gui", None):
         
     | 
| 429 | 
         
            +
                        size_str = json.dumps([int(d) for d in Window.size])
         
     | 
| 430 | 
         
            +
                        self.gui.config_set("uistate", "size", size_str)
         
     | 
| 431 | 
         
            +
                        self.gui.save_config("uistate")
         
     | 
| 432 | 
         
            +
                        if self.gui.engine: self.gui.engine.shutdown()
         
     | 
| 433 | 
         
            +
                        
         
     | 
| 434 | 
         
            +
                def signal_handler(self, _signal, _frame):
         
     | 
| 435 | 
         
            +
                    """Handles signals like Ctrl+C."""
         
     | 
| 436 | 
         
            +
                    self.stop()
         
     | 
| 437 | 
         
            +
             
     | 
| 438 | 
         
            +
            def run_app():
         
     | 
| 439 | 
         
            +
                """Initializes and runs the application."""
         
     | 
| 440 | 
         
            +
                class CrashHandler(ExceptionHandler):
         
     | 
| 441 | 
         
            +
                    def handle_exception(self, inst):
         
     | 
| 442 | 
         
            +
                        trace = "".join(traceback.format_tb(sys.exc_info()[2]))
         
     | 
| 443 | 
         
            +
                        app = MDApp.get_running_app()   
         
     | 
| 444 | 
         
            +
                        message = f"Exception {inst.__class__.__name__}: {inst}\n{trace}"
         
     | 
| 445 | 
         
            +
                        if app and app.gui: app.gui.log(message, OUTPUT_ERROR)
         
     | 
| 446 | 
         
            +
                        else: print(message)
         
     | 
| 447 | 
         
            +
                        return ExceptionManager.PASS
         
     | 
| 448 | 
         
            +
                ExceptionManager.add_handler(CrashHandler())
         
     | 
| 449 | 
         
            +
                
         
     | 
| 450 | 
         
            +
                Config.set('graphics', 'window_state', 'hidden')
         
     | 
| 451 | 
         
            +
             
     | 
| 452 | 
         
            +
                app = KaTrainApp(); signal.signal(signal.SIGINT, app.signal_handler); app.run()
         
     | 
| 453 | 
         
            +
             
     | 
| 454 | 
         
            +
            if __name__ == "__main__":
         
     | 
| 455 | 
         
            +
                run_app()
         
     | 
    	
        katrain/katrain/__main__.spec
    ADDED
    
    | 
         @@ -0,0 +1,38 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            # -*- mode: python ; coding: utf-8 -*-
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
             
     | 
| 4 | 
         
            +
            a = Analysis(
         
     | 
| 5 | 
         
            +
                ['__main__.py'],
         
     | 
| 6 | 
         
            +
                pathex=[],
         
     | 
| 7 | 
         
            +
                binaries=[],
         
     | 
| 8 | 
         
            +
                datas=[('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-Regular.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-Italic.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-Bold.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-BoldItalic.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-Thin.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-BoldItalic.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-LightItalic.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-Light.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-BlackItalic.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-Black.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-MediumItalic.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-Medium.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\Roboto-ThinItalic.ttf', 'kivymd\\fonts'), ('C:\\Users\\puco2\\katrain\\katrain\\fonts\\materialdesignicons-webfont.ttf', 'kivymd\\fonts')],
         
     | 
| 9 | 
         
            +
                hiddenimports=[],
         
     | 
| 10 | 
         
            +
                hookspath=[],
         
     | 
| 11 | 
         
            +
                hooksconfig={},
         
     | 
| 12 | 
         
            +
                runtime_hooks=[],
         
     | 
| 13 | 
         
            +
                excludes=[],
         
     | 
| 14 | 
         
            +
                noarchive=False,
         
     | 
| 15 | 
         
            +
                optimize=0,
         
     | 
| 16 | 
         
            +
            )
         
     | 
| 17 | 
         
            +
            pyz = PYZ(a.pure)
         
     | 
| 18 | 
         
            +
             
     | 
| 19 | 
         
            +
            exe = EXE(
         
     | 
| 20 | 
         
            +
                pyz,
         
     | 
| 21 | 
         
            +
                a.scripts,
         
     | 
| 22 | 
         
            +
                a.binaries,
         
     | 
| 23 | 
         
            +
                a.datas,
         
     | 
| 24 | 
         
            +
                [],
         
     | 
| 25 | 
         
            +
                name='__main__',
         
     | 
| 26 | 
         
            +
                debug=False,
         
     | 
| 27 | 
         
            +
                bootloader_ignore_signals=False,
         
     | 
| 28 | 
         
            +
                strip=False,
         
     | 
| 29 | 
         
            +
                upx=True,
         
     | 
| 30 | 
         
            +
                upx_exclude=[],
         
     | 
| 31 | 
         
            +
                runtime_tmpdir=None,
         
     | 
| 32 | 
         
            +
                console=False,
         
     | 
| 33 | 
         
            +
                disable_windowed_traceback=False,
         
     | 
| 34 | 
         
            +
                argv_emulation=False,
         
     | 
| 35 | 
         
            +
                target_arch=None,
         
     | 
| 36 | 
         
            +
                codesign_identity=None,
         
     | 
| 37 | 
         
            +
                entitlements_file=None,
         
     | 
| 38 | 
         
            +
            )
         
     | 
    	
        katrain/katrain/__pycache__/__init__.cpython-310.pyc
    ADDED
    
    | 
         Binary file (142 Bytes). View file 
     | 
| 
         | 
    	
        katrain/katrain/config.json
    ADDED
    
    | 
         @@ -0,0 +1,235 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            {
         
     | 
| 2 | 
         
            +
                "engine": {
         
     | 
| 3 | 
         
            +
                    "katago": "",
         
     | 
| 4 | 
         
            +
                    "altcommand": "",
         
     | 
| 5 | 
         
            +
                    "model": "katrain/models/g170e-b15c192-s1672170752-d466197061.bin.gz",
         
     | 
| 6 | 
         
            +
                    "config": "katrain/KataGo/analysis_config.cfg",
         
     | 
| 7 | 
         
            +
                    "threads": 12,
         
     | 
| 8 | 
         
            +
                    "max_visits": 500,
         
     | 
| 9 | 
         
            +
                    "fast_visits": 25,
         
     | 
| 10 | 
         
            +
                    "max_time": 8.0,
         
     | 
| 11 | 
         
            +
                    "wide_root_noise": 0.04,
         
     | 
| 12 | 
         
            +
                    "_enable_ownership": true
         
     | 
| 13 | 
         
            +
                },
         
     | 
| 14 | 
         
            +
                "contribute": {
         
     | 
| 15 | 
         
            +
                    "katago": "",
         
     | 
| 16 | 
         
            +
                    "config": "katrain/KataGo/contribute_config.cfg",
         
     | 
| 17 | 
         
            +
                    "ownership": false,
         
     | 
| 18 | 
         
            +
                    "maxgames": 6,
         
     | 
| 19 | 
         
            +
                    "movespeed": 2,
         
     | 
| 20 | 
         
            +
                    "username": "",
         
     | 
| 21 | 
         
            +
                    "password": "",
         
     | 
| 22 | 
         
            +
                    "savepath": "./dist_sgf/",
         
     | 
| 23 | 
         
            +
                    "savesgf": false
         
     | 
| 24 | 
         
            +
                },
         
     | 
| 25 | 
         
            +
                "general": {
         
     | 
| 26 | 
         
            +
                    "sgf_load": "~/Downloads",
         
     | 
| 27 | 
         
            +
                    "sgf_save": "./sgfout",
         
     | 
| 28 | 
         
            +
                    "anim_pv_time": 0.5,
         
     | 
| 29 | 
         
            +
                    "debug_level": 0,
         
     | 
| 30 | 
         
            +
                    "lang": "en",
         
     | 
| 31 | 
         
            +
                    "version": "1.13.0",
         
     | 
| 32 | 
         
            +
                    "load_fast_analysis": false,
         
     | 
| 33 | 
         
            +
                    "load_sgf_rewind": true
         
     | 
| 34 | 
         
            +
                },
         
     | 
| 35 | 
         
            +
                "timer": {
         
     | 
| 36 | 
         
            +
                    "byo_length": 30,
         
     | 
| 37 | 
         
            +
                    "byo_periods": 5,
         
     | 
| 38 | 
         
            +
                    "minimal_use": 0,
         
     | 
| 39 | 
         
            +
                    "main_time": 0,
         
     | 
| 40 | 
         
            +
                    "sound": true
         
     | 
| 41 | 
         
            +
                },
         
     | 
| 42 | 
         
            +
                "game": {
         
     | 
| 43 | 
         
            +
                    "size": "19",
         
     | 
| 44 | 
         
            +
                    "komi": 6.5,
         
     | 
| 45 | 
         
            +
                    "handicap": 0,
         
     | 
| 46 | 
         
            +
                    "rules": "japanese",
         
     | 
| 47 | 
         
            +
                    "clear_cache": false,
         
     | 
| 48 | 
         
            +
                    "setup_move":100,
         
     | 
| 49 | 
         
            +
                    "setup_advantage":20
         
     | 
| 50 | 
         
            +
                },
         
     | 
| 51 | 
         
            +
                "trainer": {
         
     | 
| 52 | 
         
            +
                    "theme": "theme:normal",
         
     | 
| 53 | 
         
            +
                    "num_undo_prompts": [
         
     | 
| 54 | 
         
            +
                        1,
         
     | 
| 55 | 
         
            +
                        1,
         
     | 
| 56 | 
         
            +
                        1,
         
     | 
| 57 | 
         
            +
                        0.5,
         
     | 
| 58 | 
         
            +
                        0,
         
     | 
| 59 | 
         
            +
                        0
         
     | 
| 60 | 
         
            +
                    ],
         
     | 
| 61 | 
         
            +
                    "eval_thresholds": [
         
     | 
| 62 | 
         
            +
                        12,
         
     | 
| 63 | 
         
            +
                        6,
         
     | 
| 64 | 
         
            +
                        3,
         
     | 
| 65 | 
         
            +
                        1.5,
         
     | 
| 66 | 
         
            +
                        0.5,
         
     | 
| 67 | 
         
            +
                        0
         
     | 
| 68 | 
         
            +
                    ],
         
     | 
| 69 | 
         
            +
                    "save_feedback": [
         
     | 
| 70 | 
         
            +
                        true,
         
     | 
| 71 | 
         
            +
                        true,
         
     | 
| 72 | 
         
            +
                        true,
         
     | 
| 73 | 
         
            +
                        true,
         
     | 
| 74 | 
         
            +
                        false,
         
     | 
| 75 | 
         
            +
                        false
         
     | 
| 76 | 
         
            +
                    ],
         
     | 
| 77 | 
         
            +
                    "show_dots": [
         
     | 
| 78 | 
         
            +
                        true,
         
     | 
| 79 | 
         
            +
                        true,
         
     | 
| 80 | 
         
            +
                        true,
         
     | 
| 81 | 
         
            +
                        true,
         
     | 
| 82 | 
         
            +
                        true,
         
     | 
| 83 | 
         
            +
                        true
         
     | 
| 84 | 
         
            +
                    ],
         
     | 
| 85 | 
         
            +
                    "extra_precision": false,
         
     | 
| 86 | 
         
            +
                    "save_analysis": false,
         
     | 
| 87 | 
         
            +
                    "save_marks": false,
         
     | 
| 88 | 
         
            +
                    "low_visits": 25,
         
     | 
| 89 | 
         
            +
                    "eval_on_show_last": 3,
         
     | 
| 90 | 
         
            +
                    "top_moves_show": "top_move_delta_score",
         
     | 
| 91 | 
         
            +
                    "top_moves_show_secondary": "top_move_visits",
         
     | 
| 92 | 
         
            +
                    "eval_show_ai": true,
         
     | 
| 93 | 
         
            +
                    "lock_ai": false
         
     | 
| 94 | 
         
            +
                },
         
     | 
| 95 | 
         
            +
                "ai": {
         
     | 
| 96 | 
         
            +
                    "ai:default": {},
         
     | 
| 97 | 
         
            +
                    "ai:antimirror": {},
         
     | 
| 98 | 
         
            +
                    "ai:handicap": {
         
     | 
| 99 | 
         
            +
                        "automatic": true,
         
     | 
| 100 | 
         
            +
                        "pda": 0
         
     | 
| 101 | 
         
            +
                    },
         
     | 
| 102 | 
         
            +
                    "ai:jigo": {
         
     | 
| 103 | 
         
            +
                        "target_score": 0.5
         
     | 
| 104 | 
         
            +
                    },
         
     | 
| 105 | 
         
            +
                    "ai:scoreloss": {
         
     | 
| 106 | 
         
            +
                        "strength": 0.2
         
     | 
| 107 | 
         
            +
                    },
         
     | 
| 108 | 
         
            +
                    "ai:policy": {
         
     | 
| 109 | 
         
            +
                        "opening_moves": 22.0
         
     | 
| 110 | 
         
            +
                    },
         
     | 
| 111 | 
         
            +
                    "ai:simple": {
         
     | 
| 112 | 
         
            +
                        "max_points_lost": 1.75,
         
     | 
| 113 | 
         
            +
                        "settled_weight": 1.0,
         
     | 
| 114 | 
         
            +
                        "opponent_fac": 0.5,
         
     | 
| 115 | 
         
            +
                        "min_visits": 3,
         
     | 
| 116 | 
         
            +
                        "attach_penalty": 1,
         
     | 
| 117 | 
         
            +
                        "tenuki_penalty": 0.5
         
     | 
| 118 | 
         
            +
                    },
         
     | 
| 119 | 
         
            +
                    "ai:p:weighted": {
         
     | 
| 120 | 
         
            +
                        "weaken_fac": 1.25,
         
     | 
| 121 | 
         
            +
                        "pick_override": 1.0,
         
     | 
| 122 | 
         
            +
                        "lower_bound": 0.001
         
     | 
| 123 | 
         
            +
                    },
         
     | 
| 124 | 
         
            +
                    "ai:p:pick": {
         
     | 
| 125 | 
         
            +
                        "pick_override": 0.95,
         
     | 
| 126 | 
         
            +
                        "pick_n": 5,
         
     | 
| 127 | 
         
            +
                        "pick_frac": 0.35
         
     | 
| 128 | 
         
            +
                    },
         
     | 
| 129 | 
         
            +
                    "ai:p:local": {
         
     | 
| 130 | 
         
            +
                        "pick_override": 0.95,
         
     | 
| 131 | 
         
            +
                        "stddev": 1.5,
         
     | 
| 132 | 
         
            +
                        "pick_n": 15,
         
     | 
| 133 | 
         
            +
                        "pick_frac": 0.0,
         
     | 
| 134 | 
         
            +
                        "endgame": 0.5
         
     | 
| 135 | 
         
            +
                    },
         
     | 
| 136 | 
         
            +
                    "ai:p:tenuki": {
         
     | 
| 137 | 
         
            +
                        "pick_override": 0.85,
         
     | 
| 138 | 
         
            +
                        "stddev": 7.5,
         
     | 
| 139 | 
         
            +
                        "pick_n": 5,
         
     | 
| 140 | 
         
            +
                        "pick_frac": 0.4,
         
     | 
| 141 | 
         
            +
                        "endgame": 0.45
         
     | 
| 142 | 
         
            +
                    },
         
     | 
| 143 | 
         
            +
                    "ai:p:influence": {
         
     | 
| 144 | 
         
            +
                        "pick_override": 0.95,
         
     | 
| 145 | 
         
            +
                        "pick_n": 5,
         
     | 
| 146 | 
         
            +
                        "pick_frac": 0.3,
         
     | 
| 147 | 
         
            +
                        "threshold": 3.5,
         
     | 
| 148 | 
         
            +
                        "line_weight": 10,
         
     | 
| 149 | 
         
            +
                        "endgame": 0.4
         
     | 
| 150 | 
         
            +
                    },
         
     | 
| 151 | 
         
            +
                    "ai:p:territory": {
         
     | 
| 152 | 
         
            +
                        "pick_override": 0.95,
         
     | 
| 153 | 
         
            +
                        "pick_n": 5,
         
     | 
| 154 | 
         
            +
                        "pick_frac": 0.3,
         
     | 
| 155 | 
         
            +
                        "threshold": 3.5,
         
     | 
| 156 | 
         
            +
                        "line_weight": 2,
         
     | 
| 157 | 
         
            +
                        "endgame": 0.4
         
     | 
| 158 | 
         
            +
                    },
         
     | 
| 159 | 
         
            +
                    "ai:p:rank": {
         
     | 
| 160 | 
         
            +
                        "kyu_rank": 4.0
         
     | 
| 161 | 
         
            +
                    }
         
     | 
| 162 | 
         
            +
                },
         
     | 
| 163 | 
         
            +
                "ui_state": {
         
     | 
| 164 | 
         
            +
                    "restoresize": true,
         
     | 
| 165 | 
         
            +
                    "size": [],
         
     | 
| 166 | 
         
            +
                    "play": {
         
     | 
| 167 | 
         
            +
                        "analysis_controls": {
         
     | 
| 168 | 
         
            +
                            "show_children": true,
         
     | 
| 169 | 
         
            +
                            "eval": false,
         
     | 
| 170 | 
         
            +
                            "hints": false,
         
     | 
| 171 | 
         
            +
                            "policy": false,
         
     | 
| 172 | 
         
            +
                            "ownership": false
         
     | 
| 173 | 
         
            +
                        },
         
     | 
| 174 | 
         
            +
                        "panels": {
         
     | 
| 175 | 
         
            +
                            "graph_panel": [
         
     | 
| 176 | 
         
            +
                                "open",
         
     | 
| 177 | 
         
            +
                                {
         
     | 
| 178 | 
         
            +
                                    "score": true,
         
     | 
| 179 | 
         
            +
                                    "winrate": false
         
     | 
| 180 | 
         
            +
                                }
         
     | 
| 181 | 
         
            +
                            ],
         
     | 
| 182 | 
         
            +
                            "stats_panel": [
         
     | 
| 183 | 
         
            +
                                "open",
         
     | 
| 184 | 
         
            +
                                {
         
     | 
| 185 | 
         
            +
                                    "score": true,
         
     | 
| 186 | 
         
            +
                                    "winrate": true,
         
     | 
| 187 | 
         
            +
                                    "points": true
         
     | 
| 188 | 
         
            +
                                }
         
     | 
| 189 | 
         
            +
                            ],
         
     | 
| 190 | 
         
            +
                            "notes_panel": [
         
     | 
| 191 | 
         
            +
                                "open",
         
     | 
| 192 | 
         
            +
                                {
         
     | 
| 193 | 
         
            +
                                    "info": true,
         
     | 
| 194 | 
         
            +
                                    "info-details": false,
         
     | 
| 195 | 
         
            +
                                    "notes": false
         
     | 
| 196 | 
         
            +
                                }
         
     | 
| 197 | 
         
            +
                            ]
         
     | 
| 198 | 
         
            +
                        }
         
     | 
| 199 | 
         
            +
                    },
         
     | 
| 200 | 
         
            +
                    "analyze": {
         
     | 
| 201 | 
         
            +
                        "analysis_controls": {
         
     | 
| 202 | 
         
            +
                            "show_children": true,
         
     | 
| 203 | 
         
            +
                            "eval": true,
         
     | 
| 204 | 
         
            +
                            "hints": true,
         
     | 
| 205 | 
         
            +
                            "policy": false,
         
     | 
| 206 | 
         
            +
                            "ownership": true
         
     | 
| 207 | 
         
            +
                        },
         
     | 
| 208 | 
         
            +
                        "panels": {
         
     | 
| 209 | 
         
            +
                            "graph_panel": [
         
     | 
| 210 | 
         
            +
                                "open",
         
     | 
| 211 | 
         
            +
                                {
         
     | 
| 212 | 
         
            +
                                    "score": true,
         
     | 
| 213 | 
         
            +
                                    "winrate": true
         
     | 
| 214 | 
         
            +
                                }
         
     | 
| 215 | 
         
            +
                            ],
         
     | 
| 216 | 
         
            +
                            "stats_panel": [
         
     | 
| 217 | 
         
            +
                                "open",
         
     | 
| 218 | 
         
            +
                                {
         
     | 
| 219 | 
         
            +
                                    "score": true,
         
     | 
| 220 | 
         
            +
                                    "winrate": true,
         
     | 
| 221 | 
         
            +
                                    "points": true
         
     | 
| 222 | 
         
            +
                                }
         
     | 
| 223 | 
         
            +
                            ],
         
     | 
| 224 | 
         
            +
                            "notes_panel": [
         
     | 
| 225 | 
         
            +
                                "open",
         
     | 
| 226 | 
         
            +
                                {
         
     | 
| 227 | 
         
            +
                                    "info": true,
         
     | 
| 228 | 
         
            +
                                    "info-details": true,
         
     | 
| 229 | 
         
            +
                                    "notes": false
         
     | 
| 230 | 
         
            +
                                }
         
     | 
| 231 | 
         
            +
                            ]
         
     | 
| 232 | 
         
            +
                        }
         
     | 
| 233 | 
         
            +
                    }
         
     | 
| 234 | 
         
            +
                }
         
     | 
| 235 | 
         
            +
            }
         
     | 
    	
        katrain/katrain/core/__init__.py
    ADDED
    
    | 
         
            File without changes
         
     | 
    	
        katrain/katrain/core/__pycache__/__init__.cpython-310.pyc
    ADDED
    
    | 
         Binary file (147 Bytes). View file 
     | 
| 
         | 
    	
        katrain/katrain/core/__pycache__/ai.cpython-310.pyc
    ADDED
    
    | 
         Binary file (20.3 kB). View file 
     | 
| 
         | 
    	
        katrain/katrain/core/__pycache__/base_katrain.cpython-310.pyc
    ADDED
    
    | 
         Binary file (4.11 kB). View file 
     | 
| 
         | 
    	
        katrain/katrain/core/__pycache__/constants.cpython-310.pyc
    ADDED
    
    | 
         Binary file (8.15 kB). View file 
     | 
| 
         | 
    	
        katrain/katrain/core/__pycache__/game.cpython-310.pyc
    ADDED
    
    | 
         Binary file (28.4 kB). View file 
     | 
| 
         | 
    	
        katrain/katrain/core/__pycache__/game_node.cpython-310.pyc
    ADDED
    
    | 
         Binary file (15.4 kB). View file 
     | 
| 
         | 
    	
        katrain/katrain/core/__pycache__/lang.cpython-310.pyc
    ADDED
    
    | 
         Binary file (2.9 kB). View file 
     | 
| 
         | 
    	
        katrain/katrain/core/__pycache__/sgf_parser.cpython-310.pyc
    ADDED
    
    | 
         Binary file (23.1 kB). View file 
     | 
| 
         | 
    	
        katrain/katrain/core/__pycache__/utils.cpython-310.pyc
    ADDED
    
    | 
         Binary file (3.92 kB). View file 
     | 
| 
         | 
    	
        katrain/katrain/core/ai.py
    ADDED
    
    | 
         @@ -0,0 +1,516 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import heapq
         
     | 
| 2 | 
         
            +
            import math
         
     | 
| 3 | 
         
            +
            import random
         
     | 
| 4 | 
         
            +
            import time
         
     | 
| 5 | 
         
            +
            from typing import Dict, List, Optional, Tuple
         
     | 
| 6 | 
         
            +
             
     | 
| 7 | 
         
            +
            from katrain.core.constants import (
         
     | 
| 8 | 
         
            +
                AI_DEFAULT,
         
     | 
| 9 | 
         
            +
                AI_HANDICAP,
         
     | 
| 10 | 
         
            +
                AI_INFLUENCE,
         
     | 
| 11 | 
         
            +
                AI_INFLUENCE_ELO_GRID,
         
     | 
| 12 | 
         
            +
                AI_JIGO,
         
     | 
| 13 | 
         
            +
                AI_ANTIMIRROR,
         
     | 
| 14 | 
         
            +
                AI_LOCAL,
         
     | 
| 15 | 
         
            +
                AI_LOCAL_ELO_GRID,
         
     | 
| 16 | 
         
            +
                AI_PICK,
         
     | 
| 17 | 
         
            +
                AI_PICK_ELO_GRID,
         
     | 
| 18 | 
         
            +
                AI_POLICY,
         
     | 
| 19 | 
         
            +
                AI_RANK,
         
     | 
| 20 | 
         
            +
                AI_SCORELOSS,
         
     | 
| 21 | 
         
            +
                AI_SCORELOSS_ELO,
         
     | 
| 22 | 
         
            +
                AI_SETTLE_STONES,
         
     | 
| 23 | 
         
            +
                AI_SIMPLE_OWNERSHIP,
         
     | 
| 24 | 
         
            +
                AI_STRATEGIES_PICK,
         
     | 
| 25 | 
         
            +
                AI_STRATEGIES_POLICY,
         
     | 
| 26 | 
         
            +
                AI_STRENGTH,
         
     | 
| 27 | 
         
            +
                AI_TENUKI,
         
     | 
| 28 | 
         
            +
                AI_TENUKI_ELO_GRID,
         
     | 
| 29 | 
         
            +
                AI_TERRITORY,
         
     | 
| 30 | 
         
            +
                AI_TERRITORY_ELO_GRID,
         
     | 
| 31 | 
         
            +
                AI_WEIGHTED,
         
     | 
| 32 | 
         
            +
                AI_WEIGHTED_ELO,
         
     | 
| 33 | 
         
            +
                CALIBRATED_RANK_ELO,
         
     | 
| 34 | 
         
            +
                OUTPUT_DEBUG,
         
     | 
| 35 | 
         
            +
                OUTPUT_ERROR,
         
     | 
| 36 | 
         
            +
                OUTPUT_INFO,
         
     | 
| 37 | 
         
            +
                PRIORITY_EXTRA_AI_QUERY,
         
     | 
| 38 | 
         
            +
                ADDITIONAL_MOVE_ORDER,
         
     | 
| 39 | 
         
            +
            )
         
     | 
| 40 | 
         
            +
            from katrain.core.game import Game, GameNode, Move
         
     | 
| 41 | 
         
            +
            from katrain.core.utils import var_to_grid, weighted_selection_without_replacement, evaluation_class
         
     | 
| 42 | 
         
            +
             
     | 
| 43 | 
         
            +
             
     | 
| 44 | 
         
            +
            def interp_ix(lst, x):
         
     | 
| 45 | 
         
            +
                i = 0
         
     | 
| 46 | 
         
            +
                while i + 1 < len(lst) - 1 and lst[i + 1] < x:
         
     | 
| 47 | 
         
            +
                    i += 1
         
     | 
| 48 | 
         
            +
                t = max(0, min(1, (x - lst[i]) / (lst[i + 1] - lst[i])))
         
     | 
| 49 | 
         
            +
                return i, t
         
     | 
| 50 | 
         
            +
             
     | 
| 51 | 
         
            +
             
     | 
| 52 | 
         
            +
            def interp1d(lst, x):
         
     | 
| 53 | 
         
            +
                xs, ys = zip(*lst)
         
     | 
| 54 | 
         
            +
                i, t = interp_ix(xs, x)
         
     | 
| 55 | 
         
            +
                return (1 - t) * ys[i] + t * ys[i + 1]
         
     | 
| 56 | 
         
            +
             
     | 
| 57 | 
         
            +
             
     | 
| 58 | 
         
            +
            def interp2d(gridspec, x, y):
         
     | 
| 59 | 
         
            +
                xs, ys, matrix = gridspec
         
     | 
| 60 | 
         
            +
                i, t = interp_ix(xs, x)
         
     | 
| 61 | 
         
            +
                j, s = interp_ix(ys, y)
         
     | 
| 62 | 
         
            +
                return (
         
     | 
| 63 | 
         
            +
                    matrix[j][i] * (1 - t) * (1 - s)
         
     | 
| 64 | 
         
            +
                    + matrix[j][i + 1] * t * (1 - s)
         
     | 
| 65 | 
         
            +
                    + matrix[j + 1][i] * (1 - t) * s
         
     | 
| 66 | 
         
            +
                    + matrix[j + 1][i + 1] * t * s
         
     | 
| 67 | 
         
            +
                )
         
     | 
| 68 | 
         
            +
             
     | 
| 69 | 
         
            +
             
     | 
| 70 | 
         
            +
            def ai_rank_estimation(strategy, settings) -> int:
         
     | 
| 71 | 
         
            +
                if strategy in [AI_DEFAULT, AI_HANDICAP, AI_JIGO]:
         
     | 
| 72 | 
         
            +
                    return 9
         
     | 
| 73 | 
         
            +
                if strategy == AI_RANK:
         
     | 
| 74 | 
         
            +
                    return 1 - settings["kyu_rank"]
         
     | 
| 75 | 
         
            +
                if strategy in [AI_WEIGHTED, AI_SCORELOSS, AI_LOCAL, AI_TENUKI, AI_TERRITORY, AI_INFLUENCE, AI_PICK]:
         
     | 
| 76 | 
         
            +
                    if strategy == AI_WEIGHTED:
         
     | 
| 77 | 
         
            +
                        elo = interp1d(AI_WEIGHTED_ELO, settings["weaken_fac"])
         
     | 
| 78 | 
         
            +
                    if strategy == AI_SCORELOSS:
         
     | 
| 79 | 
         
            +
                        elo = interp1d(AI_SCORELOSS_ELO, settings["strength"])
         
     | 
| 80 | 
         
            +
                    if strategy == AI_PICK:
         
     | 
| 81 | 
         
            +
                        elo = interp2d(AI_PICK_ELO_GRID, settings["pick_frac"], settings["pick_n"])
         
     | 
| 82 | 
         
            +
                    if strategy == AI_LOCAL:
         
     | 
| 83 | 
         
            +
                        elo = interp2d(AI_LOCAL_ELO_GRID, settings["pick_frac"], settings["pick_n"])
         
     | 
| 84 | 
         
            +
                    if strategy == AI_TENUKI:
         
     | 
| 85 | 
         
            +
                        elo = interp2d(AI_TENUKI_ELO_GRID, settings["pick_frac"], settings["pick_n"])
         
     | 
| 86 | 
         
            +
                    if strategy == AI_TERRITORY:
         
     | 
| 87 | 
         
            +
                        elo = interp2d(AI_TERRITORY_ELO_GRID, settings["pick_frac"], settings["pick_n"])
         
     | 
| 88 | 
         
            +
                    if strategy == AI_INFLUENCE:
         
     | 
| 89 | 
         
            +
                        elo = interp2d(AI_INFLUENCE_ELO_GRID, settings["pick_frac"], settings["pick_n"])
         
     | 
| 90 | 
         
            +
             
     | 
| 91 | 
         
            +
                    kyu = interp1d(CALIBRATED_RANK_ELO, elo)
         
     | 
| 92 | 
         
            +
                    return 1 - kyu
         
     | 
| 93 | 
         
            +
                else:
         
     | 
| 94 | 
         
            +
                    return AI_STRENGTH[strategy]
         
     | 
| 95 | 
         
            +
             
     | 
| 96 | 
         
            +
             
     | 
| 97 | 
         
            +
            def game_report(game, thresholds, depth_filter=None):
         
     | 
| 98 | 
         
            +
                cn = game.current_node
         
     | 
| 99 | 
         
            +
                nodes = cn.nodes_from_root
         
     | 
| 100 | 
         
            +
                while cn.children:  # main branch
         
     | 
| 101 | 
         
            +
                    cn = cn.children[0]
         
     | 
| 102 | 
         
            +
                    nodes.append(cn)
         
     | 
| 103 | 
         
            +
             
     | 
| 104 | 
         
            +
                x, y = game.board_size
         
     | 
| 105 | 
         
            +
                depth_filter = [math.ceil(board_frac * x * y) for board_frac in depth_filter or (0, 1e9)]
         
     | 
| 106 | 
         
            +
                nodes = [n for n in nodes if n.move and not n.is_root and depth_filter[0] <= n.depth < depth_filter[1]]
         
     | 
| 107 | 
         
            +
                histogram = [{"B": 0, "W": 0} for _ in thresholds]
         
     | 
| 108 | 
         
            +
                ai_top_move_count = {"B": 0, "W": 0}
         
     | 
| 109 | 
         
            +
                ai_approved_move_count = {"B": 0, "W": 0}
         
     | 
| 110 | 
         
            +
                player_ptloss = {"B": [], "W": []}
         
     | 
| 111 | 
         
            +
                weights = {"B": [], "W": []}
         
     | 
| 112 | 
         
            +
             
     | 
| 113 | 
         
            +
                for n in nodes:
         
     | 
| 114 | 
         
            +
                    points_lost = n.points_lost
         
     | 
| 115 | 
         
            +
                    if n.points_lost is None:
         
     | 
| 116 | 
         
            +
                        continue
         
     | 
| 117 | 
         
            +
                    else:
         
     | 
| 118 | 
         
            +
                        points_lost = max(0, points_lost)
         
     | 
| 119 | 
         
            +
                    bucket = len(thresholds) - 1 - evaluation_class(points_lost, thresholds)
         
     | 
| 120 | 
         
            +
                    player_ptloss[n.player].append(points_lost)
         
     | 
| 121 | 
         
            +
                    histogram[bucket][n.player] += 1
         
     | 
| 122 | 
         
            +
                    cands = n.parent.candidate_moves
         
     | 
| 123 | 
         
            +
                    filtered_cands = [d for d in cands if d["order"] < ADDITIONAL_MOVE_ORDER and "prior" in d]
         
     | 
| 124 | 
         
            +
                    weight = min(
         
     | 
| 125 | 
         
            +
                        1.0,
         
     | 
| 126 | 
         
            +
                        sum([max(d["pointsLost"], 0) * d["prior"] for d in filtered_cands])
         
     | 
| 127 | 
         
            +
                        / (sum(d["prior"] for d in filtered_cands) or 1e-6),
         
     | 
| 128 | 
         
            +
                    )  # complexity capped at 1
         
     | 
| 129 | 
         
            +
                    # adj_weight between 0.05 - 1, dependent on difficulty and points lost
         
     | 
| 130 | 
         
            +
                    adj_weight = max(0.05, min(1.0, max(weight, points_lost / 4)))
         
     | 
| 131 | 
         
            +
                    weights[n.player].append((weight, adj_weight))
         
     | 
| 132 | 
         
            +
                    if n.parent.analysis_complete:
         
     | 
| 133 | 
         
            +
                        ai_top_move_count[n.player] += int(cands[0]["move"] == n.move.gtp())
         
     | 
| 134 | 
         
            +
                        ai_approved_move_count[n.player] += int(
         
     | 
| 135 | 
         
            +
                            n.move.gtp()
         
     | 
| 136 | 
         
            +
                            in [d["move"] for d in filtered_cands if d["order"] == 0 or (d["pointsLost"] < 0.5 and d["order"] < 5)]
         
     | 
| 137 | 
         
            +
                        )
         
     | 
| 138 | 
         
            +
             
     | 
| 139 | 
         
            +
                wt_loss = {
         
     | 
| 140 | 
         
            +
                    bw: sum(s * aw for s, (w, aw) in zip(player_ptloss[bw], weights[bw]))
         
     | 
| 141 | 
         
            +
                    / (sum(aw for _, aw in weights[bw]) or 1e-6)
         
     | 
| 142 | 
         
            +
                    for bw in "BW"
         
     | 
| 143 | 
         
            +
                }
         
     | 
| 144 | 
         
            +
                sum_stats = {
         
     | 
| 145 | 
         
            +
                    bw: {
         
     | 
| 146 | 
         
            +
                        "accuracy": 100 * 0.75 ** wt_loss[bw],
         
     | 
| 147 | 
         
            +
                        "complexity": sum(w for w, aw in weights[bw]) / len(player_ptloss[bw]),
         
     | 
| 148 | 
         
            +
                        "mean_ptloss": sum(player_ptloss[bw]) / len(player_ptloss[bw]),
         
     | 
| 149 | 
         
            +
                        "weighted_ptloss": wt_loss[bw],
         
     | 
| 150 | 
         
            +
                        "ai_top_move": ai_top_move_count[bw] / len(player_ptloss[bw]),
         
     | 
| 151 | 
         
            +
                        "ai_top5_move": ai_approved_move_count[bw] / len(player_ptloss[bw]),
         
     | 
| 152 | 
         
            +
                    }
         
     | 
| 153 | 
         
            +
                    if len(player_ptloss[bw]) > 0
         
     | 
| 154 | 
         
            +
                    else {}
         
     | 
| 155 | 
         
            +
                    for bw in "BW"
         
     | 
| 156 | 
         
            +
                }
         
     | 
| 157 | 
         
            +
                return sum_stats, histogram, player_ptloss
         
     | 
| 158 | 
         
            +
             
     | 
| 159 | 
         
            +
             
     | 
| 160 | 
         
            +
            def dirichlet_noise(num, dir_alpha=0.3):
         
     | 
| 161 | 
         
            +
                sample = [random.gammavariate(dir_alpha, 1) for _ in range(num)]
         
     | 
| 162 | 
         
            +
                sum_sample = sum(sample)
         
     | 
| 163 | 
         
            +
                return [s / sum_sample for s in sample]
         
     | 
| 164 | 
         
            +
             
     | 
| 165 | 
         
            +
             
     | 
| 166 | 
         
            +
            def fmt_moves(moves: List[Tuple[float, Move]]):
         
     | 
| 167 | 
         
            +
                return ", ".join(f"{mv.gtp()} ({p:.2%})" for p, mv in moves)
         
     | 
| 168 | 
         
            +
             
     | 
| 169 | 
         
            +
             
     | 
| 170 | 
         
            +
            def policy_weighted_move(policy_moves, lower_bound, weaken_fac):
         
     | 
| 171 | 
         
            +
                lower_bound, weaken_fac = max(0, lower_bound), max(0.01, weaken_fac)
         
     | 
| 172 | 
         
            +
                weighted_coords = [
         
     | 
| 173 | 
         
            +
                    (pv, pv ** (1 / weaken_fac), move) for pv, move in policy_moves if pv > lower_bound and not move.is_pass
         
     | 
| 174 | 
         
            +
                ]
         
     | 
| 175 | 
         
            +
                if weighted_coords:
         
     | 
| 176 | 
         
            +
                    top = weighted_selection_without_replacement(weighted_coords, 1)[0]
         
     | 
| 177 | 
         
            +
                    move = top[2]
         
     | 
| 178 | 
         
            +
                    ai_thoughts = f"Playing policy-weighted random move {move.gtp()} ({top[0]:.1%}) from {len(weighted_coords)} moves above lower_bound of {lower_bound:.1%}."
         
     | 
| 179 | 
         
            +
                else:
         
     | 
| 180 | 
         
            +
                    move = policy_moves[0][1]
         
     | 
| 181 | 
         
            +
                    ai_thoughts = f"Playing top policy move because no non-pass move > above lower_bound of {lower_bound:.1%}."
         
     | 
| 182 | 
         
            +
                return move, ai_thoughts
         
     | 
| 183 | 
         
            +
             
     | 
| 184 | 
         
            +
             
     | 
| 185 | 
         
            +
            def generate_influence_territory_weights(ai_mode, ai_settings, policy_grid, size):
         
     | 
| 186 | 
         
            +
                thr_line = ai_settings["threshold"] - 1  # zero-based
         
     | 
| 187 | 
         
            +
                if ai_mode == AI_INFLUENCE:
         
     | 
| 188 | 
         
            +
                    weight = lambda x, y: (1 / ai_settings["line_weight"]) ** (  # noqa E731
         
     | 
| 189 | 
         
            +
                        max(0, thr_line - min(size[0] - 1 - x, x)) + max(0, thr_line - min(size[1] - 1 - y, y))
         
     | 
| 190 | 
         
            +
                    )  # noqa E731
         
     | 
| 191 | 
         
            +
                else:
         
     | 
| 192 | 
         
            +
                    weight = lambda x, y: (1 / ai_settings["line_weight"]) ** (  # noqa E731
         
     | 
| 193 | 
         
            +
                        max(0, min(size[0] - 1 - x, x, size[1] - 1 - y, y) - thr_line)
         
     | 
| 194 | 
         
            +
                    )
         
     | 
| 195 | 
         
            +
                weighted_coords = [
         
     | 
| 196 | 
         
            +
                    (policy_grid[y][x] * weight(x, y), weight(x, y), x, y)
         
     | 
| 197 | 
         
            +
                    for x in range(size[0])
         
     | 
| 198 | 
         
            +
                    for y in range(size[1])
         
     | 
| 199 | 
         
            +
                    if policy_grid[y][x] > 0
         
     | 
| 200 | 
         
            +
                ]
         
     | 
| 201 | 
         
            +
                ai_thoughts = f"Generated weights for {ai_mode} according to weight factor {ai_settings['line_weight']} and distance from {thr_line + 1}th line. "
         
     | 
| 202 | 
         
            +
                return weighted_coords, ai_thoughts
         
     | 
| 203 | 
         
            +
             
     | 
| 204 | 
         
            +
             
     | 
| 205 | 
         
            +
            def generate_local_tenuki_weights(ai_mode, ai_settings, policy_grid, cn, size):
         
     | 
| 206 | 
         
            +
                var = ai_settings["stddev"] ** 2
         
     | 
| 207 | 
         
            +
                mx, my = cn.move.coords
         
     | 
| 208 | 
         
            +
                weighted_coords = [
         
     | 
| 209 | 
         
            +
                    (policy_grid[y][x], math.exp(-0.5 * ((x - mx) ** 2 + (y - my) ** 2) / var), x, y)
         
     | 
| 210 | 
         
            +
                    for x in range(size[0])
         
     | 
| 211 | 
         
            +
                    for y in range(size[1])
         
     | 
| 212 | 
         
            +
                    if policy_grid[y][x] > 0
         
     | 
| 213 | 
         
            +
                ]
         
     | 
| 214 | 
         
            +
                ai_thoughts = f"Generated weights based on one minus gaussian with variance {var} around coordinates {mx},{my}. "
         
     | 
| 215 | 
         
            +
                if ai_mode == AI_TENUKI:
         
     | 
| 216 | 
         
            +
                    weighted_coords = [(p, 1 - w, x, y) for p, w, x, y in weighted_coords]
         
     | 
| 217 | 
         
            +
                    ai_thoughts = (
         
     | 
| 218 | 
         
            +
                        f"Generated weights based on one minus gaussian with variance {var} around coordinates {mx},{my}. "
         
     | 
| 219 | 
         
            +
                    )
         
     | 
| 220 | 
         
            +
                return weighted_coords, ai_thoughts
         
     | 
| 221 | 
         
            +
             
     | 
| 222 | 
         
            +
             
     | 
| 223 | 
         
            +
            def request_ai_analysis(game: Game, cn: GameNode, extra_settings: Dict) -> Optional[Dict]:
         
     | 
| 224 | 
         
            +
                error = False
         
     | 
| 225 | 
         
            +
                analysis = None
         
     | 
| 226 | 
         
            +
             
     | 
| 227 | 
         
            +
                def set_analysis(a, partial_result):
         
     | 
| 228 | 
         
            +
                    nonlocal analysis
         
     | 
| 229 | 
         
            +
                    if not partial_result:
         
     | 
| 230 | 
         
            +
                        analysis = a
         
     | 
| 231 | 
         
            +
             
     | 
| 232 | 
         
            +
                def set_error(a):
         
     | 
| 233 | 
         
            +
                    nonlocal error
         
     | 
| 234 | 
         
            +
                    game.katrain.log(f"Error in additional analysis query: {a}")
         
     | 
| 235 | 
         
            +
                    error = True
         
     | 
| 236 | 
         
            +
             
     | 
| 237 | 
         
            +
                engine = game.engines[cn.player]
         
     | 
| 238 | 
         
            +
                engine.request_analysis(
         
     | 
| 239 | 
         
            +
                    cn,
         
     | 
| 240 | 
         
            +
                    callback=set_analysis,
         
     | 
| 241 | 
         
            +
                    error_callback=set_error,
         
     | 
| 242 | 
         
            +
                    priority=PRIORITY_EXTRA_AI_QUERY,
         
     | 
| 243 | 
         
            +
                    ownership=False,
         
     | 
| 244 | 
         
            +
                    extra_settings=extra_settings,
         
     | 
| 245 | 
         
            +
                )
         
     | 
| 246 | 
         
            +
                while not (error or analysis):
         
     | 
| 247 | 
         
            +
                    time.sleep(0.01)  # TODO: prevent deadlock if esc, check node in queries?
         
     | 
| 248 | 
         
            +
                    engine.check_alive(exception_if_dead=True)
         
     | 
| 249 | 
         
            +
                return analysis
         
     | 
| 250 | 
         
            +
             
     | 
| 251 | 
         
            +
             
     | 
| 252 | 
         
            +
            def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode]:
         
     | 
| 253 | 
         
            +
                cn = game.current_node
         
     | 
| 254 | 
         
            +
             
     | 
| 255 | 
         
            +
                if ai_mode == AI_HANDICAP:
         
     | 
| 256 | 
         
            +
                    pda = ai_settings["pda"]
         
     | 
| 257 | 
         
            +
                    if ai_settings["automatic"]:
         
     | 
| 258 | 
         
            +
                        n_handicaps = len(game.root.get_list_property("AB", []))
         
     | 
| 259 | 
         
            +
                        MOVE_VALUE = 14  # could be rules dependent
         
     | 
| 260 | 
         
            +
                        b_stones_advantage = max(n_handicaps - 1, 0) - (cn.komi - MOVE_VALUE / 2) / MOVE_VALUE
         
     | 
| 261 | 
         
            +
                        pda = min(3, max(-3, -b_stones_advantage * (3 / 8)))  # max PDA at 8 stone adv, normal 9 stone game is 8.46
         
     | 
| 262 | 
         
            +
                    handicap_analysis = request_ai_analysis(
         
     | 
| 263 | 
         
            +
                        game, cn, {"playoutDoublingAdvantage": pda, "playoutDoublingAdvantagePla": "BLACK"}
         
     | 
| 264 | 
         
            +
                    )
         
     | 
| 265 | 
         
            +
                    if not handicap_analysis:
         
     | 
| 266 | 
         
            +
                        game.katrain.log("Error getting handicap-based move", OUTPUT_ERROR)
         
     | 
| 267 | 
         
            +
                        ai_mode = AI_DEFAULT
         
     | 
| 268 | 
         
            +
                elif ai_mode == AI_ANTIMIRROR:
         
     | 
| 269 | 
         
            +
                    antimirror_analysis = request_ai_analysis(game, cn, {"antiMirror": True})
         
     | 
| 270 | 
         
            +
                    if not antimirror_analysis:
         
     | 
| 271 | 
         
            +
                        game.katrain.log("Error getting antimirror move", OUTPUT_ERROR)
         
     | 
| 272 | 
         
            +
                        ai_mode = AI_DEFAULT
         
     | 
| 273 | 
         
            +
             
     | 
| 274 | 
         
            +
                while not cn.analysis_complete:
         
     | 
| 275 | 
         
            +
                    time.sleep(0.01)
         
     | 
| 276 | 
         
            +
                    game.engines[cn.next_player].check_alive(exception_if_dead=True)
         
     | 
| 277 | 
         
            +
             
     | 
| 278 | 
         
            +
                ai_thoughts = ""
         
     | 
| 279 | 
         
            +
                if (ai_mode in AI_STRATEGIES_POLICY) and cn.policy:  # pure policy based move
         
     | 
| 280 | 
         
            +
                    policy_moves = cn.policy_ranking
         
     | 
| 281 | 
         
            +
                    pass_policy = cn.policy[-1]
         
     | 
| 282 | 
         
            +
                    # dont make it jump around for the last few sensible non pass moves
         
     | 
| 283 | 
         
            +
                    top_5_pass = any([polmove[1].is_pass for polmove in policy_moves[:5]])
         
     | 
| 284 | 
         
            +
             
     | 
| 285 | 
         
            +
                    size = game.board_size
         
     | 
| 286 | 
         
            +
                    policy_grid = var_to_grid(cn.policy, size)  # type: List[List[float]]
         
     | 
| 287 | 
         
            +
                    top_policy_move = policy_moves[0][1]
         
     | 
| 288 | 
         
            +
                    ai_thoughts += f"Using policy based strategy, base top 5 moves are {fmt_moves(policy_moves[:5])}. "
         
     | 
| 289 | 
         
            +
                    if (ai_mode == AI_POLICY and cn.depth <= ai_settings["opening_moves"]) or (
         
     | 
| 290 | 
         
            +
                        ai_mode in [AI_LOCAL, AI_TENUKI] and not (cn.move and cn.move.coords)
         
     | 
| 291 | 
         
            +
                    ):
         
     | 
| 292 | 
         
            +
                        ai_mode = AI_WEIGHTED
         
     | 
| 293 | 
         
            +
                        ai_thoughts += "Strategy override, using policy-weighted strategy instead. "
         
     | 
| 294 | 
         
            +
                        ai_settings = {"pick_override": 0.9, "weaken_fac": 1, "lower_bound": 0.02}
         
     | 
| 295 | 
         
            +
             
     | 
| 296 | 
         
            +
                    if top_5_pass:
         
     | 
| 297 | 
         
            +
                        aimove = top_policy_move
         
     | 
| 298 | 
         
            +
                        ai_thoughts += "Playing top one because one of them is pass."
         
     | 
| 299 | 
         
            +
                    elif ai_mode == AI_POLICY:
         
     | 
| 300 | 
         
            +
                        aimove = top_policy_move
         
     | 
| 301 | 
         
            +
                        ai_thoughts += f"Playing top policy move {aimove.gtp()}."
         
     | 
| 302 | 
         
            +
                    else:  # weighted or pick-based
         
     | 
| 303 | 
         
            +
                        legal_policy_moves = [(pol, mv) for pol, mv in policy_moves if not mv.is_pass and pol > 0]
         
     | 
| 304 | 
         
            +
                        board_squares = size[0] * size[1]
         
     | 
| 305 | 
         
            +
                        if ai_mode == AI_RANK:  # calibrated, override from 0.8 at start to ~0.4 at full board
         
     | 
| 306 | 
         
            +
                            override = 0.8 * (1 - 0.5 * (board_squares - len(legal_policy_moves)) / board_squares)
         
     | 
| 307 | 
         
            +
                            overridetwo = 0.85 + max(0, 0.02 * (ai_settings["kyu_rank"] - 8))
         
     | 
| 308 | 
         
            +
                        else:
         
     | 
| 309 | 
         
            +
                            override = ai_settings["pick_override"]
         
     | 
| 310 | 
         
            +
                            overridetwo = 1.0
         
     | 
| 311 | 
         
            +
             
     | 
| 312 | 
         
            +
                        if policy_moves[0][0] > override:
         
     | 
| 313 | 
         
            +
                            aimove = top_policy_move
         
     | 
| 314 | 
         
            +
                            ai_thoughts += f"Top policy move has weight > {override:.1%}, so overriding other strategies."
         
     | 
| 315 | 
         
            +
                        elif policy_moves[0][0] + policy_moves[1][0] > overridetwo:
         
     | 
| 316 | 
         
            +
                            aimove = top_policy_move
         
     | 
| 317 | 
         
            +
                            ai_thoughts += (
         
     | 
| 318 | 
         
            +
                                f"Top two policy moves have cumulative weight > {overridetwo:.1%}, so overriding other strategies."
         
     | 
| 319 | 
         
            +
                            )
         
     | 
| 320 | 
         
            +
                        elif ai_mode == AI_WEIGHTED:
         
     | 
| 321 | 
         
            +
                            aimove, ai_thoughts = policy_weighted_move(
         
     | 
| 322 | 
         
            +
                                policy_moves, ai_settings["lower_bound"], ai_settings["weaken_fac"]
         
     | 
| 323 | 
         
            +
                            )
         
     | 
| 324 | 
         
            +
                        elif ai_mode in AI_STRATEGIES_PICK:
         
     | 
| 325 | 
         
            +
             
     | 
| 326 | 
         
            +
                            if ai_mode != AI_RANK:
         
     | 
| 327 | 
         
            +
                                n_moves = max(1, int(ai_settings["pick_frac"] * len(legal_policy_moves) + ai_settings["pick_n"]))
         
     | 
| 328 | 
         
            +
                            else:
         
     | 
| 329 | 
         
            +
                                orig_calib_avemodrank = 0.063015 + 0.7624 * board_squares / (
         
     | 
| 330 | 
         
            +
                                    10 ** (-0.05737 * ai_settings["kyu_rank"] + 1.9482)
         
     | 
| 331 | 
         
            +
                                )
         
     | 
| 332 | 
         
            +
                                norm_leg_moves = len(legal_policy_moves) / board_squares
         
     | 
| 333 | 
         
            +
                                modified_calib_avemodrank = (
         
     | 
| 334 | 
         
            +
                                    0.3931
         
     | 
| 335 | 
         
            +
                                    + 0.6559
         
     | 
| 336 | 
         
            +
                                    * norm_leg_moves
         
     | 
| 337 | 
         
            +
                                    * math.exp(
         
     | 
| 338 | 
         
            +
                                        -1
         
     | 
| 339 | 
         
            +
                                        * (
         
     | 
| 340 | 
         
            +
                                            3.002 * norm_leg_moves * norm_leg_moves
         
     | 
| 341 | 
         
            +
                                            - norm_leg_moves
         
     | 
| 342 | 
         
            +
                                            - 0.034889 * ai_settings["kyu_rank"]
         
     | 
| 343 | 
         
            +
                                            - 0.5097
         
     | 
| 344 | 
         
            +
                                        )
         
     | 
| 345 | 
         
            +
                                        ** 2
         
     | 
| 346 | 
         
            +
                                    )
         
     | 
| 347 | 
         
            +
                                    - 0.01093 * ai_settings["kyu_rank"]
         
     | 
| 348 | 
         
            +
                                ) * orig_calib_avemodrank
         
     | 
| 349 | 
         
            +
                                n_moves = board_squares * norm_leg_moves / (1.31165 * (modified_calib_avemodrank + 1) - 0.082653)
         
     | 
| 350 | 
         
            +
                                n_moves = max(1, round(n_moves))
         
     | 
| 351 | 
         
            +
             
     | 
| 352 | 
         
            +
                            if ai_mode in [AI_INFLUENCE, AI_TERRITORY, AI_LOCAL, AI_TENUKI]:
         
     | 
| 353 | 
         
            +
                                if cn.depth > ai_settings["endgame"] * board_squares:
         
     | 
| 354 | 
         
            +
                                    weighted_coords = [(pol, 1, *mv.coords) for pol, mv in legal_policy_moves]
         
     | 
| 355 | 
         
            +
                                    x_ai_thoughts = (
         
     | 
| 356 | 
         
            +
                                        f"Generated equal weights as move number >= {ai_settings['endgame'] * size[0] * size[1]}. "
         
     | 
| 357 | 
         
            +
                                    )
         
     | 
| 358 | 
         
            +
                                    n_moves = int(max(n_moves, len(legal_policy_moves) // 2))
         
     | 
| 359 | 
         
            +
                                elif ai_mode in [AI_INFLUENCE, AI_TERRITORY]:
         
     | 
| 360 | 
         
            +
                                    weighted_coords, x_ai_thoughts = generate_influence_territory_weights(
         
     | 
| 361 | 
         
            +
                                        ai_mode, ai_settings, policy_grid, size
         
     | 
| 362 | 
         
            +
                                    )
         
     | 
| 363 | 
         
            +
                                else:  # ai_mode in [AI_LOCAL, AI_TENUKI]
         
     | 
| 364 | 
         
            +
                                    weighted_coords, x_ai_thoughts = generate_local_tenuki_weights(
         
     | 
| 365 | 
         
            +
                                        ai_mode, ai_settings, policy_grid, cn, size
         
     | 
| 366 | 
         
            +
                                    )
         
     | 
| 367 | 
         
            +
                                ai_thoughts += x_ai_thoughts
         
     | 
| 368 | 
         
            +
                            else:  # ai_mode in [AI_PICK, AI_RANK]:
         
     | 
| 369 | 
         
            +
                                weighted_coords = [
         
     | 
| 370 | 
         
            +
                                    (policy_grid[y][x], 1, x, y)
         
     | 
| 371 | 
         
            +
                                    for x in range(size[0])
         
     | 
| 372 | 
         
            +
                                    for y in range(size[1])
         
     | 
| 373 | 
         
            +
                                    if policy_grid[y][x] > 0
         
     | 
| 374 | 
         
            +
                                ]
         
     | 
| 375 | 
         
            +
             
     | 
| 376 | 
         
            +
                            pick_moves = weighted_selection_without_replacement(weighted_coords, n_moves)
         
     | 
| 377 | 
         
            +
                            ai_thoughts += f"Picked {min(n_moves,len(weighted_coords))} random moves according to weights. "
         
     | 
| 378 | 
         
            +
             
     | 
| 379 | 
         
            +
                            if pick_moves:
         
     | 
| 380 | 
         
            +
                                new_top = [
         
     | 
| 381 | 
         
            +
                                    (p, Move((x, y), player=cn.next_player)) for p, wt, x, y in heapq.nlargest(5, pick_moves)
         
     | 
| 382 | 
         
            +
                                ]
         
     | 
| 383 | 
         
            +
                                aimove = new_top[0][1]
         
     | 
| 384 | 
         
            +
                                ai_thoughts += f"Top 5 among these were {fmt_moves(new_top)} and picked top {aimove.gtp()}. "
         
     | 
| 385 | 
         
            +
                                if new_top[0][0] < pass_policy:
         
     | 
| 386 | 
         
            +
                                    ai_thoughts += f"But found pass ({pass_policy:.2%} to be higher rated than {aimove.gtp()} ({new_top[0][0]:.2%}) so will play top policy move instead."
         
     | 
| 387 | 
         
            +
                                    aimove = top_policy_move
         
     | 
| 388 | 
         
            +
                            else:
         
     | 
| 389 | 
         
            +
                                aimove = top_policy_move
         
     | 
| 390 | 
         
            +
                                ai_thoughts += f"Pick policy strategy {ai_mode} failed to find legal moves, so is playing top policy move {aimove.gtp()}."
         
     | 
| 391 | 
         
            +
                        else:
         
     | 
| 392 | 
         
            +
                            raise ValueError(f"Unknown Policy-based AI mode {ai_mode}")
         
     | 
| 393 | 
         
            +
                else:  # Engine based move
         
     | 
| 394 | 
         
            +
                    candidate_ai_moves = cn.candidate_moves
         
     | 
| 395 | 
         
            +
                    if ai_mode == AI_HANDICAP:
         
     | 
| 396 | 
         
            +
                        candidate_ai_moves = handicap_analysis["moveInfos"]
         
     | 
| 397 | 
         
            +
                    elif ai_mode == AI_ANTIMIRROR:
         
     | 
| 398 | 
         
            +
                        candidate_ai_moves = antimirror_analysis["moveInfos"]
         
     | 
| 399 | 
         
            +
             
     | 
| 400 | 
         
            +
                    top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player)
         
     | 
| 401 | 
         
            +
                    if top_cand.is_pass and ai_mode not in [
         
     | 
| 402 | 
         
            +
                        AI_DEFAULT,
         
     | 
| 403 | 
         
            +
                        AI_HANDICAP,
         
     | 
| 404 | 
         
            +
                    ]:  # don't play suicidal to balance score
         
     | 
| 405 | 
         
            +
                        aimove = top_cand
         
     | 
| 406 | 
         
            +
                        ai_thoughts += "Top move is pass, so passing regardless of strategy. "
         
     | 
| 407 | 
         
            +
                    else:
         
     | 
| 408 | 
         
            +
                        if ai_mode == AI_JIGO:
         
     | 
| 409 | 
         
            +
                            sign = cn.player_sign(cn.next_player)
         
     | 
| 410 | 
         
            +
                            jigo_move = min(
         
     | 
| 411 | 
         
            +
                                candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings["target_score"])
         
     | 
| 412 | 
         
            +
                            )
         
     | 
| 413 | 
         
            +
                            aimove = Move.from_gtp(jigo_move["move"], player=cn.next_player)
         
     | 
| 414 | 
         
            +
                            ai_thoughts += f"Jigo strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} as closest to 0.5 point win"
         
     | 
| 415 | 
         
            +
                        elif ai_mode == AI_SCORELOSS:
         
     | 
| 416 | 
         
            +
                            c = ai_settings["strength"]
         
     | 
| 417 | 
         
            +
                            moves = [
         
     | 
| 418 | 
         
            +
                                (
         
     | 
| 419 | 
         
            +
                                    d["pointsLost"],
         
     | 
| 420 | 
         
            +
                                    math.exp(min(200, -c * max(0, d["pointsLost"]))),
         
     | 
| 421 | 
         
            +
                                    Move.from_gtp(d["move"], player=cn.next_player),
         
     | 
| 422 | 
         
            +
                                )
         
     | 
| 423 | 
         
            +
                                for d in candidate_ai_moves
         
     | 
| 424 | 
         
            +
                            ]
         
     | 
| 425 | 
         
            +
                            topmove = weighted_selection_without_replacement(moves, 1)[0]
         
     | 
| 426 | 
         
            +
                            aimove = topmove[2]
         
     | 
| 427 | 
         
            +
                            ai_thoughts += f"ScoreLoss strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} (weight {topmove[1]:.3f}, point loss {topmove[0]:.1f}) based on score weights."
         
     | 
| 428 | 
         
            +
                        elif ai_mode in [AI_SIMPLE_OWNERSHIP, AI_SETTLE_STONES]:
         
     | 
| 429 | 
         
            +
                            stones_with_player = {(*s.coords, s.player) for s in game.stones}
         
     | 
| 430 | 
         
            +
                            next_player_sign = cn.player_sign(cn.next_player)
         
     | 
| 431 | 
         
            +
                            if ai_mode == AI_SIMPLE_OWNERSHIP:
         
     | 
| 432 | 
         
            +
             
     | 
| 433 | 
         
            +
                                def settledness(d, player_sign, player):
         
     | 
| 434 | 
         
            +
                                    return sum([abs(o) for o in d["ownership"] if player_sign * o > 0])
         
     | 
| 435 | 
         
            +
             
     | 
| 436 | 
         
            +
                            else:
         
     | 
| 437 | 
         
            +
                                board_size_x, board_size_y = game.board_size
         
     | 
| 438 | 
         
            +
             
     | 
| 439 | 
         
            +
                                def settledness(d, player_sign, player):
         
     | 
| 440 | 
         
            +
                                    ownership_grid = var_to_grid(d["ownership"], (board_size_x, board_size_y))
         
     | 
| 441 | 
         
            +
                                    return sum(
         
     | 
| 442 | 
         
            +
                                        [abs(ownership_grid[s.coords[0]][s.coords[1]]) for s in game.stones if s.player == player]
         
     | 
| 443 | 
         
            +
                                    )
         
     | 
| 444 | 
         
            +
             
     | 
| 445 | 
         
            +
                            def is_attachment(move):
         
     | 
| 446 | 
         
            +
                                if move.is_pass:
         
     | 
| 447 | 
         
            +
                                    return False
         
     | 
| 448 | 
         
            +
                                attach_opponent_stones = sum(
         
     | 
| 449 | 
         
            +
                                    (move.coords[0] + dx, move.coords[1] + dy, cn.player) in stones_with_player
         
     | 
| 450 | 
         
            +
                                    for dx in [-1, 0, 1]
         
     | 
| 451 | 
         
            +
                                    for dy in [-1, 0, 1]
         
     | 
| 452 | 
         
            +
                                    if abs(dx) + abs(dy) == 1
         
     | 
| 453 | 
         
            +
                                )
         
     | 
| 454 | 
         
            +
                                nearby_own_stones = sum(
         
     | 
| 455 | 
         
            +
                                    (move.coords[0] + dx, move.coords[1] + dy, cn.next_player) in stones_with_player
         
     | 
| 456 | 
         
            +
                                    for dx in [-2, 0, 1, 2]
         
     | 
| 457 | 
         
            +
                                    for dy in [-2 - 1, 0, 1, 2]
         
     | 
| 458 | 
         
            +
                                    if abs(dx) + abs(dy) <= 2  # allows clamps/jumps
         
     | 
| 459 | 
         
            +
                                )
         
     | 
| 460 | 
         
            +
                                return attach_opponent_stones >= 1 and nearby_own_stones == 0
         
     | 
| 461 | 
         
            +
             
     | 
| 462 | 
         
            +
                            def is_tenuki(d):
         
     | 
| 463 | 
         
            +
                                return not d.is_pass and not any(
         
     | 
| 464 | 
         
            +
                                    not node
         
     | 
| 465 | 
         
            +
                                    or not node.move
         
     | 
| 466 | 
         
            +
                                    or node.move.is_pass
         
     | 
| 467 | 
         
            +
                                    or max(abs(last_c - cand_c) for last_c, cand_c in zip(node.move.coords, d.coords)) < 5
         
     | 
| 468 | 
         
            +
                                    for node in [cn, cn.parent]
         
     | 
| 469 | 
         
            +
                                )
         
     | 
| 470 | 
         
            +
             
     | 
| 471 | 
         
            +
                            moves_with_settledness = sorted(
         
     | 
| 472 | 
         
            +
                                [
         
     | 
| 473 | 
         
            +
                                    (
         
     | 
| 474 | 
         
            +
                                        move,
         
     | 
| 475 | 
         
            +
                                        settledness(d, next_player_sign, cn.next_player),
         
     | 
| 476 | 
         
            +
                                        settledness(d, -next_player_sign, cn.player),
         
     | 
| 477 | 
         
            +
                                        is_attachment(move),
         
     | 
| 478 | 
         
            +
                                        is_tenuki(move),
         
     | 
| 479 | 
         
            +
                                        d,
         
     | 
| 480 | 
         
            +
                                    )
         
     | 
| 481 | 
         
            +
                                    for d in candidate_ai_moves
         
     | 
| 482 | 
         
            +
                                    if d["pointsLost"] < ai_settings["max_points_lost"]
         
     | 
| 483 | 
         
            +
                                    and "ownership" in d
         
     | 
| 484 | 
         
            +
                                    and (d["order"] <= 1 or d["visits"] >= ai_settings.get("min_visits", 1))
         
     | 
| 485 | 
         
            +
                                    for move in [Move.from_gtp(d["move"], player=cn.next_player)]
         
     | 
| 486 | 
         
            +
                                    if not (move.is_pass and d["pointsLost"] > 0.75)
         
     | 
| 487 | 
         
            +
                                ],
         
     | 
| 488 | 
         
            +
                                key=lambda t: t[5]["pointsLost"]
         
     | 
| 489 | 
         
            +
                                + ai_settings["attach_penalty"] * t[3]
         
     | 
| 490 | 
         
            +
                                + ai_settings["tenuki_penalty"] * t[4]
         
     | 
| 491 | 
         
            +
                                - ai_settings["settled_weight"] * (t[1] + ai_settings["opponent_fac"] * t[2]),
         
     | 
| 492 | 
         
            +
                            )
         
     | 
| 493 | 
         
            +
                            if moves_with_settledness:
         
     | 
| 494 | 
         
            +
                                cands = [
         
     | 
| 495 | 
         
            +
                                    f"{move.gtp()} ({d['pointsLost']:.1f} pt lost, {d['visits']} visits, {settled:.1f} settledness, {oppsettled:.1f} opponent settledness{', attachment' if isattach else ''}{', tenuki' if istenuki else ''})"
         
     | 
| 496 | 
         
            +
                                    for move, settled, oppsettled, isattach, istenuki, d in moves_with_settledness[:5]
         
     | 
| 497 | 
         
            +
                                ]
         
     | 
| 498 | 
         
            +
                                ai_thoughts += f"{ai_mode} strategy. Top 5 Candidates {', '.join(cands)} "
         
     | 
| 499 | 
         
            +
                                aimove = moves_with_settledness[0][0]
         
     | 
| 500 | 
         
            +
                            else:
         
     | 
| 501 | 
         
            +
                                raise (Exception("No moves found - are you using an older KataGo with no per-move ownership info?"))
         
     | 
| 502 | 
         
            +
                        else:
         
     | 
| 503 | 
         
            +
                            if ai_mode not in [AI_DEFAULT, AI_HANDICAP, AI_ANTIMIRROR]:
         
     | 
| 504 | 
         
            +
                                game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO)
         
     | 
| 505 | 
         
            +
                                ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback."
         
     | 
| 506 | 
         
            +
                            aimove = top_cand
         
     | 
| 507 | 
         
            +
                            if ai_mode == AI_HANDICAP:
         
     | 
| 508 | 
         
            +
                                ai_thoughts += f"Handicap strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move. PDA based score {cn.format_score(handicap_analysis['rootInfo']['scoreLead'])} and win rate {cn.format_winrate(handicap_analysis['rootInfo']['winrate'])}"
         
     | 
| 509 | 
         
            +
                            if ai_mode == AI_ANTIMIRROR:
         
     | 
| 510 | 
         
            +
                                ai_thoughts += f"AntiMirror strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move. antiMirror based score {cn.format_score(antimirror_analysis['rootInfo']['scoreLead'])} and win rate {cn.format_winrate(antimirror_analysis['rootInfo']['winrate'])}"
         
     | 
| 511 | 
         
            +
                            else:
         
     | 
| 512 | 
         
            +
                                ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move"
         
     | 
| 513 | 
         
            +
                game.katrain.log(f"AI thoughts: {ai_thoughts}", OUTPUT_DEBUG)
         
     | 
| 514 | 
         
            +
                played_node = game.play(aimove)
         
     | 
| 515 | 
         
            +
                played_node.ai_thoughts = ai_thoughts
         
     | 
| 516 | 
         
            +
                return aimove, played_node
         
     | 
    	
        katrain/katrain/core/base_katrain.py
    ADDED
    
    | 
         @@ -0,0 +1,96 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            # base_katrain.py (진짜 최종 완성본)
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
            import json
         
     | 
| 4 | 
         
            +
            import os
         
     | 
| 5 | 
         
            +
            from configparser import ConfigParser, NoSectionError, NoOptionError
         
     | 
| 6 | 
         
            +
             
     | 
| 7 | 
         
            +
            # --- 필요한 모든 부품을 정확하게 import ---
         
     | 
| 8 | 
         
            +
            from katrain.core.constants import *
         
     | 
| 9 | 
         
            +
            from katrain.core.lang import i18n
         
     | 
| 10 | 
         
            +
            from katrain.core.utils import PATHS
         
     | 
| 11 | 
         
            +
            from katrain.core.ai import ai_rank_estimation
         
     | 
| 12 | 
         
            +
             
     | 
| 13 | 
         
            +
            class KaTrainBase:
         
     | 
| 14 | 
         
            +
                """KaTrain의 GUI와 엔진 로직 사이에 공유되는 기본 클래스"""
         
     | 
| 15 | 
         
            +
             
     | 
| 16 | 
         
            +
                def __init__(self, katrain_app, config: ConfigParser):
         
     | 
| 17 | 
         
            +
                    self.katrain = katrain_app
         
     | 
| 18 | 
         
            +
                    self._config = config
         
     | 
| 19 | 
         
            +
                    self.players_info = {}
         
     | 
| 20 | 
         
            +
                    self.debug_level = self.config("general/debug_level", 0, int)
         
     | 
| 21 | 
         
            +
             
     | 
| 22 | 
         
            +
                    for bw in "BW":
         
     | 
| 23 | 
         
            +
                        self.players_info[bw] = self.PlayerInfo(bw, self)
         
     | 
| 24 | 
         
            +
             
     | 
| 25 | 
         
            +
                def config(self, key, default=None, vartype=str):
         
     | 
| 26 | 
         
            +
                    try:
         
     | 
| 27 | 
         
            +
                        if "/" in key:
         
     | 
| 28 | 
         
            +
                            parts = key.split("/")
         
     | 
| 29 | 
         
            +
                            section = parts[0]
         
     | 
| 30 | 
         
            +
                            name = "/".join(parts[1:])
         
     | 
| 31 | 
         
            +
                            if vartype == bool:
         
     | 
| 32 | 
         
            +
                                return self._config.getboolean(section, name)
         
     | 
| 33 | 
         
            +
                            if vartype == int:
         
     | 
| 34 | 
         
            +
                                return self._config.getint(section, name)
         
     | 
| 35 | 
         
            +
                            return self._config.get(section, name)
         
     | 
| 36 | 
         
            +
                        else: # section만 요청된 경우
         
     | 
| 37 | 
         
            +
                            return dict(self._config.items(key))
         
     | 
| 38 | 
         
            +
                    except (ValueError, KeyError, NoSectionError, NoOptionError):
         
     | 
| 39 | 
         
            +
                        return default if "/" in key else (default or {})
         
     | 
| 40 | 
         
            +
             
     | 
| 41 | 
         
            +
                def save_config(self, sections=None):
         
     | 
| 42 | 
         
            +
                    config_path = os.path.join(PATHS["USER"], "config.json")
         
     | 
| 43 | 
         
            +
                    save_sections = [sections] if isinstance(sections, str) else (sections or self._config.sections())
         
     | 
| 44 | 
         
            +
             
     | 
| 45 | 
         
            +
                    output_dict = {}
         
     | 
| 46 | 
         
            +
                    for s in save_sections:
         
     | 
| 47 | 
         
            +
                        if self._config.has_section(s):
         
     | 
| 48 | 
         
            +
                            output_dict[s] = dict(self._config.items(s))
         
     | 
| 49 | 
         
            +
             
     | 
| 50 | 
         
            +
                    with open(config_path, "w") as f:
         
     | 
| 51 | 
         
            +
                        json.dump(output_dict, f, indent=4)
         
     | 
| 52 | 
         
            +
                    
         
     | 
| 53 | 
         
            +
                def log(self, message, level=OUTPUT_INFO):
         
     | 
| 54 | 
         
            +
                    if self.debug_level is not None and level <= self.debug_level:
         
     | 
| 55 | 
         
            +
                        print(message)
         
     | 
| 56 | 
         
            +
             
     | 
| 57 | 
         
            +
                def update_player(self, bw, **kwargs):
         
     | 
| 58 | 
         
            +
                    self.players_info[bw].update(**kwargs)
         
     | 
| 59 | 
         
            +
                    self.save_config("players")
         
     | 
| 60 | 
         
            +
                    self.update_calculated_ranks()
         
     | 
| 61 | 
         
            +
                    
         
     | 
| 62 | 
         
            +
                def update_calculated_ranks(self):
         
     | 
| 63 | 
         
            +
                    for bw, player_info in self.players_info.items():
         
     | 
| 64 | 
         
            +
                        if player_info.player_type == PLAYER_AI:
         
     | 
| 65 | 
         
            +
                            settings = {"komi": self.config("game/komi"), "rules": self.config("game/rules")}
         
     | 
| 66 | 
         
            +
                            player_info.calculated_rank = ai_rank_estimation(player_info.player_subtype, settings)
         
     | 
| 67 | 
         
            +
             
     | 
| 68 | 
         
            +
                @property
         
     | 
| 69 | 
         
            +
                def next_player_info(self):
         
     | 
| 70 | 
         
            +
                    if hasattr(self, 'game') and self.game and self.game.current_node:
         
     | 
| 71 | 
         
            +
                        return self.players_info[self.game.current_node.next_player]
         
     | 
| 72 | 
         
            +
                    return self.players_info["B"]
         
     | 
| 73 | 
         
            +
             
     | 
| 74 | 
         
            +
                @property
         
     | 
| 75 | 
         
            +
                def last_player_info(self):
         
     | 
| 76 | 
         
            +
                    if hasattr(self, 'game') and self.game and self.game.current_node and self.game.current_node.player:
         
     | 
| 77 | 
         
            +
                        return self.players_info[self.game.current_node.player]
         
     | 
| 78 | 
         
            +
                    return self.players_info["W"]
         
     | 
| 79 | 
         
            +
             
     | 
| 80 | 
         
            +
                class PlayerInfo:
         
     | 
| 81 | 
         
            +
                    def __init__(self, bw, katrain_base):
         
     | 
| 82 | 
         
            +
                        self.bw = bw
         
     | 
| 83 | 
         
            +
                        self.katrain_base = katrain_base
         
     | 
| 84 | 
         
            +
                        self.name = katrain_base.config(f"players/{bw}/name", None)
         
     | 
| 85 | 
         
            +
                        self.player_type = katrain_base.config(f"players/{bw}/type", PLAYER_HUMAN)
         
     | 
| 86 | 
         
            +
                        self.player_subtype = katrain_base.config(f"players/{bw}/subtype", AI_DEFAULT)
         
     | 
| 87 | 
         
            +
                        self.sgf_rank = None
         
     | 
| 88 | 
         
            +
                        self.calculated_rank = None
         
     | 
| 89 | 
         
            +
                        
         
     | 
| 90 | 
         
            +
                    def update(self, **kwargs):
         
     | 
| 91 | 
         
            +
                        for k, v in kwargs.items():
         
     | 
| 92 | 
         
            +
                            if hasattr(self, k):
         
     | 
| 93 | 
         
            +
                                setattr(self, k, v)
         
     | 
| 94 | 
         
            +
                                if not self.katrain_base._config.has_section("players"):
         
     | 
| 95 | 
         
            +
                                    self.katrain_base._config.add_section("players")
         
     | 
| 96 | 
         
            +
                                self.katrain_base._config.set("players", f"{self.bw}/{k}", str(v))
         
     | 
    	
        katrain/katrain/core/constants.py
    ADDED
    
    | 
         @@ -0,0 +1,272 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            PROGRAM_NAME = "KaTrain"
         
     | 
| 2 | 
         
            +
            VERSION = "1.13.0"
         
     | 
| 3 | 
         
            +
            HOMEPAGE = "https://github.com/sanderland/katrain"
         
     | 
| 4 | 
         
            +
            CONFIG_MIN_VERSION = "1.11.0"  # keep config files from this version
         
     | 
| 5 | 
         
            +
            ANALYSIS_FORMAT_VERSION = "1.0"
         
     | 
| 6 | 
         
            +
            DATA_FOLDER = "~/.katrain"
         
     | 
| 7 | 
         
            +
             
     | 
| 8 | 
         
            +
             
     | 
| 9 | 
         
            +
            OUTPUT_ERROR = -1
         
     | 
| 10 | 
         
            +
            OUTPUT_HONGIK_STDERR = -0.5
         
     | 
| 11 | 
         
            +
            OUTPUT_INFO = 0
         
     | 
| 12 | 
         
            +
            OUTPUT_DEBUG = 1
         
     | 
| 13 | 
         
            +
            OUTPUT_EXTRA_DEBUG = 2
         
     | 
| 14 | 
         
            +
             
     | 
| 15 | 
         
            +
            HONGIK_EXCEPTION = "KATAGO-INTERNAL-ERROR"
         
     | 
| 16 | 
         
            +
             
     | 
| 17 | 
         
            +
            STATUS_ANALYSIS = 1.0  # same priority for analysis/info
         
     | 
| 18 | 
         
            +
            STATUS_INFO = 1.1
         
     | 
| 19 | 
         
            +
            STATUS_TEACHING = 2.0
         
     | 
| 20 | 
         
            +
            STATUS_ERROR = 1000.0
         
     | 
| 21 | 
         
            +
             
     | 
| 22 | 
         
            +
            ADDITIONAL_MOVE_ORDER = 999
         
     | 
| 23 | 
         
            +
             
     | 
| 24 | 
         
            +
            PRIORITY_GAME_ANALYSIS = -100
         
     | 
| 25 | 
         
            +
            PRIORITY_SWEEP = -10  # sweep is live, but slow, so deprioritize
         
     | 
| 26 | 
         
            +
            PRIORITY_ALTERNATIVES = 100  # extra analysis, live interaction
         
     | 
| 27 | 
         
            +
            PRIORITY_EQUALIZE = 100
         
     | 
| 28 | 
         
            +
            PRIORITY_EXTRA_ANALYSIS = 100
         
     | 
| 29 | 
         
            +
            PRIORITY_DEFAULT = 1000  # new move, high pri
         
     | 
| 30 | 
         
            +
            PRIORITY_EXTRA_AI_QUERY = 10_000
         
     | 
| 31 | 
         
            +
             
     | 
| 32 | 
         
            +
            PLAYER_HUMAN, PLAYER_AI = "player:human", "player:ai"
         
     | 
| 33 | 
         
            +
            PLAYER_TYPES = [PLAYER_HUMAN, PLAYER_AI]
         
     | 
| 34 | 
         
            +
             
     | 
| 35 | 
         
            +
            PLAYING_NORMAL, PLAYING_TEACHING = "game:normal", "game:teach"
         
     | 
| 36 | 
         
            +
            GAME_TYPES = [PLAYING_NORMAL, PLAYING_TEACHING]
         
     | 
| 37 | 
         
            +
             
     | 
| 38 | 
         
            +
            PLAYER_BLACK = "B"
         
     | 
| 39 | 
         
            +
            PLAYER_WHITE = "W"
         
     | 
| 40 | 
         
            +
             
     | 
| 41 | 
         
            +
            MODE_PLAY, MODE_ANALYZE = "play", "analyze"
         
     | 
| 42 | 
         
            +
             
     | 
| 43 | 
         
            +
            AI_DEFAULT = "ai:default"
         
     | 
| 44 | 
         
            +
            AI_HANDICAP = "ai:handicap"
         
     | 
| 45 | 
         
            +
            AI_SCORELOSS = "ai:scoreloss"
         
     | 
| 46 | 
         
            +
            AI_WEIGHTED = "ai:p:weighted"
         
     | 
| 47 | 
         
            +
            AI_JIGO = "ai:jigo"
         
     | 
| 48 | 
         
            +
            AI_ANTIMIRROR = "ai:antimirror"
         
     | 
| 49 | 
         
            +
            AI_POLICY = "ai:policy"
         
     | 
| 50 | 
         
            +
            AI_PICK = "ai:p:pick"
         
     | 
| 51 | 
         
            +
            AI_LOCAL = "ai:p:local"
         
     | 
| 52 | 
         
            +
            AI_TENUKI = "ai:p:tenuki"
         
     | 
| 53 | 
         
            +
            AI_INFLUENCE = "ai:p:influence"
         
     | 
| 54 | 
         
            +
            AI_TERRITORY = "ai:p:territory"
         
     | 
| 55 | 
         
            +
            AI_RANK = "ai:p:rank"
         
     | 
| 56 | 
         
            +
            AI_SIMPLE_OWNERSHIP = "ai:simple"
         
     | 
| 57 | 
         
            +
            AI_SETTLE_STONES = "ai:settle"
         
     | 
| 58 | 
         
            +
             
     | 
| 59 | 
         
            +
            AI_CONFIG_DEFAULT = AI_RANK
         
     | 
| 60 | 
         
            +
             
     | 
| 61 | 
         
            +
            AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SCORELOSS, AI_SIMPLE_OWNERSHIP, AI_JIGO, AI_ANTIMIRROR]
         
     | 
| 62 | 
         
            +
            AI_STRATEGIES_PICK = [AI_PICK, AI_LOCAL, AI_TENUKI, AI_INFLUENCE, AI_TERRITORY, AI_RANK]
         
     | 
| 63 | 
         
            +
            AI_STRATEGIES_POLICY = [AI_WEIGHTED, AI_POLICY] + AI_STRATEGIES_PICK
         
     | 
| 64 | 
         
            +
            AI_STRATEGIES = AI_STRATEGIES_ENGINE + AI_STRATEGIES_POLICY
         
     | 
| 65 | 
         
            +
            AI_STRATEGIES_RECOMMENDED_ORDER = [
         
     | 
| 66 | 
         
            +
                AI_DEFAULT,
         
     | 
| 67 | 
         
            +
                AI_RANK,
         
     | 
| 68 | 
         
            +
                AI_HANDICAP,
         
     | 
| 69 | 
         
            +
                AI_SIMPLE_OWNERSHIP,
         
     | 
| 70 | 
         
            +
                AI_SCORELOSS,
         
     | 
| 71 | 
         
            +
                AI_POLICY,
         
     | 
| 72 | 
         
            +
                AI_WEIGHTED,
         
     | 
| 73 | 
         
            +
                AI_JIGO,
         
     | 
| 74 | 
         
            +
                AI_ANTIMIRROR,
         
     | 
| 75 | 
         
            +
                AI_PICK,
         
     | 
| 76 | 
         
            +
                AI_LOCAL,
         
     | 
| 77 | 
         
            +
                AI_TENUKI,
         
     | 
| 78 | 
         
            +
                AI_TERRITORY,
         
     | 
| 79 | 
         
            +
                AI_INFLUENCE,
         
     | 
| 80 | 
         
            +
            ]
         
     | 
| 81 | 
         
            +
             
     | 
| 82 | 
         
            +
            AI_STRENGTH = {  # dan ranks, backup if model is missing. TODO: remove some?
         
     | 
| 83 | 
         
            +
                AI_DEFAULT: 9,
         
     | 
| 84 | 
         
            +
                AI_ANTIMIRROR: 9,
         
     | 
| 85 | 
         
            +
                AI_POLICY: 5,
         
     | 
| 86 | 
         
            +
                AI_JIGO: float("nan"),
         
     | 
| 87 | 
         
            +
                AI_SCORELOSS: -4,
         
     | 
| 88 | 
         
            +
                AI_WEIGHTED: -4,
         
     | 
| 89 | 
         
            +
                AI_PICK: -7,
         
     | 
| 90 | 
         
            +
                AI_LOCAL: -4,
         
     | 
| 91 | 
         
            +
                AI_TENUKI: -7,
         
     | 
| 92 | 
         
            +
                AI_INFLUENCE: -7,
         
     | 
| 93 | 
         
            +
                AI_TERRITORY: -7,
         
     | 
| 94 | 
         
            +
                AI_RANK: float("nan"),
         
     | 
| 95 | 
         
            +
                AI_SIMPLE_OWNERSHIP: 2,
         
     | 
| 96 | 
         
            +
                AI_SETTLE_STONES: 2,
         
     | 
| 97 | 
         
            +
            }
         
     | 
| 98 | 
         
            +
             
     | 
| 99 | 
         
            +
            AI_OPTION_VALUES = {
         
     | 
| 100 | 
         
            +
                "kyu_rank": [(k, f"{k}[strength:kyu]") for k in range(15, 0, -1)]
         
     | 
| 101 | 
         
            +
                + [(k, f"{1-k}[strength:dan]") for k in range(0, -3, -1)],
         
     | 
| 102 | 
         
            +
                "strength": [0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5, 1],
         
     | 
| 103 | 
         
            +
                "opening_moves": range(0, 51),
         
     | 
| 104 | 
         
            +
                "pick_override": [0, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 0.99, 1],
         
     | 
| 105 | 
         
            +
                "lower_bound": [(v, f"{v:.2%}") for v in [0, 0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05]],
         
     | 
| 106 | 
         
            +
                "weaken_fac": [x / 20 for x in range(10, 3 * 20 + 1)],
         
     | 
| 107 | 
         
            +
                "endgame": [x / 100 for x in range(10, 80, 5)],
         
     | 
| 108 | 
         
            +
                "pick_frac": [x / 100 for x in range(0, 101, 5)],
         
     | 
| 109 | 
         
            +
                "pick_n": range(0, 26),
         
     | 
| 110 | 
         
            +
                "stddev": [x / 2 for x in range(21)],
         
     | 
| 111 | 
         
            +
                "line_weight": range(0, 11),
         
     | 
| 112 | 
         
            +
                "threshold": [2, 2.5, 3, 3.5, 4, 4.5],
         
     | 
| 113 | 
         
            +
                "automatic": "bool",
         
     | 
| 114 | 
         
            +
                "pda": [(x / 10, f"{'W' if x<0 else 'B'}+{abs(x/10):.1f}") for x in range(-30, 31)],
         
     | 
| 115 | 
         
            +
                "max_points_lost": [x / 10 for x in range(51)],
         
     | 
| 116 | 
         
            +
                "settled_weight": [x / 4 for x in range(0, 17)],
         
     | 
| 117 | 
         
            +
                "opponent_fac": [x / 10 for x in range(-20, 11)],
         
     | 
| 118 | 
         
            +
                "min_visits": range(1, 10),
         
     | 
| 119 | 
         
            +
                "attach_penalty": [x / 10 for x in range(-10, 51)],
         
     | 
| 120 | 
         
            +
                "tenuki_penalty": [x / 10 for x in range(-10, 51)],
         
     | 
| 121 | 
         
            +
            }
         
     | 
| 122 | 
         
            +
            AI_KEY_PROPERTIES = {
         
     | 
| 123 | 
         
            +
                "kyu_rank",
         
     | 
| 124 | 
         
            +
                "strength",
         
     | 
| 125 | 
         
            +
                "weaken_fac",
         
     | 
| 126 | 
         
            +
                "pick_frac",
         
     | 
| 127 | 
         
            +
                "pick_n",
         
     | 
| 128 | 
         
            +
                "automatic",
         
     | 
| 129 | 
         
            +
                "max_points_lost",
         
     | 
| 130 | 
         
            +
                "min_visits",
         
     | 
| 131 | 
         
            +
            }
         
     | 
| 132 | 
         
            +
             
     | 
| 133 | 
         
            +
             
     | 
| 134 | 
         
            +
            CALIBRATED_RANK_ELO = [
         
     | 
| 135 | 
         
            +
                (-21.679482223451032, 18),
         
     | 
| 136 | 
         
            +
                (42.60243194422105, 17),
         
     | 
| 137 | 
         
            +
                (106.88434611189314, 16),
         
     | 
| 138 | 
         
            +
                (171.16626027956522, 15),
         
     | 
| 139 | 
         
            +
                (235.44817444723742, 14),
         
     | 
| 140 | 
         
            +
                (299.7300886149095, 13),
         
     | 
| 141 | 
         
            +
                (364.0120027825817, 12),
         
     | 
| 142 | 
         
            +
                (428.2939169502538, 11),
         
     | 
| 143 | 
         
            +
                (492.5758311179259, 10),
         
     | 
| 144 | 
         
            +
                (556.8577452855981, 9),
         
     | 
| 145 | 
         
            +
                (621.1396594532702, 8),
         
     | 
| 146 | 
         
            +
                (685.4215736209424, 7),
         
     | 
| 147 | 
         
            +
                (749.7034877886144, 6),
         
     | 
| 148 | 
         
            +
                (813.9854019562865, 5),
         
     | 
| 149 | 
         
            +
                (878.2673161239586, 4),
         
     | 
| 150 | 
         
            +
                (942.5492302916308, 3),
         
     | 
| 151 | 
         
            +
                (1006.8311444593029, 2),
         
     | 
| 152 | 
         
            +
                (1071.113058626975, 1),
         
     | 
| 153 | 
         
            +
                (1135.3949727946472, 0),
         
     | 
| 154 | 
         
            +
                (1199.6768869623193, -1),
         
     | 
| 155 | 
         
            +
                (1263.9588011299913, -2),
         
     | 
| 156 | 
         
            +
                (1700, -4),
         
     | 
| 157 | 
         
            +
            ]
         
     | 
| 158 | 
         
            +
             
     | 
| 159 | 
         
            +
             
     | 
| 160 | 
         
            +
            AI_WEIGHTED_ELO = [
         
     | 
| 161 | 
         
            +
                (0.5, 1591.5718897531551),
         
     | 
| 162 | 
         
            +
                (1.0, 1269.9896556526198),
         
     | 
| 163 | 
         
            +
                (1.25, 1042.25179764667),
         
     | 
| 164 | 
         
            +
                (1.5, 848.9410084463602),
         
     | 
| 165 | 
         
            +
                (1.75, 630.1483212024823),
         
     | 
| 166 | 
         
            +
                (2, 575.3637091858013),
         
     | 
| 167 | 
         
            +
                (2.5, 410.9747543504796),
         
     | 
| 168 | 
         
            +
                (3.0, 219.8667371799533),
         
     | 
| 169 | 
         
            +
            ]
         
     | 
| 170 | 
         
            +
             
     | 
| 171 | 
         
            +
            AI_SCORELOSS_ELO = [
         
     | 
| 172 | 
         
            +
                (0.0, 539),
         
     | 
| 173 | 
         
            +
                (0.05, 625),
         
     | 
| 174 | 
         
            +
                (0.1, 859),
         
     | 
| 175 | 
         
            +
                (0.2, 1035),
         
     | 
| 176 | 
         
            +
                (0.3, 1201),
         
     | 
| 177 | 
         
            +
                (0.4, 1299),
         
     | 
| 178 | 
         
            +
                (0.5, 1346),
         
     | 
| 179 | 
         
            +
                (0.75, 1374),
         
     | 
| 180 | 
         
            +
                (1.0, 1386),
         
     | 
| 181 | 
         
            +
            ]
         
     | 
| 182 | 
         
            +
             
     | 
| 183 | 
         
            +
             
     | 
| 184 | 
         
            +
            AI_LOCAL_ELO_GRID = [
         
     | 
| 185 | 
         
            +
                [0.0, 0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0],
         
     | 
| 186 | 
         
            +
                [0, 5, 10, 15, 25, 50],
         
     | 
| 187 | 
         
            +
                [
         
     | 
| 188 | 
         
            +
                    [-204.0, 791.0, 1154.0, 1372.0, 1402.0, 1473.0, 1700.0, 1700.0],
         
     | 
| 189 | 
         
            +
                    [174.0, 1094.0, 1191.0, 1384.0, 1435.0, 1522.0, 1700.0, 1700.0],
         
     | 
| 190 | 
         
            +
                    [619.0, 1155.0, 1323.0, 1390.0, 1450.0, 1558.0, 1700.0, 1700.0],
         
     | 
| 191 | 
         
            +
                    [975.0, 1289.0, 1332.0, 1401.0, 1461.0, 1575.0, 1700.0, 1700.0],
         
     | 
| 192 | 
         
            +
                    [1344.0, 1348.0, 1358.0, 1467.0, 1477.0, 1616.0, 1700.0, 1700.0],
         
     | 
| 193 | 
         
            +
                    [1425.0, 1474.0, 1489.0, 1524.0, 1571.0, 1700.0, 1700.0, 1700.0],
         
     | 
| 194 | 
         
            +
                ],
         
     | 
| 195 | 
         
            +
            ]
         
     | 
| 196 | 
         
            +
            AI_TENUKI_ELO_GRID = [
         
     | 
| 197 | 
         
            +
                [0.0, 0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0],
         
     | 
| 198 | 
         
            +
                [0, 5, 10, 15, 25, 50],
         
     | 
| 199 | 
         
            +
                [
         
     | 
| 200 | 
         
            +
                    [47.0, 335.0, 530.0, 678.0, 830.0, 1070.0, 1376.0, 1700.0],
         
     | 
| 201 | 
         
            +
                    [99.0, 469.0, 546.0, 707.0, 855.0, 1090.0, 1413.0, 1700.0],
         
     | 
| 202 | 
         
            +
                    [327.0, 513.0, 605.0, 745.0, 875.0, 1110.0, 1424.0, 1700.0],
         
     | 
| 203 | 
         
            +
                    [429.0, 519.0, 620.0, 754.0, 900.0, 1130.0, 1435.0, 1700.0],
         
     | 
| 204 | 
         
            +
                    [492.0, 607.0, 682.0, 797.0, 1000.0, 1208.0, 1454.0, 1700.0],
         
     | 
| 205 | 
         
            +
                    [778.0, 830.0, 909.0, 949.0, 1169.0, 1461.0, 1483.0, 1700.0],
         
     | 
| 206 | 
         
            +
                ],
         
     | 
| 207 | 
         
            +
            ]
         
     | 
| 208 | 
         
            +
            AI_TERRITORY_ELO_GRID = [
         
     | 
| 209 | 
         
            +
                [0.0, 0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0],
         
     | 
| 210 | 
         
            +
                [0, 5, 10, 15, 25, 50],
         
     | 
| 211 | 
         
            +
                [
         
     | 
| 212 | 
         
            +
                    [34.0, 383.0, 566.0, 748.0, 980.0, 1264.0, 1527.0, 1700.0],
         
     | 
| 213 | 
         
            +
                    [131.0, 450.0, 586.0, 826.0, 995.0, 1280.0, 1537.0, 1700.0],
         
     | 
| 214 | 
         
            +
                    [291.0, 517.0, 627.0, 850.0, 1010.0, 1310.0, 1547.0, 1700.0],
         
     | 
| 215 | 
         
            +
                    [454.0, 526.0, 696.0, 870.0, 1038.0, 1340.0, 1590.0, 1700.0],
         
     | 
| 216 | 
         
            +
                    [491.0, 603.0, 747.0, 890.0, 1050.0, 1390.0, 1635.0, 1700.0],
         
     | 
| 217 | 
         
            +
                    [718.0, 841.0, 1039.0, 1076.0, 1332.0, 1523.0, 1700.0, 1700.0],
         
     | 
| 218 | 
         
            +
                ],
         
     | 
| 219 | 
         
            +
            ]
         
     | 
| 220 | 
         
            +
            AI_INFLUENCE_ELO_GRID = [
         
     | 
| 221 | 
         
            +
                [0.0, 0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0],
         
     | 
| 222 | 
         
            +
                [0, 5, 10, 15, 25, 50],
         
     | 
| 223 | 
         
            +
                [
         
     | 
| 224 | 
         
            +
                    [217.0, 439.0, 572.0, 768.0, 960.0, 1227.0, 1449.0, 1521.0],
         
     | 
| 225 | 
         
            +
                    [302.0, 551.0, 580.0, 800.0, 1028.0, 1257.0, 1470.0, 1529.0],
         
     | 
| 226 | 
         
            +
                    [388.0, 572.0, 619.0, 839.0, 1077.0, 1305.0, 1490.0, 1561.0],
         
     | 
| 227 | 
         
            +
                    [467.0, 591.0, 764.0, 878.0, 1097.0, 1390.0, 1530.0, 1591.0],
         
     | 
| 228 | 
         
            +
                    [539.0, 622.0, 815.0, 953.0, 1120.0, 1420.0, 1560.0, 1601.0],
         
     | 
| 229 | 
         
            +
                    [772.0, 912.0, 958.0, 1145.0, 1318.0, 1511.0, 1577.0, 1623.0],
         
     | 
| 230 | 
         
            +
                ],
         
     | 
| 231 | 
         
            +
            ]
         
     | 
| 232 | 
         
            +
            AI_PICK_ELO_GRID = [
         
     | 
| 233 | 
         
            +
                [0.0, 0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0],
         
     | 
| 234 | 
         
            +
                [0, 5, 10, 15, 25, 50],
         
     | 
| 235 | 
         
            +
                [
         
     | 
| 236 | 
         
            +
                    [-533.0, -515.0, -355.0, 234.0, 650.0, 1147.0, 1546.0, 1700.0],
         
     | 
| 237 | 
         
            +
                    [-531.0, -450.0, -69.0, 347.0, 670.0, 1182.0, 1550.0, 1700.0],
         
     | 
| 238 | 
         
            +
                    [-450.0, -311.0, 140.0, 459.0, 693.0, 1252.0, 1555.0, 1700.0],
         
     | 
| 239 | 
         
            +
                    [-365.0, -82.0, 265.0, 508.0, 864.0, 1301.0, 1619.0, 1700.0],
         
     | 
| 240 | 
         
            +
                    [-113.0, 273.0, 363.0, 641.0, 983.0, 1486.0, 1700.0, 1700.0],
         
     | 
| 241 | 
         
            +
                    [514.0, 670.0, 870.0, 1128.0, 1305.0, 1550.0, 1700.0, 1700.0],
         
     | 
| 242 | 
         
            +
                ],
         
     | 
| 243 | 
         
            +
            ]
         
     | 
| 244 | 
         
            +
             
     | 
| 245 | 
         
            +
             
     | 
| 246 | 
         
            +
            TOP_MOVE_DELTA_SCORE = "top_move_delta_score"
         
     | 
| 247 | 
         
            +
            TOP_MOVE_SCORE = "top_move_score"
         
     | 
| 248 | 
         
            +
            TOP_MOVE_DELTA_WINRATE = "top_move_delta_winrate"
         
     | 
| 249 | 
         
            +
            TOP_MOVE_WINRATE = "top_move_winrate"
         
     | 
| 250 | 
         
            +
            TOP_MOVE_VISITS = "top_move_visits"
         
     | 
| 251 | 
         
            +
            # TOP_MOVE_UTILITY = "top_move_utility"
         
     | 
| 252 | 
         
            +
            # TOP_MOVE_UTILITYLCB = "top_move_utiltiy_lcb"
         
     | 
| 253 | 
         
            +
            # TOP_MOVE_SCORE_STDDEV = "top_move_score_stddev"
         
     | 
| 254 | 
         
            +
            TOP_MOVE_NOTHING = "top_move_nothing"
         
     | 
| 255 | 
         
            +
             
     | 
| 256 | 
         
            +
             
     | 
| 257 | 
         
            +
            TOP_MOVE_OPTIONS = [
         
     | 
| 258 | 
         
            +
                TOP_MOVE_SCORE,
         
     | 
| 259 | 
         
            +
                TOP_MOVE_DELTA_SCORE,
         
     | 
| 260 | 
         
            +
                TOP_MOVE_WINRATE,
         
     | 
| 261 | 
         
            +
                TOP_MOVE_DELTA_WINRATE,
         
     | 
| 262 | 
         
            +
                TOP_MOVE_VISITS,
         
     | 
| 263 | 
         
            +
                TOP_MOVE_NOTHING,
         
     | 
| 264 | 
         
            +
                # TOP_MOVE_SCORE_STDDEV,
         
     | 
| 265 | 
         
            +
                # TOP_MOVE_UTILITY,
         
     | 
| 266 | 
         
            +
                # TOP_MOVE_UTILITYLCB
         
     | 
| 267 | 
         
            +
            ]
         
     | 
| 268 | 
         
            +
            REPORT_DT = 1
         
     | 
| 269 | 
         
            +
            PONDERING_REPORT_DT = 0.25
         
     | 
| 270 | 
         
            +
             
     | 
| 271 | 
         
            +
            SGF_INTERNAL_COMMENTS_MARKER = "\u3164\u200b"
         
     | 
| 272 | 
         
            +
            SGF_SEPARATOR_MARKER = "\u3164\u3164"
         
     | 
    	
        katrain/katrain/core/contribute_engine.py
    ADDED
    
    | 
         @@ -0,0 +1,302 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import json
         
     | 
| 2 | 
         
            +
            import os
         
     | 
| 3 | 
         
            +
            import random
         
     | 
| 4 | 
         
            +
            import shlex
         
     | 
| 5 | 
         
            +
            import shutil
         
     | 
| 6 | 
         
            +
            import signal
         
     | 
| 7 | 
         
            +
            import subprocess
         
     | 
| 8 | 
         
            +
            import threading
         
     | 
| 9 | 
         
            +
            import time
         
     | 
| 10 | 
         
            +
            import traceback
         
     | 
| 11 | 
         
            +
            from collections import defaultdict
         
     | 
| 12 | 
         
            +
             
     | 
| 13 | 
         
            +
            from katrain.core.constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO, OUTPUT_HONGIK_STDERR, DATA_FOLDER
         
     | 
| 14 | 
         
            +
            from katrain.katrain.core.AI_engine import BaseEngine
         
     | 
| 15 | 
         
            +
            from katrain.core.game import BaseGame
         
     | 
| 16 | 
         
            +
            from katrain.core.lang import i18n
         
     | 
| 17 | 
         
            +
            from katrain.core.sgf_parser import Move
         
     | 
| 18 | 
         
            +
            from katrain.core.utils import find_package_resource
         
     | 
| 19 | 
         
            +
             
     | 
| 20 | 
         
            +
             
     | 
| 21 | 
         
            +
            class KataGoContributeEngine(BaseEngine):
         
     | 
| 22 | 
         
            +
                """Starts and communicates with the KataGo contribute program"""
         
     | 
| 23 | 
         
            +
             
     | 
| 24 | 
         
            +
                DEFAULT_MAX_GAMES = 8
         
     | 
| 25 | 
         
            +
             
     | 
| 26 | 
         
            +
                SHOW_RESULT_TIME = 5
         
     | 
| 27 | 
         
            +
                GIVE_UP_AFTER = 120
         
     | 
| 28 | 
         
            +
             
     | 
| 29 | 
         
            +
                def __init__(self, katrain):
         
     | 
| 30 | 
         
            +
                    super().__init__(katrain, katrain.config("contribute"))
         
     | 
| 31 | 
         
            +
                    self.katrain = katrain
         
     | 
| 32 | 
         
            +
                    base_dir = os.path.expanduser("~/.katrain/katago_contribute")
         
     | 
| 33 | 
         
            +
                    self.katago_process = None
         
     | 
| 34 | 
         
            +
                    self.stdout_thread = None
         
     | 
| 35 | 
         
            +
                    self.stderr_thread = None
         
     | 
| 36 | 
         
            +
                    self.shell = False
         
     | 
| 37 | 
         
            +
                    self.active_games = {}
         
     | 
| 38 | 
         
            +
                    self.finished_games = set()
         
     | 
| 39 | 
         
            +
                    self.showing_game = None
         
     | 
| 40 | 
         
            +
                    self.last_advance = 0
         
     | 
| 41 | 
         
            +
                    self.move_count = 0
         
     | 
| 42 | 
         
            +
                    self.uploaded_games_count = 0
         
     | 
| 43 | 
         
            +
                    self.last_move_for_game = defaultdict(int)
         
     | 
| 44 | 
         
            +
                    self.visits_count = 0
         
     | 
| 45 | 
         
            +
                    self.start_time = 0
         
     | 
| 46 | 
         
            +
                    self.server_error = None
         
     | 
| 47 | 
         
            +
                    self.paused = False
         
     | 
| 48 | 
         
            +
                    self.save_sgf = self.config.get("savesgf", False)
         
     | 
| 49 | 
         
            +
                    self.save_path = self.config.get("savepath", "./dist_sgf/")
         
     | 
| 50 | 
         
            +
                    self.move_speed = self.config.get("movespeed", 2.0)
         
     | 
| 51 | 
         
            +
             
     | 
| 52 | 
         
            +
                    exe = self.get_engine_path(self.config.get("katago"))
         
     | 
| 53 | 
         
            +
                    cacert_path = os.path.join(os.path.split(exe)[0], "cacert.pem")
         
     | 
| 54 | 
         
            +
                    if not os.path.isfile(cacert_path):
         
     | 
| 55 | 
         
            +
                        try:
         
     | 
| 56 | 
         
            +
                            shutil.copyfile(find_package_resource("katrain/KataGo/cacert.pem"), cacert_path)
         
     | 
| 57 | 
         
            +
                        except Exception as e:
         
     | 
| 58 | 
         
            +
                            self.katrain.log(
         
     | 
| 59 | 
         
            +
                                f"Could not copy cacert file ({e}), please add it manually to your katago.exe directory",
         
     | 
| 60 | 
         
            +
                                OUTPUT_ERROR,
         
     | 
| 61 | 
         
            +
                            )
         
     | 
| 62 | 
         
            +
                    cfg = find_package_resource(self.config.get("config"))
         
     | 
| 63 | 
         
            +
             
     | 
| 64 | 
         
            +
                    settings_dict = {
         
     | 
| 65 | 
         
            +
                        "username": self.config.get("username"),
         
     | 
| 66 | 
         
            +
                        "password": self.config.get("password"),
         
     | 
| 67 | 
         
            +
                        "maxSimultaneousGames": self.config.get("maxgames") or self.DEFAULT_MAX_GAMES,
         
     | 
| 68 | 
         
            +
                        "includeOwnership": self.config.get("ownership") or False,
         
     | 
| 69 | 
         
            +
                        "logGamesAsJson": True,
         
     | 
| 70 | 
         
            +
                        "homeDataDir": os.path.expanduser(DATA_FOLDER),
         
     | 
| 71 | 
         
            +
                    }
         
     | 
| 72 | 
         
            +
                    self.max_buffer_games = 2 * settings_dict["maxSimultaneousGames"]
         
     | 
| 73 | 
         
            +
                    settings = {f"{k}={v}" for k, v in settings_dict.items()}
         
     | 
| 74 | 
         
            +
                    self.command = shlex.split(
         
     | 
| 75 | 
         
            +
                        f'"{exe}" contribute -config "{cfg}" -base-dir "{base_dir}" -override-config "{",".join(settings)}"'
         
     | 
| 76 | 
         
            +
                    )
         
     | 
| 77 | 
         
            +
                    self.start()
         
     | 
| 78 | 
         
            +
             
     | 
| 79 | 
         
            +
                @staticmethod
         
     | 
| 80 | 
         
            +
                def game_ended(game):
         
     | 
| 81 | 
         
            +
                    cn = game.current_node
         
     | 
| 82 | 
         
            +
                    if cn.is_pass and cn.analysis_exists:
         
     | 
| 83 | 
         
            +
                        moves = cn.candidate_moves
         
     | 
| 84 | 
         
            +
                        if moves and moves[0]["move"] == "pass":
         
     | 
| 85 | 
         
            +
                            game.play(Move(None, player=game.current_node.next_player))  # play pass
         
     | 
| 86 | 
         
            +
                    return game.end_result
         
     | 
| 87 | 
         
            +
             
     | 
| 88 | 
         
            +
                def advance_showing_game(self):
         
     | 
| 89 | 
         
            +
                    current_game = self.active_games.get(self.showing_game)
         
     | 
| 90 | 
         
            +
                    if current_game:
         
     | 
| 91 | 
         
            +
                        end_result = self.game_ended(current_game)
         
     | 
| 92 | 
         
            +
                        if end_result is not None:
         
     | 
| 93 | 
         
            +
                            self.finished_games.add(self.showing_game)
         
     | 
| 94 | 
         
            +
                            if time.time() - self.last_advance > self.SHOW_RESULT_TIME:
         
     | 
| 95 | 
         
            +
                                del self.active_games[self.showing_game]
         
     | 
| 96 | 
         
            +
                                if self.save_sgf:
         
     | 
| 97 | 
         
            +
                                    filename = os.path.join(self.save_path, f"{self.showing_game}.sgf")
         
     | 
| 98 | 
         
            +
                                    self.katrain.log(current_game.write_sgf(filename, self.katrain.config("trainer")), OUTPUT_INFO)
         
     | 
| 99 | 
         
            +
             
     | 
| 100 | 
         
            +
                                self.katrain.log(f"Game {self.showing_game} finished, finding a new one", OUTPUT_INFO)
         
     | 
| 101 | 
         
            +
                                self.showing_game = None
         
     | 
| 102 | 
         
            +
                        elif time.time() - self.last_advance > self.move_speed or len(self.active_games) > self.max_buffer_games:
         
     | 
| 103 | 
         
            +
                            if current_game.current_node.children:
         
     | 
| 104 | 
         
            +
                                current_game.redo(1)
         
     | 
| 105 | 
         
            +
                                self.last_advance = time.time()
         
     | 
| 106 | 
         
            +
                                self.katrain("update-state")
         
     | 
| 107 | 
         
            +
                            elif time.time() - self.last_advance > self.GIVE_UP_AFTER:
         
     | 
| 108 | 
         
            +
                                self.katrain.log(
         
     | 
| 109 | 
         
            +
                                    f"Giving up on game {self.showing_game} which appears stuck, finding a new one", OUTPUT_INFO
         
     | 
| 110 | 
         
            +
                                )
         
     | 
| 111 | 
         
            +
                                self.showing_game = None
         
     | 
| 112 | 
         
            +
                    else:
         
     | 
| 113 | 
         
            +
                        if self.active_games:
         
     | 
| 114 | 
         
            +
                            self.showing_game = None
         
     | 
| 115 | 
         
            +
                            best_count = -1
         
     | 
| 116 | 
         
            +
                            for game_id, game in self.active_games.items():  # find game with most moves left to show
         
     | 
| 117 | 
         
            +
                                count = 0
         
     | 
| 118 | 
         
            +
                                node = game.current_node
         
     | 
| 119 | 
         
            +
                                while node.children:
         
     | 
| 120 | 
         
            +
                                    node = node.children[0]
         
     | 
| 121 | 
         
            +
                                    count += 1
         
     | 
| 122 | 
         
            +
                                if count > best_count:
         
     | 
| 123 | 
         
            +
                                    best_count = count
         
     | 
| 124 | 
         
            +
                                    self.showing_game = game_id
         
     | 
| 125 | 
         
            +
                            self.last_advance = time.time()
         
     | 
| 126 | 
         
            +
                            self.katrain.log(f"Showing game {self.showing_game}, {best_count} moves left to show.", OUTPUT_INFO)
         
     | 
| 127 | 
         
            +
             
     | 
| 128 | 
         
            +
                            self.katrain.game = self.active_games[self.showing_game]
         
     | 
| 129 | 
         
            +
                            self.katrain("update-state", redraw_board=True)
         
     | 
| 130 | 
         
            +
             
     | 
| 131 | 
         
            +
                def status(self):
         
     | 
| 132 | 
         
            +
                    return f"Contributing to distributed training\nGames: {self.uploaded_games_count} uploaded, {len(self.active_games)} in buffer, {len(self.finished_games)} shown\n{self.move_count} moves played ({60*self.move_count/(time.time()-self.start_time):.1f}/min, {self.visits_count / (time.time() - self.start_time):.1f} visits/s)\n"
         
     | 
| 133 | 
         
            +
             
     | 
| 134 | 
         
            +
                def is_idle(self):
         
     | 
| 135 | 
         
            +
                    return False
         
     | 
| 136 | 
         
            +
             
     | 
| 137 | 
         
            +
                def queries_remaining(self):
         
     | 
| 138 | 
         
            +
                    return 1
         
     | 
| 139 | 
         
            +
             
     | 
| 140 | 
         
            +
                def start(self):
         
     | 
| 141 | 
         
            +
                    try:
         
     | 
| 142 | 
         
            +
                        self.katrain.log(f"Starting Distributed KataGo with {self.command}", OUTPUT_INFO)
         
     | 
| 143 | 
         
            +
                        startupinfo = None
         
     | 
| 144 | 
         
            +
                        if hasattr(subprocess, "STARTUPINFO"):
         
     | 
| 145 | 
         
            +
                            startupinfo = subprocess.STARTUPINFO()
         
     | 
| 146 | 
         
            +
                            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW  # stop command box popups on win/pyinstaller
         
     | 
| 147 | 
         
            +
                        self.katago_process = subprocess.Popen(
         
     | 
| 148 | 
         
            +
                            self.command,
         
     | 
| 149 | 
         
            +
                            stdout=subprocess.PIPE,
         
     | 
| 150 | 
         
            +
                            stderr=subprocess.PIPE,
         
     | 
| 151 | 
         
            +
                            stdin=subprocess.PIPE,
         
     | 
| 152 | 
         
            +
                            startupinfo=startupinfo,
         
     | 
| 153 | 
         
            +
                            shell=self.shell,
         
     | 
| 154 | 
         
            +
                        )
         
     | 
| 155 | 
         
            +
                    except (FileNotFoundError, PermissionError, OSError) as e:
         
     | 
| 156 | 
         
            +
                        self.katrain.log(
         
     | 
| 157 | 
         
            +
                            i18n._("Starting Kata failed").format(command=self.command, error=e),
         
     | 
| 158 | 
         
            +
                            OUTPUT_ERROR,
         
     | 
| 159 | 
         
            +
                        )
         
     | 
| 160 | 
         
            +
                        return  # don't start
         
     | 
| 161 | 
         
            +
                    self.paused = False
         
     | 
| 162 | 
         
            +
                    self.stdout_thread = threading.Thread(target=self._read_stdout_thread, daemon=True)
         
     | 
| 163 | 
         
            +
                    self.stderr_thread = threading.Thread(target=self._read_stderr_thread, daemon=True)
         
     | 
| 164 | 
         
            +
                    self.stdout_thread.start()
         
     | 
| 165 | 
         
            +
                    self.stderr_thread.start()
         
     | 
| 166 | 
         
            +
             
     | 
| 167 | 
         
            +
                def check_alive(self, os_error="", maybe_open_help=False):
         
     | 
| 168 | 
         
            +
                    ok = self.katago_process and self.katago_process.poll() is None
         
     | 
| 169 | 
         
            +
                    if not ok:
         
     | 
| 170 | 
         
            +
                        if self.katago_process:
         
     | 
| 171 | 
         
            +
                            code = self.katago_process and self.katago_process.poll()
         
     | 
| 172 | 
         
            +
                            if code == 3221225781:
         
     | 
| 173 | 
         
            +
                                died_msg = i18n._("Engine missing DLL")
         
     | 
| 174 | 
         
            +
                            else:
         
     | 
| 175 | 
         
            +
                                os_error += f"status {code}"
         
     | 
| 176 | 
         
            +
                                died_msg = i18n._("Engine died unexpectedly").format(error=os_error)
         
     | 
| 177 | 
         
            +
                            if code != 1 and not self.server_error:  # deliberate exit, already showed message?
         
     | 
| 178 | 
         
            +
                                self.katrain.log(died_msg, OUTPUT_ERROR)
         
     | 
| 179 | 
         
            +
                            self.katago_process = None
         
     | 
| 180 | 
         
            +
                    return ok
         
     | 
| 181 | 
         
            +
             
     | 
| 182 | 
         
            +
                def shutdown(self, finish=False):
         
     | 
| 183 | 
         
            +
                    process = self.katago_process
         
     | 
| 184 | 
         
            +
                    if process:
         
     | 
| 185 | 
         
            +
                        self.katago_process.stdin.write(b"forcequit\n")
         
     | 
| 186 | 
         
            +
                        self.katago_process.stdin.flush()
         
     | 
| 187 | 
         
            +
                        self.katago_process = None
         
     | 
| 188 | 
         
            +
                        process.terminate()
         
     | 
| 189 | 
         
            +
                    if finish is not None:
         
     | 
| 190 | 
         
            +
                        for t in [self.stderr_thread, self.stdout_thread]:
         
     | 
| 191 | 
         
            +
                            if t:
         
     | 
| 192 | 
         
            +
                                t.join()
         
     | 
| 193 | 
         
            +
             
     | 
| 194 | 
         
            +
                def graceful_shutdown(self):
         
     | 
| 195 | 
         
            +
                    """respond to esc"""
         
     | 
| 196 | 
         
            +
                    if self.katago_process:
         
     | 
| 197 | 
         
            +
                        self.katago_process.stdin.write(b"quit\n")
         
     | 
| 198 | 
         
            +
                        self.katago_process.stdin.flush()
         
     | 
| 199 | 
         
            +
                        self.katrain.log("Finishing games in progress and stopping contribution", OUTPUT_KATAGO_STDERR)
         
     | 
| 200 | 
         
            +
             
     | 
| 201 | 
         
            +
                def pause(self):
         
     | 
| 202 | 
         
            +
                    """respond to pause"""
         
     | 
| 203 | 
         
            +
                    if self.katago_process:
         
     | 
| 204 | 
         
            +
                        if not self.paused:
         
     | 
| 205 | 
         
            +
                            self.katago_process.stdin.write(b"pause\n")
         
     | 
| 206 | 
         
            +
                            self.katago_process.stdin.flush()
         
     | 
| 207 | 
         
            +
                            self.katrain.log("Pausing contribution", OUTPUT_KATAGO_STDERR)
         
     | 
| 208 | 
         
            +
                        else:
         
     | 
| 209 | 
         
            +
                            self.katago_process.stdin.write(b"resume\n")
         
     | 
| 210 | 
         
            +
                            self.katago_process.stdin.flush()
         
     | 
| 211 | 
         
            +
                            self.katrain.log("Resuming contribution", OUTPUT_KATAGO_STDERR)
         
     | 
| 212 | 
         
            +
                        self.paused = not self.paused
         
     | 
| 213 | 
         
            +
             
     | 
| 214 | 
         
            +
                def _read_stderr_thread(self):
         
     | 
| 215 | 
         
            +
                    while self.katago_process is not None:
         
     | 
| 216 | 
         
            +
                        try:
         
     | 
| 217 | 
         
            +
                            line = self.katago_process.stderr.readline()
         
     | 
| 218 | 
         
            +
                            if line:
         
     | 
| 219 | 
         
            +
                                try:
         
     | 
| 220 | 
         
            +
                                    message = line.decode(errors="ignore").strip()
         
     | 
| 221 | 
         
            +
                                    if any(
         
     | 
| 222 | 
         
            +
                                        s in message
         
     | 
| 223 | 
         
            +
                                        for s in ["not status code 200 OK", "Server returned error", "Uncaught exception:"]
         
     | 
| 224 | 
         
            +
                                    ):
         
     | 
| 225 | 
         
            +
                                        message = message.replace("what():", "").replace("Uncaught exception:", "").strip()
         
     | 
| 226 | 
         
            +
                                        self.server_error = message  # don't be surprised by engine dying
         
     | 
| 227 | 
         
            +
                                        self.katrain.log(message, OUTPUT_ERROR)
         
     | 
| 228 | 
         
            +
                                        return
         
     | 
| 229 | 
         
            +
                                    else:
         
     | 
| 230 | 
         
            +
                                        self.katrain.log(message, OUTPUT_KATAGO_STDERR)
         
     | 
| 231 | 
         
            +
                                except Exception as e:
         
     | 
| 232 | 
         
            +
                                    print("ERROR in processing KataGo stderr:", line, "Exception", e)
         
     | 
| 233 | 
         
            +
                            elif self.katago_process and not self.check_alive():
         
     | 
| 234 | 
         
            +
                                return
         
     | 
| 235 | 
         
            +
                        except Exception as e:
         
     | 
| 236 | 
         
            +
                            self.katrain.log(f"Exception in reading stdout {e}", OUTPUT_DEBUG)
         
     | 
| 237 | 
         
            +
                            return
         
     | 
| 238 | 
         
            +
             
     | 
| 239 | 
         
            +
                def _read_stdout_thread(self):
         
     | 
| 240 | 
         
            +
                    while self.katago_process is not None:
         
     | 
| 241 | 
         
            +
                        try:
         
     | 
| 242 | 
         
            +
                            line = self.katago_process.stdout.readline()
         
     | 
| 243 | 
         
            +
                            if line:
         
     | 
| 244 | 
         
            +
                                line = line.decode(errors="ignore").strip()
         
     | 
| 245 | 
         
            +
                                if line.startswith("{"):
         
     | 
| 246 | 
         
            +
                                    try:
         
     | 
| 247 | 
         
            +
                                        analysis = json.loads(line)
         
     | 
| 248 | 
         
            +
                                        if "gameId" in analysis:
         
     | 
| 249 | 
         
            +
                                            game_id = analysis["gameId"]
         
     | 
| 250 | 
         
            +
                                            if game_id in self.finished_games:
         
     | 
| 251 | 
         
            +
                                                continue
         
     | 
| 252 | 
         
            +
                                            current_game = self.active_games.get(game_id)
         
     | 
| 253 | 
         
            +
                                            new_game = current_game is None
         
     | 
| 254 | 
         
            +
                                            if new_game:
         
     | 
| 255 | 
         
            +
                                                board_size = [analysis["boardXSize"], analysis["boardYSize"]]
         
     | 
| 256 | 
         
            +
                                                placements = {
         
     | 
| 257 | 
         
            +
                                                    f"A{bw}": [
         
     | 
| 258 | 
         
            +
                                                        Move.from_gtp(move, pl).sgf(board_size)
         
     | 
| 259 | 
         
            +
                                                        for pl, move in analysis["initialStones"]
         
     | 
| 260 | 
         
            +
                                                        if pl == bw
         
     | 
| 261 | 
         
            +
                                                    ]
         
     | 
| 262 | 
         
            +
                                                    for bw in "BW"
         
     | 
| 263 | 
         
            +
                                                }
         
     | 
| 264 | 
         
            +
                                                game_properties = {k: v for k, v in placements.items() if v}
         
     | 
| 265 | 
         
            +
                                                game_properties["SZ"] = f"{board_size[0]}:{board_size[1]}"
         
     | 
| 266 | 
         
            +
                                                game_properties["KM"] = analysis["rules"]["komi"]
         
     | 
| 267 | 
         
            +
                                                game_properties["RU"] = json.dumps(analysis["rules"])
         
     | 
| 268 | 
         
            +
                                                game_properties["PB"] = analysis["blackPlayer"]
         
     | 
| 269 | 
         
            +
                                                game_properties["PW"] = analysis["whitePlayer"]
         
     | 
| 270 | 
         
            +
                                                current_game = BaseGame(
         
     | 
| 271 | 
         
            +
                                                    self.katrain, game_properties=game_properties, bypass_config=True
         
     | 
| 272 | 
         
            +
                                                )
         
     | 
| 273 | 
         
            +
                                                self.active_games[game_id] = current_game
         
     | 
| 274 | 
         
            +
                                            last_node = current_game.sync_branch(
         
     | 
| 275 | 
         
            +
                                                [Move.from_gtp(coord, pl) for pl, coord in analysis["moves"]]
         
     | 
| 276 | 
         
            +
                                            )
         
     | 
| 277 | 
         
            +
                                            last_node.set_analysis(analysis)
         
     | 
| 278 | 
         
            +
                                            if new_game:
         
     | 
| 279 | 
         
            +
                                                current_game.set_current_node(last_node)
         
     | 
| 280 | 
         
            +
                                            self.start_time = self.start_time or time.time() - 1
         
     | 
| 281 | 
         
            +
                                            self.move_count += 1
         
     | 
| 282 | 
         
            +
                                            self.visits_count += analysis["rootInfo"]["visits"]
         
     | 
| 283 | 
         
            +
                                            last_move = self.last_move_for_game[game_id]
         
     | 
| 284 | 
         
            +
                                            self.last_move_for_game[game_id] = time.time()
         
     | 
| 285 | 
         
            +
                                            dt = self.last_move_for_game[game_id] - last_move if last_move else 0
         
     | 
| 286 | 
         
            +
                                            self.katrain.log(
         
     | 
| 287 | 
         
            +
                                                f"[{time.time()-self.start_time:.1f}] Game {game_id} Move {analysis['turnNumber']}: {' '.join(analysis['move'])} Visits {analysis['rootInfo']['visits']} Time {dt:.1f}s\t Moves/min {60*self.move_count/(time.time()-self.start_time):.1f} Visits/s {self.visits_count/(time.time()-self.start_time):.1f}",
         
     | 
| 288 | 
         
            +
                                                OUTPUT_DEBUG,
         
     | 
| 289 | 
         
            +
                                            )
         
     | 
| 290 | 
         
            +
                                            self.katrain("update-state")
         
     | 
| 291 | 
         
            +
                                    except Exception as e:
         
     | 
| 292 | 
         
            +
                                        traceback.print_exc()
         
     | 
| 293 | 
         
            +
                                        self.katrain.log(f"Exception {e} in parsing or processing JSON: {line}", OUTPUT_ERROR)
         
     | 
| 294 | 
         
            +
                                elif "uploaded sgf" in line:
         
     | 
| 295 | 
         
            +
                                    self.uploaded_games_count += 1
         
     | 
| 296 | 
         
            +
                                else:
         
     | 
| 297 | 
         
            +
                                    self.katrain.log(line, OUTPUT_KATAGO_STDERR)
         
     | 
| 298 | 
         
            +
                            elif self.katago_process and not self.check_alive():  # stderr will do this
         
     | 
| 299 | 
         
            +
                                return
         
     | 
| 300 | 
         
            +
                        except Exception as e:
         
     | 
| 301 | 
         
            +
                            self.katrain.log(f"Exception in reading stdout {e}", OUTPUT_DEBUG)
         
     | 
| 302 | 
         
            +
                            return
         
     | 
    	
        katrain/katrain/core/game.py
    ADDED
    
    | 
         @@ -0,0 +1,818 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import copy
         
     | 
| 2 | 
         
            +
            import math
         
     | 
| 3 | 
         
            +
            import os
         
     | 
| 4 | 
         
            +
            import re
         
     | 
| 5 | 
         
            +
            import threading
         
     | 
| 6 | 
         
            +
            from datetime import datetime
         
     | 
| 7 | 
         
            +
            from typing import Dict, List, Optional, Union
         
     | 
| 8 | 
         
            +
             
     | 
| 9 | 
         
            +
            from kivy.clock import Clock
         
     | 
| 10 | 
         
            +
             
     | 
| 11 | 
         
            +
            from katrain.core.constants import (
         
     | 
| 12 | 
         
            +
                OUTPUT_DEBUG,
         
     | 
| 13 | 
         
            +
                OUTPUT_EXTRA_DEBUG,
         
     | 
| 14 | 
         
            +
                OUTPUT_INFO,
         
     | 
| 15 | 
         
            +
                PLAYER_AI,
         
     | 
| 16 | 
         
            +
                PLAYER_HUMAN,
         
     | 
| 17 | 
         
            +
                PROGRAM_NAME,
         
     | 
| 18 | 
         
            +
                SGF_INTERNAL_COMMENTS_MARKER,
         
     | 
| 19 | 
         
            +
                STATUS_ANALYSIS,
         
     | 
| 20 | 
         
            +
                STATUS_ERROR,
         
     | 
| 21 | 
         
            +
                STATUS_INFO,
         
     | 
| 22 | 
         
            +
                STATUS_TEACHING,
         
     | 
| 23 | 
         
            +
                PRIORITY_GAME_ANALYSIS,
         
     | 
| 24 | 
         
            +
                PRIORITY_EXTRA_ANALYSIS,
         
     | 
| 25 | 
         
            +
                PRIORITY_SWEEP,
         
     | 
| 26 | 
         
            +
                PRIORITY_ALTERNATIVES,
         
     | 
| 27 | 
         
            +
                PRIORITY_EQUALIZE,
         
     | 
| 28 | 
         
            +
                PRIORITY_DEFAULT,
         
     | 
| 29 | 
         
            +
            )
         
     | 
| 30 | 
         
            +
            from hongik.engine_ai import HongikAIEngine
         
     | 
| 31 | 
         
            +
            from katrain.core.game_node import GameNode
         
     | 
| 32 | 
         
            +
            from katrain.core.lang import i18n, rank_label
         
     | 
| 33 | 
         
            +
            from katrain.core.sgf_parser import SGF, Move
         
     | 
| 34 | 
         
            +
            from katrain.core.utils import var_to_grid, weighted_selection_without_replacement
         
     | 
| 35 | 
         
            +
             
     | 
| 36 | 
         
            +
             
     | 
| 37 | 
         
            +
            class IllegalMoveException(Exception):
         
     | 
| 38 | 
         
            +
                pass
         
     | 
| 39 | 
         
            +
             
     | 
| 40 | 
         
            +
             
     | 
| 41 | 
         
            +
            class KaTrainSGF(SGF):
         
     | 
| 42 | 
         
            +
                _NODE_CLASS = GameNode
         
     | 
| 43 | 
         
            +
             
     | 
| 44 | 
         
            +
             
     | 
| 45 | 
         
            +
            class BaseGame:
         
     | 
| 46 | 
         
            +
                """Represents a game of go, including an implementation of capture rules."""
         
     | 
| 47 | 
         
            +
             
     | 
| 48 | 
         
            +
                DEFAULT_PROPERTIES = {"GM": 1, "FF": 4}
         
     | 
| 49 | 
         
            +
             
     | 
| 50 | 
         
            +
                def __init__(
         
     | 
| 51 | 
         
            +
                    self,
         
     | 
| 52 | 
         
            +
                    katrain,
         
     | 
| 53 | 
         
            +
                    move_tree: GameNode = None,
         
     | 
| 54 | 
         
            +
                    game_properties: Optional[Dict] = None,
         
     | 
| 55 | 
         
            +
                    sgf_filename=None,
         
     | 
| 56 | 
         
            +
                    bypass_config=False,  # TODO: refactor?
         
     | 
| 57 | 
         
            +
                ):
         
     | 
| 58 | 
         
            +
                    self.katrain = katrain
         
     | 
| 59 | 
         
            +
                    self._lock = threading.Lock()
         
     | 
| 60 | 
         
            +
                    self.game_id = datetime.strftime(datetime.now(), "%Y-%m-%d %H %M %S")
         
     | 
| 61 | 
         
            +
                    self.sgf_filename = sgf_filename
         
     | 
| 62 | 
         
            +
             
     | 
| 63 | 
         
            +
                    self.insert_mode = False
         
     | 
| 64 | 
         
            +
                    self.external_game = False  # not generated by katrain at some point
         
     | 
| 65 | 
         
            +
             
     | 
| 66 | 
         
            +
                    if move_tree:
         
     | 
| 67 | 
         
            +
                        self.root = move_tree
         
     | 
| 68 | 
         
            +
                        self.external_game = PROGRAM_NAME not in self.root.get_property("AP", "")
         
     | 
| 69 | 
         
            +
                        handicap = int(self.root.handicap)
         
     | 
| 70 | 
         
            +
                        num_starting_moves_black = 0
         
     | 
| 71 | 
         
            +
                        node = self.root
         
     | 
| 72 | 
         
            +
                        while node.children:
         
     | 
| 73 | 
         
            +
                            node = node.children[0]
         
     | 
| 74 | 
         
            +
                            if node.player == "B":
         
     | 
| 75 | 
         
            +
                                num_starting_moves_black += 1
         
     | 
| 76 | 
         
            +
                            else:
         
     | 
| 77 | 
         
            +
                                break
         
     | 
| 78 | 
         
            +
             
     | 
| 79 | 
         
            +
                        if (
         
     | 
| 80 | 
         
            +
                            handicap >= 2
         
     | 
| 81 | 
         
            +
                            and not self.root.placements
         
     | 
| 82 | 
         
            +
                            and not (num_starting_moves_black == handicap)
         
     | 
| 83 | 
         
            +
                            and not (self.root.children and self.root.children[0].placements)
         
     | 
| 84 | 
         
            +
                        ):  # not really according to sgf, and not sure if still needed, last clause for fox
         
     | 
| 85 | 
         
            +
                            self.root.place_handicap_stones(handicap)
         
     | 
| 86 | 
         
            +
                    else:
         
     | 
| 87 | 
         
            +
                        default_properties = {**Game.DEFAULT_PROPERTIES, "DT": self.game_id}
         
     | 
| 88 | 
         
            +
                        if not bypass_config:
         
     | 
| 89 | 
         
            +
                            default_properties.update(
         
     | 
| 90 | 
         
            +
                                {
         
     | 
| 91 | 
         
            +
                                    "SZ": katrain.config("game/size"),
         
     | 
| 92 | 
         
            +
                                    "KM": katrain.config("game/komi"),
         
     | 
| 93 | 
         
            +
                                    "RU": katrain.config("game/rules"),
         
     | 
| 94 | 
         
            +
                                }
         
     | 
| 95 | 
         
            +
                            )
         
     | 
| 96 | 
         
            +
                        self.root = GameNode(
         
     | 
| 97 | 
         
            +
                            properties={
         
     | 
| 98 | 
         
            +
                                **default_properties,
         
     | 
| 99 | 
         
            +
                                **(game_properties or {}),
         
     | 
| 100 | 
         
            +
                            }
         
     | 
| 101 | 
         
            +
                        )
         
     | 
| 102 | 
         
            +
                        handicap = katrain.config("game/handicap")
         
     | 
| 103 | 
         
            +
                        if not bypass_config and handicap:
         
     | 
| 104 | 
         
            +
                            self.root.place_handicap_stones(handicap)
         
     | 
| 105 | 
         
            +
             
     | 
| 106 | 
         
            +
                    if not self.root.get_property("RU"):  # if rules missing in sgf, inherit current
         
     | 
| 107 | 
         
            +
                        self.root.set_property("RU", katrain.config("game/rules"))
         
     | 
| 108 | 
         
            +
             
     | 
| 109 | 
         
            +
                    self.set_current_node(self.root)
         
     | 
| 110 | 
         
            +
                    self.main_time_used = 0
         
     | 
| 111 | 
         
            +
             
     | 
| 112 | 
         
            +
                    # restore shortcuts
         
     | 
| 113 | 
         
            +
                    shortcut_id_to_node = {node.get_property("KTSID", None): node for node in self.root.nodes_in_tree}
         
     | 
| 114 | 
         
            +
                    for node in self.root.nodes_in_tree:
         
     | 
| 115 | 
         
            +
                        shortcut_id = node.get_property("KTSF", None)
         
     | 
| 116 | 
         
            +
                        if shortcut_id and shortcut_id in shortcut_id_to_node:
         
     | 
| 117 | 
         
            +
                            shortcut_id_to_node[shortcut_id].add_shortcut(node)
         
     | 
| 118 | 
         
            +
             
     | 
| 119 | 
         
            +
                # -- move tree functions --
         
     | 
| 120 | 
         
            +
                def _init_state(self):
         
     | 
| 121 | 
         
            +
                    board_size_x, board_size_y = self.board_size
         
     | 
| 122 | 
         
            +
                    self.board = [
         
     | 
| 123 | 
         
            +
                        [-1 for _x in range(board_size_x)] for _y in range(board_size_y)
         
     | 
| 124 | 
         
            +
                    ]  # type: List[List[int]]  #  board pos -> chain id
         
     | 
| 125 | 
         
            +
                    self.chains = []  # type: List[List[Move]]  #   chain id -> chain
         
     | 
| 126 | 
         
            +
                    self.prisoners = []  # type: List[Move]
         
     | 
| 127 | 
         
            +
                    self.last_capture = []  # type: List[Move]
         
     | 
| 128 | 
         
            +
             
     | 
| 129 | 
         
            +
                def _calculate_groups(self):
         
     | 
| 130 | 
         
            +
                    with self._lock:
         
     | 
| 131 | 
         
            +
                        self._init_state()
         
     | 
| 132 | 
         
            +
                        try:
         
     | 
| 133 | 
         
            +
                            for node in self.current_node.nodes_from_root:
         
     | 
| 134 | 
         
            +
                                for m in node.move_with_placements:
         
     | 
| 135 | 
         
            +
                                    self._validate_move_and_update_chains(
         
     | 
| 136 | 
         
            +
                                        m, True
         
     | 
| 137 | 
         
            +
                                    )  # ignore ko since we didn't know if it was forced
         
     | 
| 138 | 
         
            +
                                if node.clear_placements:  # handle AE by playing all moves left from empty board
         
     | 
| 139 | 
         
            +
                                    clear_coords = {c.coords for c in node.clear_placements}
         
     | 
| 140 | 
         
            +
                                    stones = [m for c in self.chains for m in c if m.coords not in clear_coords]
         
     | 
| 141 | 
         
            +
                                    self._init_state()
         
     | 
| 142 | 
         
            +
                                    for m in stones:
         
     | 
| 143 | 
         
            +
                                        self._validate_move_and_update_chains(m, True)
         
     | 
| 144 | 
         
            +
                        except IllegalMoveException as e:
         
     | 
| 145 | 
         
            +
                            raise Exception(f"Unexpected illegal move ({str(e)})")
         
     | 
| 146 | 
         
            +
             
     | 
| 147 | 
         
            +
                def _validate_move_and_update_chains(self, move: Move, ignore_ko: bool):
         
     | 
| 148 | 
         
            +
                    board_size_x, board_size_y = self.board_size
         
     | 
| 149 | 
         
            +
             
     | 
| 150 | 
         
            +
                    def neighbours(moves):
         
     | 
| 151 | 
         
            +
                        return {
         
     | 
| 152 | 
         
            +
                            self.board[m.coords[1] + dy][m.coords[0] + dx]
         
     | 
| 153 | 
         
            +
                            for m in moves
         
     | 
| 154 | 
         
            +
                            for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]
         
     | 
| 155 | 
         
            +
                            if 0 <= m.coords[0] + dx < board_size_x and 0 <= m.coords[1] + dy < board_size_y
         
     | 
| 156 | 
         
            +
                        }
         
     | 
| 157 | 
         
            +
             
     | 
| 158 | 
         
            +
                    ko_or_snapback = len(self.last_capture) == 1 and self.last_capture[0] == move
         
     | 
| 159 | 
         
            +
                    self.last_capture = []
         
     | 
| 160 | 
         
            +
             
     | 
| 161 | 
         
            +
                    if move.is_pass:
         
     | 
| 162 | 
         
            +
                        return
         
     | 
| 163 | 
         
            +
             
     | 
| 164 | 
         
            +
                    if self.board[move.coords[1]][move.coords[0]] != -1:
         
     | 
| 165 | 
         
            +
                        raise IllegalMoveException("Space occupied")
         
     | 
| 166 | 
         
            +
             
     | 
| 167 | 
         
            +
                    # merge chains connected by this move, or create a new one
         
     | 
| 168 | 
         
            +
                    nb_chains = list({c for c in neighbours([move]) if c >= 0 and self.chains[c][0].player == move.player})
         
     | 
| 169 | 
         
            +
                    if nb_chains:
         
     | 
| 170 | 
         
            +
                        this_chain = nb_chains[0]
         
     | 
| 171 | 
         
            +
                        self.board = [[nb_chains[0] if sq in nb_chains else sq for sq in line] for line in self.board]
         
     | 
| 172 | 
         
            +
                        for oc in nb_chains[1:]:
         
     | 
| 173 | 
         
            +
                            self.chains[nb_chains[0]] += self.chains[oc]
         
     | 
| 174 | 
         
            +
                            self.chains[oc] = []
         
     | 
| 175 | 
         
            +
                        self.chains[nb_chains[0]].append(move)
         
     | 
| 176 | 
         
            +
                    else:
         
     | 
| 177 | 
         
            +
                        this_chain = len(self.chains)
         
     | 
| 178 | 
         
            +
                        self.chains.append([move])
         
     | 
| 179 | 
         
            +
                    self.board[move.coords[1]][move.coords[0]] = this_chain
         
     | 
| 180 | 
         
            +
             
     | 
| 181 | 
         
            +
                    # check captures
         
     | 
| 182 | 
         
            +
                    opp_nb_chains = {c for c in neighbours([move]) if c >= 0 and self.chains[c][0].player != move.player}
         
     | 
| 183 | 
         
            +
                    for c in opp_nb_chains:
         
     | 
| 184 | 
         
            +
                        if -1 not in neighbours(self.chains[c]):  # no liberties
         
     | 
| 185 | 
         
            +
                            self.last_capture += self.chains[c]
         
     | 
| 186 | 
         
            +
                            for om in self.chains[c]:
         
     | 
| 187 | 
         
            +
                                self.board[om.coords[1]][om.coords[0]] = -1
         
     | 
| 188 | 
         
            +
                            self.chains[c] = []
         
     | 
| 189 | 
         
            +
                    if ko_or_snapback and len(self.last_capture) == 1 and not ignore_ko:
         
     | 
| 190 | 
         
            +
                        raise IllegalMoveException("Ko")
         
     | 
| 191 | 
         
            +
                    self.prisoners += self.last_capture
         
     | 
| 192 | 
         
            +
             
     | 
| 193 | 
         
            +
                    # suicide: check rules and throw exception if needed
         
     | 
| 194 | 
         
            +
                    if -1 not in neighbours(self.chains[this_chain]):
         
     | 
| 195 | 
         
            +
                        rules = self.rules
         
     | 
| 196 | 
         
            +
                        if len(self.chains[this_chain]) == 1:  # even in new zealand rules, single stone suicide is not allowed
         
     | 
| 197 | 
         
            +
                            raise IllegalMoveException("Single stone suicide")
         
     | 
| 198 | 
         
            +
                        elif (isinstance(rules, str) and rules in ["tromp-taylor", "new zealand"]) or (
         
     | 
| 199 | 
         
            +
                            isinstance(rules, dict) and rules.get("suicide", False)
         
     | 
| 200 | 
         
            +
                        ):
         
     | 
| 201 | 
         
            +
                            self.last_capture += self.chains[this_chain]
         
     | 
| 202 | 
         
            +
                            for om in self.chains[this_chain]:
         
     | 
| 203 | 
         
            +
                                self.board[om.coords[1]][om.coords[0]] = -1
         
     | 
| 204 | 
         
            +
                            self.chains[this_chain] = []
         
     | 
| 205 | 
         
            +
                            self.prisoners += self.last_capture
         
     | 
| 206 | 
         
            +
                        else:  # suicide not allowed by rules
         
     | 
| 207 | 
         
            +
                            raise IllegalMoveException("Suicide")
         
     | 
| 208 | 
         
            +
             
     | 
| 209 | 
         
            +
                # Play a Move from the current position, raise IllegalMoveException if invalid.
         
     | 
| 210 | 
         
            +
                def play(self, move: Move, ignore_ko: bool = False):
         
     | 
| 211 | 
         
            +
                    board_size_x, board_size_y = self.board_size
         
     | 
| 212 | 
         
            +
                    if not move.is_pass and not (0 <= move.coords[0] < board_size_x and 0 <= move.coords[1] < board_size_y):
         
     | 
| 213 | 
         
            +
                        raise IllegalMoveException(f"Move {move} outside of board coordinates")
         
     | 
| 214 | 
         
            +
                    try:
         
     | 
| 215 | 
         
            +
                        self._validate_move_and_update_chains(move, ignore_ko)
         
     | 
| 216 | 
         
            +
                    except IllegalMoveException:
         
     | 
| 217 | 
         
            +
                        self._calculate_groups()
         
     | 
| 218 | 
         
            +
                        raise
         
     | 
| 219 | 
         
            +
                    with self._lock:
         
     | 
| 220 | 
         
            +
                        played_node = self.current_node.play(move)
         
     | 
| 221 | 
         
            +
                        self.current_node = played_node
         
     | 
| 222 | 
         
            +
                    return played_node
         
     | 
| 223 | 
         
            +
             
     | 
| 224 | 
         
            +
                # Insert a list of moves from root, often just adding one.
         
     | 
| 225 | 
         
            +
                def sync_branch(self, moves: List[Move]):
         
     | 
| 226 | 
         
            +
                    node = self.root
         
     | 
| 227 | 
         
            +
                    with self._lock:
         
     | 
| 228 | 
         
            +
                        for move in moves:
         
     | 
| 229 | 
         
            +
                            node = node.play(move)
         
     | 
| 230 | 
         
            +
                    return node
         
     | 
| 231 | 
         
            +
             
     | 
| 232 | 
         
            +
                def set_current_node(self, node):
         
     | 
| 233 | 
         
            +
                    self.current_node = node
         
     | 
| 234 | 
         
            +
                    self._calculate_groups()
         
     | 
| 235 | 
         
            +
             
     | 
| 236 | 
         
            +
                def undo(self, n_times=1, stop_on_mistake=None):
         
     | 
| 237 | 
         
            +
                    break_on_branch = False
         
     | 
| 238 | 
         
            +
                    cn = self.current_node  # avoid race conditions
         
     | 
| 239 | 
         
            +
                    break_on_main_branch = False
         
     | 
| 240 | 
         
            +
                    last_branching_node = cn
         
     | 
| 241 | 
         
            +
                    if n_times == "branch":
         
     | 
| 242 | 
         
            +
                        n_times = 9999
         
     | 
| 243 | 
         
            +
                        break_on_branch = True
         
     | 
| 244 | 
         
            +
                    elif n_times == "main-branch":
         
     | 
| 245 | 
         
            +
                        n_times = 9999
         
     | 
| 246 | 
         
            +
                        break_on_main_branch = True
         
     | 
| 247 | 
         
            +
                    for move in range(n_times):
         
     | 
| 248 | 
         
            +
                        if (
         
     | 
| 249 | 
         
            +
                            stop_on_mistake is not None
         
     | 
| 250 | 
         
            +
                            and cn.points_lost is not None
         
     | 
| 251 | 
         
            +
                            and cn.points_lost >= stop_on_mistake
         
     | 
| 252 | 
         
            +
                            and self.katrain.players_info[cn.player].player_type != PLAYER_AI
         
     | 
| 253 | 
         
            +
                        ):
         
     | 
| 254 | 
         
            +
                            self.set_current_node(cn.parent)
         
     | 
| 255 | 
         
            +
                            return
         
     | 
| 256 | 
         
            +
                        previous_cn = cn
         
     | 
| 257 | 
         
            +
                        if cn.shortcut_from:
         
     | 
| 258 | 
         
            +
                            cn = cn.shortcut_from
         
     | 
| 259 | 
         
            +
                        elif not cn.is_root:
         
     | 
| 260 | 
         
            +
                            cn = cn.parent
         
     | 
| 261 | 
         
            +
                        else:
         
     | 
| 262 | 
         
            +
                            break  # root
         
     | 
| 263 | 
         
            +
                        if break_on_branch and len(cn.children) > 1:
         
     | 
| 264 | 
         
            +
                            break
         
     | 
| 265 | 
         
            +
                        elif break_on_main_branch and cn.ordered_children[0] != previous_cn:  # implies > 1 child
         
     | 
| 266 | 
         
            +
                            last_branching_node = cn
         
     | 
| 267 | 
         
            +
                    if break_on_main_branch:
         
     | 
| 268 | 
         
            +
                        cn = last_branching_node
         
     | 
| 269 | 
         
            +
                    if cn is not self.current_node:
         
     | 
| 270 | 
         
            +
                        self.set_current_node(cn)
         
     | 
| 271 | 
         
            +
             
     | 
| 272 | 
         
            +
                def redo(self, n_times=1, stop_on_mistake=None):
         
     | 
| 273 | 
         
            +
                    cn = self.current_node  # avoid race conditions
         
     | 
| 274 | 
         
            +
                    for move in range(n_times):
         
     | 
| 275 | 
         
            +
                        if cn.children:
         
     | 
| 276 | 
         
            +
                            child = cn.ordered_children[0]
         
     | 
| 277 | 
         
            +
                            shortcut_to = [m for m, v in cn.shortcuts_to if child == v]  # are we about to go to a shortcut node?
         
     | 
| 278 | 
         
            +
                            if shortcut_to:
         
     | 
| 279 | 
         
            +
                                child = shortcut_to[0]
         
     | 
| 280 | 
         
            +
                            cn = child
         
     | 
| 281 | 
         
            +
                        if (
         
     | 
| 282 | 
         
            +
                            move > 0
         
     | 
| 283 | 
         
            +
                            and stop_on_mistake is not None
         
     | 
| 284 | 
         
            +
                            and cn.points_lost is not None
         
     | 
| 285 | 
         
            +
                            and cn.points_lost >= stop_on_mistake
         
     | 
| 286 | 
         
            +
                            and self.katrain.players_info[cn.player].player_type != PLAYER_AI
         
     | 
| 287 | 
         
            +
                        ):
         
     | 
| 288 | 
         
            +
                            self.set_current_node(cn.parent)
         
     | 
| 289 | 
         
            +
                            return
         
     | 
| 290 | 
         
            +
                    if stop_on_mistake is None:
         
     | 
| 291 | 
         
            +
                        self.set_current_node(cn)
         
     | 
| 292 | 
         
            +
             
     | 
| 293 | 
         
            +
                @property
         
     | 
| 294 | 
         
            +
                def komi(self):
         
     | 
| 295 | 
         
            +
                    return self.root.komi
         
     | 
| 296 | 
         
            +
             
     | 
| 297 | 
         
            +
                @property
         
     | 
| 298 | 
         
            +
                def board_size(self):
         
     | 
| 299 | 
         
            +
                    return self.root.board_size
         
     | 
| 300 | 
         
            +
             
     | 
| 301 | 
         
            +
                @property
         
     | 
| 302 | 
         
            +
                def stones(self):
         
     | 
| 303 | 
         
            +
                    with self._lock:
         
     | 
| 304 | 
         
            +
                        return sum(self.chains, [])
         
     | 
| 305 | 
         
            +
             
     | 
| 306 | 
         
            +
                @property
         
     | 
| 307 | 
         
            +
                def end_result(self):
         
     | 
| 308 | 
         
            +
                    if self.current_node.end_state:
         
     | 
| 309 | 
         
            +
                        return self.current_node.end_state
         
     | 
| 310 | 
         
            +
                    if self.current_node.parent and self.current_node.is_pass and self.current_node.parent.is_pass:
         
     | 
| 311 | 
         
            +
                        return self.manual_score or i18n._("board-game-end")   #홍익 살펴볼곳
         
     | 
| 312 | 
         
            +
             
     | 
| 313 | 
         
            +
                @property
         
     | 
| 314 | 
         
            +
                def prisoner_count(
         
     | 
| 315 | 
         
            +
                    self,
         
     | 
| 316 | 
         
            +
                ) -> Dict:  # returns prisoners that are of a certain colour as {B: black stones captures, W: white stones captures}
         
     | 
| 317 | 
         
            +
                    return {player: sum([m.player == player for m in self.prisoners]) for player in Move.PLAYERS}
         
     | 
| 318 | 
         
            +
             
     | 
| 319 | 
         
            +
                @property
         
     | 
| 320 | 
         
            +
                def rules(self):
         
     | 
| 321 | 
         
            +
                    return HongikAIEngine.get_rules(self.root.ruleset)
         
     | 
| 322 | 
         
            +
             
     | 
| 323 | 
         
            +
                @property
         
     | 
| 324 | 
         
            +
                def manual_score(self):
         
     | 
| 325 | 
         
            +
                    rules = self.rules
         
     | 
| 326 | 
         
            +
                    if (
         
     | 
| 327 | 
         
            +
                        not self.current_node.ownership
         
     | 
| 328 | 
         
            +
                        or str(rules).lower() not in ["jp", "japanese"]
         
     | 
| 329 | 
         
            +
                        or not self.current_node.parent
         
     | 
| 330 | 
         
            +
                        or not self.current_node.parent.ownership
         
     | 
| 331 | 
         
            +
                    ):
         
     | 
| 332 | 
         
            +
                        if not self.current_node.score:
         
     | 
| 333 | 
         
            +
                            return None
         
     | 
| 334 | 
         
            +
                        return self.current_node.format_score(round(2 * self.current_node.score) / 2) + "?"
         
     | 
| 335 | 
         
            +
                    board_size_x, board_size_y = self.board_size
         
     | 
| 336 | 
         
            +
                    mean_ownership = [(c + p) / 2 for c, p in zip(self.current_node.ownership, self.current_node.parent.ownership)]
         
     | 
| 337 | 
         
            +
                    ownership_grid = var_to_grid(mean_ownership, (board_size_x, board_size_y))
         
     | 
| 338 | 
         
            +
                    stones = {m.coords: m.player for m in self.stones}
         
     | 
| 339 | 
         
            +
                    lo_threshold = 0.15
         
     | 
| 340 | 
         
            +
                    hi_threshold = 0.85
         
     | 
| 341 | 
         
            +
                    max_unknown = 10
         
     | 
| 342 | 
         
            +
                    max_dame = 4 * (board_size_x + board_size_y)
         
     | 
| 343 | 
         
            +
             
     | 
| 344 | 
         
            +
                    def japanese_score_square(square, owner):
         
     | 
| 345 | 
         
            +
                        player = stones.get(square, None)
         
     | 
| 346 | 
         
            +
                        if (
         
     | 
| 347 | 
         
            +
                            (player == "B" and owner > hi_threshold)
         
     | 
| 348 | 
         
            +
                            or (player == "W" and owner < -hi_threshold)
         
     | 
| 349 | 
         
            +
                            or abs(owner) < lo_threshold
         
     | 
| 350 | 
         
            +
                        ):
         
     | 
| 351 | 
         
            +
                            return 0  # dame or own stones
         
     | 
| 352 | 
         
            +
                        if player is None and abs(owner) >= hi_threshold:
         
     | 
| 353 | 
         
            +
                            return round(owner)  # surrounded empty intersection
         
     | 
| 354 | 
         
            +
                        if (player == "B" and owner < -hi_threshold) or (player == "W" and owner > hi_threshold):
         
     | 
| 355 | 
         
            +
                            return 2 * round(owner)  # captured stone
         
     | 
| 356 | 
         
            +
                        return math.nan  # unknown!
         
     | 
| 357 | 
         
            +
             
     | 
| 358 | 
         
            +
                    scored_squares = [
         
     | 
| 359 | 
         
            +
                        japanese_score_square((x, y), ownership_grid[y][x])
         
     | 
| 360 | 
         
            +
                        for y in range(board_size_y)
         
     | 
| 361 | 
         
            +
                        for x in range(board_size_x)
         
     | 
| 362 | 
         
            +
                    ]
         
     | 
| 363 | 
         
            +
                    num_sq = {t: sum([s == t for s in scored_squares]) for t in [-2, -1, 0, 1, 2]}
         
     | 
| 364 | 
         
            +
                    num_unkn = sum(math.isnan(s) for s in scored_squares)
         
     | 
| 365 | 
         
            +
                    prisoners = self.prisoner_count
         
     | 
| 366 | 
         
            +
                    score = sum([t * n for t, n in num_sq.items()]) + prisoners["W"] - prisoners["B"] - self.komi
         
     | 
| 367 | 
         
            +
                    self.katrain.log(
         
     | 
| 368 | 
         
            +
                        f"Manual Scoring: {num_sq} score by square with {num_unkn} unknown, {prisoners} captures, and {self.komi} komi -> score = {score}",
         
     | 
| 369 | 
         
            +
                        OUTPUT_DEBUG,
         
     | 
| 370 | 
         
            +
                    )
         
     | 
| 371 | 
         
            +
                    if num_unkn > max_unknown or (num_sq[0] - len(stones)) > max_dame:
         
     | 
| 372 | 
         
            +
                        return None
         
     | 
| 373 | 
         
            +
                    return self.current_node.format_score(score)
         
     | 
| 374 | 
         
            +
             
     | 
| 375 | 
         
            +
                def __repr__(self):
         
     | 
| 376 | 
         
            +
                    return (
         
     | 
| 377 | 
         
            +
                        "\n".join("".join(self.chains[c][0].player if c >= 0 else "-" for c in line) for line in self.board)
         
     | 
| 378 | 
         
            +
                        + f"\ncaptures: {self.prisoner_count}"
         
     | 
| 379 | 
         
            +
                    )
         
     | 
| 380 | 
         
            +
             
     | 
| 381 | 
         
            +
                def update_root_properties(self):
         
     | 
| 382 | 
         
            +
                    def player_name(player_info):
         
     | 
| 383 | 
         
            +
                        if player_info.name and player_info.player_type == PLAYER_HUMAN:
         
     | 
| 384 | 
         
            +
                            return player_info.name
         
     | 
| 385 | 
         
            +
                        else:
         
     | 
| 386 | 
         
            +
                            return f"{i18n._(player_info.player_type)} ({i18n._(player_info.player_subtype)}){SGF_INTERNAL_COMMENTS_MARKER}"
         
     | 
| 387 | 
         
            +
             
     | 
| 388 | 
         
            +
                    root_properties = self.root.properties
         
     | 
| 389 | 
         
            +
                    x_properties = {}
         
     | 
| 390 | 
         
            +
                    for bw in "BW":
         
     | 
| 391 | 
         
            +
                        if not self.external_game:
         
     | 
| 392 | 
         
            +
                            x_properties["P" + bw] = player_name(self.katrain.players_info[bw])
         
     | 
| 393 | 
         
            +
                            player_info = self.katrain.players_info[bw]
         
     | 
| 394 | 
         
            +
                            if player_info.player_type == PLAYER_AI:
         
     | 
| 395 | 
         
            +
                                x_properties[bw + "R"] = rank_label(player_info.calculated_rank)
         
     | 
| 396 | 
         
            +
                    if "+" in str(self.end_result):
         
     | 
| 397 | 
         
            +
                        x_properties["RE"] = self.end_result
         
     | 
| 398 | 
         
            +
                    self.root.properties = {**root_properties, **{k: [v] for k, v in x_properties.items()}}
         
     | 
| 399 | 
         
            +
             
     | 
| 400 | 
         
            +
                def generate_filename(self):
         
     | 
| 401 | 
         
            +
                    self.update_root_properties()
         
     | 
| 402 | 
         
            +
                    player_names = {
         
     | 
| 403 | 
         
            +
                        bw: re.sub(r"[\u200b\u3164'<>:\"/\\|?*]", "", self.root.get_property("P" + bw, bw)) for bw in "BW"
         
     | 
| 404 | 
         
            +
                    }
         
     | 
| 405 | 
         
            +
                    base_game_name = f"{PROGRAM_NAME}_{player_names['B']} vs {player_names['W']}"
         
     | 
| 406 | 
         
            +
                    return f"{base_game_name} {self.game_id}.sgf"
         
     | 
| 407 | 
         
            +
             
     | 
| 408 | 
         
            +
                def write_sgf(self, filename: str, trainer_config: Optional[Dict] = None):
         
     | 
| 409 | 
         
            +
                    if trainer_config is None:
         
     | 
| 410 | 
         
            +
                        trainer_config = self.katrain.config("trainer", {})
         
     | 
| 411 | 
         
            +
                    save_feedback = trainer_config.get("save_feedback", False)
         
     | 
| 412 | 
         
            +
                    eval_thresholds = trainer_config["eval_thresholds"]
         
     | 
| 413 | 
         
            +
                    save_analysis = trainer_config.get("save_analysis", False)
         
     | 
| 414 | 
         
            +
                    save_marks = trainer_config.get("save_marks", False)
         
     | 
| 415 | 
         
            +
                    self.update_root_properties()
         
     | 
| 416 | 
         
            +
                    show_dots_for = {
         
     | 
| 417 | 
         
            +
                        bw: trainer_config.get("eval_show_ai", True) or self.katrain.players_info[bw].human for bw in "BW"
         
     | 
| 418 | 
         
            +
                    }
         
     | 
| 419 | 
         
            +
                    sgf = self.root.sgf(
         
     | 
| 420 | 
         
            +
                        save_comments_player=show_dots_for,
         
     | 
| 421 | 
         
            +
                        save_comments_class=save_feedback,
         
     | 
| 422 | 
         
            +
                        eval_thresholds=eval_thresholds,
         
     | 
| 423 | 
         
            +
                        save_analysis=save_analysis,
         
     | 
| 424 | 
         
            +
                        save_marks=save_marks,
         
     | 
| 425 | 
         
            +
                    )
         
     | 
| 426 | 
         
            +
                    self.sgf_filename = filename
         
     | 
| 427 | 
         
            +
                    os.makedirs(os.path.dirname(filename), exist_ok=True)
         
     | 
| 428 | 
         
            +
                    with open(filename, "w", encoding="utf-8") as f:
         
     | 
| 429 | 
         
            +
                        f.write(sgf)
         
     | 
| 430 | 
         
            +
                    return i18n._("sgf written").format(file_name=filename)
         
     | 
| 431 | 
         
            +
             
     | 
| 432 | 
         
            +
             
     | 
| 433 | 
         
            +
            class Game(BaseGame):
         
     | 
| 434 | 
         
            +
                """Extensions related to analysis etc."""
         
     | 
| 435 | 
         
            +
             
     | 
| 436 | 
         
            +
                def __init__(
         
     | 
| 437 | 
         
            +
                    self,
         
     | 
| 438 | 
         
            +
                    katrain,
         
     | 
| 439 | 
         
            +
                    engine: Union[Dict, HongikAIEngine],
         
     | 
| 440 | 
         
            +
                    move_tree: GameNode = None,
         
     | 
| 441 | 
         
            +
                    analyze_fast=False,
         
     | 
| 442 | 
         
            +
                    game_properties: Optional[Dict] = None,
         
     | 
| 443 | 
         
            +
                    sgf_filename=None,
         
     | 
| 444 | 
         
            +
                ):
         
     | 
| 445 | 
         
            +
                    super().__init__(
         
     | 
| 446 | 
         
            +
                        katrain=katrain, move_tree=move_tree, game_properties=game_properties, sgf_filename=sgf_filename
         
     | 
| 447 | 
         
            +
                    )
         
     | 
| 448 | 
         
            +
                    if not isinstance(engine, Dict):
         
     | 
| 449 | 
         
            +
                        engine = {"B": engine, "W": engine}
         
     | 
| 450 | 
         
            +
                    self.engines = engine
         
     | 
| 451 | 
         
            +
             
     | 
| 452 | 
         
            +
                    self.insert_mode = False
         
     | 
| 453 | 
         
            +
                    self.insert_after = None
         
     | 
| 454 | 
         
            +
                    self.region_of_interest = None
         
     | 
| 455 | 
         
            +
             
     | 
| 456 | 
         
            +
                    threading.Thread(
         
     | 
| 457 | 
         
            +
                        target=lambda: self.analyze_all_nodes(analyze_fast=analyze_fast, even_if_present=False),
         
     | 
| 458 | 
         
            +
                        daemon=True,
         
     | 
| 459 | 
         
            +
                    ).start()  # return faster, but bypass Kivy Clock
         
     | 
| 460 | 
         
            +
             
     | 
| 461 | 
         
            +
                def analyze_all_nodes(self, priority=PRIORITY_GAME_ANALYSIS, analyze_fast=False, even_if_present=True):
         
     | 
| 462 | 
         
            +
                    for node in self.root.nodes_in_tree:
         
     | 
| 463 | 
         
            +
                        # forced, or not present, or something went wrong in loading
         
     | 
| 464 | 
         
            +
                        if even_if_present or not node.analysis_from_sgf or not node.load_analysis():
         
     | 
| 465 | 
         
            +
                            node.clear_analysis()
         
     | 
| 466 | 
         
            +
                            node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast)
         
     | 
| 467 | 
         
            +
             
     | 
| 468 | 
         
            +
                def set_current_node(self, node):
         
     | 
| 469 | 
         
            +
                    if self.insert_mode:
         
     | 
| 470 | 
         
            +
                        self.katrain.controls.set_status(i18n._("finish inserting before navigating"), STATUS_ERROR)
         
     | 
| 471 | 
         
            +
                        return
         
     | 
| 472 | 
         
            +
                    super().set_current_node(node)
         
     | 
| 473 | 
         
            +
             
     | 
| 474 | 
         
            +
                def undo(self, n_times=1, stop_on_mistake=None):
         
     | 
| 475 | 
         
            +
                    if self.insert_mode:  # in insert mode, undo = delete
         
     | 
| 476 | 
         
            +
                        cn = self.current_node  # avoid race conditions
         
     | 
| 477 | 
         
            +
                        if n_times == 1 and cn not in self.insert_after.nodes_from_root:
         
     | 
| 478 | 
         
            +
                            cn.parent.children = [c for c in cn.parent.children if c != cn]
         
     | 
| 479 | 
         
            +
                            self.current_node = cn.parent
         
     | 
| 480 | 
         
            +
                            self._calculate_groups()
         
     | 
| 481 | 
         
            +
                        return
         
     | 
| 482 | 
         
            +
                    super().undo(n_times=n_times, stop_on_mistake=stop_on_mistake)
         
     | 
| 483 | 
         
            +
             
     | 
| 484 | 
         
            +
                def reset_current_analysis(self):
         
     | 
| 485 | 
         
            +
                    cn = self.current_node
         
     | 
| 486 | 
         
            +
                    engine = self.engines[cn.next_player]
         
     | 
| 487 | 
         
            +
                    engine.terminate_queries(cn)
         
     | 
| 488 | 
         
            +
                    cn.clear_analysis()
         
     | 
| 489 | 
         
            +
                    cn.analyze(engine)
         
     | 
| 490 | 
         
            +
             
     | 
| 491 | 
         
            +
                def redo(self, n_times=1, stop_on_mistake=None):
         
     | 
| 492 | 
         
            +
                    if self.insert_mode:
         
     | 
| 493 | 
         
            +
                        return
         
     | 
| 494 | 
         
            +
                    super().redo(n_times=n_times, stop_on_mistake=stop_on_mistake)
         
     | 
| 495 | 
         
            +
             
     | 
| 496 | 
         
            +
                def set_insert_mode(self, mode):
         
     | 
| 497 | 
         
            +
                    if mode == "toggle":
         
     | 
| 498 | 
         
            +
                        mode = not self.insert_mode
         
     | 
| 499 | 
         
            +
                    if mode == self.insert_mode:
         
     | 
| 500 | 
         
            +
                        return
         
     | 
| 501 | 
         
            +
                    self.insert_mode = mode
         
     | 
| 502 | 
         
            +
                    if mode:
         
     | 
| 503 | 
         
            +
                        children = self.current_node.ordered_children
         
     | 
| 504 | 
         
            +
                        if not children:
         
     | 
| 505 | 
         
            +
                            self.insert_mode = False
         
     | 
| 506 | 
         
            +
                        else:
         
     | 
| 507 | 
         
            +
                            self.insert_after = self.current_node.ordered_children[0]
         
     | 
| 508 | 
         
            +
                            self.katrain.controls.set_status(i18n._("starting insert mode"), STATUS_INFO)
         
     | 
| 509 | 
         
            +
                    else:
         
     | 
| 510 | 
         
            +
                        copy_from_node = self.insert_after
         
     | 
| 511 | 
         
            +
                        copy_to_node = self.current_node
         
     | 
| 512 | 
         
            +
                        num_copied = 0
         
     | 
| 513 | 
         
            +
                        if copy_to_node != self.insert_after.parent:
         
     | 
| 514 | 
         
            +
                            above_insertion_root = self.insert_after.parent.nodes_from_root
         
     | 
| 515 | 
         
            +
                            already_inserted_moves = [
         
     | 
| 516 | 
         
            +
                                n.move for n in copy_to_node.nodes_from_root if n not in above_insertion_root and n.move
         
     | 
| 517 | 
         
            +
                            ]
         
     | 
| 518 | 
         
            +
                            try:
         
     | 
| 519 | 
         
            +
                                while True:
         
     | 
| 520 | 
         
            +
                                    for m in copy_from_node.move_with_placements:
         
     | 
| 521 | 
         
            +
                                        if m not in already_inserted_moves:
         
     | 
| 522 | 
         
            +
                                            self._validate_move_and_update_chains(m, True)
         
     | 
| 523 | 
         
            +
                                            # this inserts
         
     | 
| 524 | 
         
            +
                                            copy_to_node = GameNode(
         
     | 
| 525 | 
         
            +
                                                parent=copy_to_node, properties=copy.deepcopy(copy_from_node.properties)
         
     | 
| 526 | 
         
            +
                                            )
         
     | 
| 527 | 
         
            +
                                            num_copied += 1
         
     | 
| 528 | 
         
            +
                                    if not copy_from_node.children:
         
     | 
| 529 | 
         
            +
                                        break
         
     | 
| 530 | 
         
            +
                                    copy_from_node = copy_from_node.ordered_children[0]
         
     | 
| 531 | 
         
            +
                            except IllegalMoveException:
         
     | 
| 532 | 
         
            +
                                pass  # illegal move = stop
         
     | 
| 533 | 
         
            +
                            self._calculate_groups()  # recalculate groups
         
     | 
| 534 | 
         
            +
                            self.katrain.controls.set_status(
         
     | 
| 535 | 
         
            +
                                i18n._("ending insert mode").format(num_copied=num_copied), STATUS_INFO
         
     | 
| 536 | 
         
            +
                            )
         
     | 
| 537 | 
         
            +
                            self.analyze_all_nodes(analyze_fast=True, even_if_present=False)
         
     | 
| 538 | 
         
            +
                        else:
         
     | 
| 539 | 
         
            +
                            self.katrain.controls.set_status("", STATUS_INFO)
         
     | 
| 540 | 
         
            +
                    self.katrain.controls.move_tree.insert_node = self.insert_after if self.insert_mode else None
         
     | 
| 541 | 
         
            +
                    self.katrain.controls.move_tree.redraw()
         
     | 
| 542 | 
         
            +
                    self.katrain.update_state(redraw_board=True)
         
     | 
| 543 | 
         
            +
             
     | 
| 544 | 
         
            +
                # Play a Move from the current position, raise IllegalMoveException if invalid.
         
     | 
| 545 | 
         
            +
                def play(self, move: Move, ignore_ko: bool = False, analyze=True):
         
     | 
| 546 | 
         
            +
                    played_node = super().play(move, ignore_ko)
         
     | 
| 547 | 
         
            +
                    if analyze:
         
     | 
| 548 | 
         
            +
                        if self.region_of_interest:
         
     | 
| 549 | 
         
            +
                            played_node.analyze(self.engines[played_node.next_player], analyze_fast=True)
         
     | 
| 550 | 
         
            +
                            played_node.analyze(self.engines[played_node.next_player], region_of_interest=self.region_of_interest)
         
     | 
| 551 | 
         
            +
                        else:
         
     | 
| 552 | 
         
            +
                            played_node.analyze(self.engines[played_node.next_player])
         
     | 
| 553 | 
         
            +
                    return played_node
         
     | 
| 554 | 
         
            +
             
     | 
| 555 | 
         
            +
                def set_region_of_interest(self, region_of_interest):
         
     | 
| 556 | 
         
            +
                    x1, x2, y1, y2 = region_of_interest
         
     | 
| 557 | 
         
            +
                    xmin, xmax = min(x1, x2), max(x1, x2)
         
     | 
| 558 | 
         
            +
                    ymin, ymax = min(y1, y2), max(y1, y2)
         
     | 
| 559 | 
         
            +
                    szx, szy = self.board_size
         
     | 
| 560 | 
         
            +
                    if not (xmin == xmax and ymin == ymax) and not (xmax - xmin + 1 >= szx and ymax - ymin + 1 >= szy):
         
     | 
| 561 | 
         
            +
                        self.region_of_interest = [xmin, xmax, ymin, ymax]
         
     | 
| 562 | 
         
            +
                    else:
         
     | 
| 563 | 
         
            +
                        self.region_of_interest = None
         
     | 
| 564 | 
         
            +
                    self.katrain.controls.set_status("", OUTPUT_INFO)
         
     | 
| 565 | 
         
            +
             
     | 
| 566 | 
         
            +
                def analyze_extra(self, mode, **kwargs):
         
     | 
| 567 | 
         
            +
                    stones = {s.coords for s in self.stones}
         
     | 
| 568 | 
         
            +
                    cn = self.current_node
         
     | 
| 569 | 
         
            +
             
     | 
| 570 | 
         
            +
                    if mode == "stop":
         
     | 
| 571 | 
         
            +
                        self.katrain.pondering = False
         
     | 
| 572 | 
         
            +
                        for e in set(self.engines.values()):
         
     | 
| 573 | 
         
            +
                            e.stop_pondering()
         
     | 
| 574 | 
         
            +
                            e.terminate_queries()
         
     | 
| 575 | 
         
            +
                        return
         
     | 
| 576 | 
         
            +
             
     | 
| 577 | 
         
            +
                    engine = self.engines[cn.next_player]
         
     | 
| 578 | 
         
            +
             
     | 
| 579 | 
         
            +
                    if mode == "ponder":
         
     | 
| 580 | 
         
            +
                        cn.analyze(
         
     | 
| 581 | 
         
            +
                            engine,
         
     | 
| 582 | 
         
            +
                            ponder=True,
         
     | 
| 583 | 
         
            +
                            priority=PRIORITY_EXTRA_ANALYSIS,
         
     | 
| 584 | 
         
            +
                            region_of_interest=self.region_of_interest,
         
     | 
| 585 | 
         
            +
                            time_limit=False,
         
     | 
| 586 | 
         
            +
                        )
         
     | 
| 587 | 
         
            +
                        return
         
     | 
| 588 | 
         
            +
             
     | 
| 589 | 
         
            +
                    if mode == "extra":
         
     | 
| 590 | 
         
            +
                        visits = cn.analysis_visits_requested + engine.config["max_visits"]
         
     | 
| 591 | 
         
            +
                        self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
         
     | 
| 592 | 
         
            +
                        cn.analyze(
         
     | 
| 593 | 
         
            +
                            engine,
         
     | 
| 594 | 
         
            +
                            visits=visits,
         
     | 
| 595 | 
         
            +
                            priority=PRIORITY_EXTRA_ANALYSIS,
         
     | 
| 596 | 
         
            +
                            region_of_interest=self.region_of_interest,
         
     | 
| 597 | 
         
            +
                            time_limit=False,
         
     | 
| 598 | 
         
            +
                        )
         
     | 
| 599 | 
         
            +
                        return
         
     | 
| 600 | 
         
            +
             
     | 
| 601 | 
         
            +
                    if mode == "game":
         
     | 
| 602 | 
         
            +
                        nodes = self.root.nodes_in_tree
         
     | 
| 603 | 
         
            +
                        only_mistakes = kwargs.get("mistakes_only", False)
         
     | 
| 604 | 
         
            +
                        move_range = kwargs.get("move_range", None)
         
     | 
| 605 | 
         
            +
                        if move_range:
         
     | 
| 606 | 
         
            +
                            if move_range[1] < move_range[0]:
         
     | 
| 607 | 
         
            +
                                move_range = reversed(move_range)
         
     | 
| 608 | 
         
            +
                        threshold = self.katrain.config("trainer/eval_thresholds")[-4]
         
     | 
| 609 | 
         
            +
                        if "visits" in kwargs:
         
     | 
| 610 | 
         
            +
                            visits = kwargs["visits"]
         
     | 
| 611 | 
         
            +
                        else:
         
     | 
| 612 | 
         
            +
                            min_visits = min(node.analysis_visits_requested for node in nodes)
         
     | 
| 613 | 
         
            +
                            visits = min_visits + engine.config["max_visits"]
         
     | 
| 614 | 
         
            +
                        for node in nodes:
         
     | 
| 615 | 
         
            +
                            max_point_loss = max(c.points_lost or 0 for c in [node] + node.children)
         
     | 
| 616 | 
         
            +
                            if only_mistakes and max_point_loss <= threshold:
         
     | 
| 617 | 
         
            +
                                continue
         
     | 
| 618 | 
         
            +
                            if move_range and (not node.depth - 1 in range(move_range[0], move_range[1] + 1)):
         
     | 
| 619 | 
         
            +
                                continue
         
     | 
| 620 | 
         
            +
                            node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False, report_every=None)
         
     | 
| 621 | 
         
            +
                        if not move_range:
         
     | 
| 622 | 
         
            +
                            self.katrain.controls.set_status(i18n._("game re-analysis").format(visits=visits), STATUS_ANALYSIS)
         
     | 
| 623 | 
         
            +
                        else:
         
     | 
| 624 | 
         
            +
                            self.katrain.controls.set_status(
         
     | 
| 625 | 
         
            +
                                i18n._("move range analysis").format(
         
     | 
| 626 | 
         
            +
                                    start_move=move_range[0], end_move=move_range[1], visits=visits
         
     | 
| 627 | 
         
            +
                                ),
         
     | 
| 628 | 
         
            +
                                STATUS_ANALYSIS,
         
     | 
| 629 | 
         
            +
                            )
         
     | 
| 630 | 
         
            +
                        return
         
     | 
| 631 | 
         
            +
             
     | 
| 632 | 
         
            +
                    elif mode == "sweep":
         
     | 
| 633 | 
         
            +
                        board_size_x, board_size_y = self.board_size
         
     | 
| 634 | 
         
            +
             
     | 
| 635 | 
         
            +
                        if cn.analysis_exists:
         
     | 
| 636 | 
         
            +
                            policy_grid = (
         
     | 
| 637 | 
         
            +
                                var_to_grid(self.current_node.policy, size=(board_size_x, board_size_y))
         
     | 
| 638 | 
         
            +
                                if self.current_node.policy
         
     | 
| 639 | 
         
            +
                                else None
         
     | 
| 640 | 
         
            +
                            )
         
     | 
| 641 | 
         
            +
                            analyze_moves = sorted(
         
     | 
| 642 | 
         
            +
                                [
         
     | 
| 643 | 
         
            +
                                    Move(coords=(x, y), player=cn.next_player)
         
     | 
| 644 | 
         
            +
                                    for x in range(board_size_x)
         
     | 
| 645 | 
         
            +
                                    for y in range(board_size_y)
         
     | 
| 646 | 
         
            +
                                    if (policy_grid is None and (x, y) not in stones) or policy_grid[y][x] >= 0
         
     | 
| 647 | 
         
            +
                                ],
         
     | 
| 648 | 
         
            +
                                key=lambda mv: -policy_grid[mv.coords[1]][mv.coords[0]],
         
     | 
| 649 | 
         
            +
                            )
         
     | 
| 650 | 
         
            +
                        else:
         
     | 
| 651 | 
         
            +
                            analyze_moves = [
         
     | 
| 652 | 
         
            +
                                Move(coords=(x, y), player=cn.next_player)
         
     | 
| 653 | 
         
            +
                                for x in range(board_size_x)
         
     | 
| 654 | 
         
            +
                                for y in range(board_size_y)
         
     | 
| 655 | 
         
            +
                                if (x, y) not in stones
         
     | 
| 656 | 
         
            +
                            ]
         
     | 
| 657 | 
         
            +
                        visits = engine.config["fast_visits"]
         
     | 
| 658 | 
         
            +
                        self.katrain.controls.set_status(i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS)
         
     | 
| 659 | 
         
            +
                        priority = PRIORITY_SWEEP
         
     | 
| 660 | 
         
            +
                    elif mode in ["equalize", "alternative", "local"]:
         
     | 
| 661 | 
         
            +
                        if not cn.analysis_complete and mode != "local":
         
     | 
| 662 | 
         
            +
                            self.katrain.controls.set_status(i18n._("wait-before-extra-analysis"), STATUS_INFO, self.current_node)
         
     | 
| 663 | 
         
            +
                            return
         
     | 
| 664 | 
         
            +
                        if mode == "alternative":  # also do a quick update on current candidates so it doesn't look too weird
         
     | 
| 665 | 
         
            +
                            self.katrain.controls.set_status(i18n._("alternative analysis"), STATUS_ANALYSIS)
         
     | 
| 666 | 
         
            +
                            cn.analyze(engine, priority=PRIORITY_ALTERNATIVES, time_limit=False, find_alternatives="alternative")
         
     | 
| 667 | 
         
            +
                            visits = engine.config["fast_visits"]
         
     | 
| 668 | 
         
            +
                        else:  # equalize
         
     | 
| 669 | 
         
            +
                            visits = max(d["visits"] for d in cn.analysis["moves"].values())
         
     | 
| 670 | 
         
            +
                            self.katrain.controls.set_status(i18n._("equalizing analysis").format(visits=visits), STATUS_ANALYSIS)
         
     | 
| 671 | 
         
            +
                        priority = PRIORITY_EQUALIZE
         
     | 
| 672 | 
         
            +
                        analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items()]
         
     | 
| 673 | 
         
            +
                    else:
         
     | 
| 674 | 
         
            +
                        raise ValueError("Invalid analysis mode")
         
     | 
| 675 | 
         
            +
             
     | 
| 676 | 
         
            +
                    for move in analyze_moves:
         
     | 
| 677 | 
         
            +
                        if cn.analysis["moves"].get(move.gtp(), {"visits": 0})["visits"] < visits:
         
     | 
| 678 | 
         
            +
                            cn.analyze(
         
     | 
| 679 | 
         
            +
                                engine, priority=priority, visits=visits, refine_move=move, time_limit=False
         
     | 
| 680 | 
         
            +
                            )  # explicitly requested so take as long as you need
         
     | 
| 681 | 
         
            +
             
     | 
| 682 | 
         
            +
                def selfplay(self, until_move, target_b_advantage=None):
         
     | 
| 683 | 
         
            +
                    cn = self.current_node
         
     | 
| 684 | 
         
            +
             
     | 
| 685 | 
         
            +
                    if target_b_advantage is not None:
         
     | 
| 686 | 
         
            +
                        analysis_kwargs = {"visits": max(25, self.katrain.config("engine/fast_visits"))}
         
     | 
| 687 | 
         
            +
                        engine_settings = {"wideRootNoise": 0.03}
         
     | 
| 688 | 
         
            +
                    else:
         
     | 
| 689 | 
         
            +
                        analysis_kwargs = engine_settings = {}
         
     | 
| 690 | 
         
            +
             
     | 
| 691 | 
         
            +
                    def set_analysis(node, result):
         
     | 
| 692 | 
         
            +
                        node.set_analysis(result)
         
     | 
| 693 | 
         
            +
                        analyze_and_play(node)
         
     | 
| 694 | 
         
            +
             
     | 
| 695 | 
         
            +
                    def request_analysis_for_node(node):
         
     | 
| 696 | 
         
            +
                        self.engines[node.player].request_analysis(
         
     | 
| 697 | 
         
            +
                            node,
         
     | 
| 698 | 
         
            +
                            callback=lambda result, _partial: set_analysis(node, result),
         
     | 
| 699 | 
         
            +
                            priority=PRIORITY_DEFAULT,
         
     | 
| 700 | 
         
            +
                            analyze_fast=True,
         
     | 
| 701 | 
         
            +
                            extra_settings=engine_settings,
         
     | 
| 702 | 
         
            +
                            **analysis_kwargs,
         
     | 
| 703 | 
         
            +
                        )
         
     | 
| 704 | 
         
            +
             
     | 
| 705 | 
         
            +
                    def analyze_and_play(node):
         
     | 
| 706 | 
         
            +
                        nonlocal cn, engine_settings
         
     | 
| 707 | 
         
            +
                        candidates = node.candidate_moves
         
     | 
| 708 | 
         
            +
                        if self.katrain.game is not self:
         
     | 
| 709 | 
         
            +
                            return  # a new game happened
         
     | 
| 710 | 
         
            +
                        ai_thoughts = "Move generated by AI self-play\n"
         
     | 
| 711 | 
         
            +
                        if until_move != "end" and target_b_advantage is not None:  # setup pos
         
     | 
| 712 | 
         
            +
                            if node.depth >= until_move or candidates[0]["move"] == "pass":
         
     | 
| 713 | 
         
            +
                                self.set_current_node(node)
         
     | 
| 714 | 
         
            +
                                return
         
     | 
| 715 | 
         
            +
                            target_score = cn.score + (node.depth - cn.depth + 1) * (target_b_advantage - cn.score) / (
         
     | 
| 716 | 
         
            +
                                until_move - cn.depth
         
     | 
| 717 | 
         
            +
                            )
         
     | 
| 718 | 
         
            +
                            max_loss = 5
         
     | 
| 719 | 
         
            +
                            stddev = min(3, 0.5 + (until_move - node.depth) * 0.15)
         
     | 
| 720 | 
         
            +
                            ai_thoughts += f"Selecting moves aiming at score {target_score:.1f} +/- {stddev:.2f} with < {max_loss} points lost\n"
         
     | 
| 721 | 
         
            +
                            if abs(node.score - target_score) < 3 * stddev:
         
     | 
| 722 | 
         
            +
                                weighted_cands = [
         
     | 
| 723 | 
         
            +
                                    (
         
     | 
| 724 | 
         
            +
                                        move,
         
     | 
| 725 | 
         
            +
                                        math.exp(-0.5 * (abs(move["scoreLead"] - target_score) / stddev) ** 2)
         
     | 
| 726 | 
         
            +
                                        * math.exp(-0.5 * (min(0, move["pointsLost"]) / max_loss) ** 2),
         
     | 
| 727 | 
         
            +
                                    )
         
     | 
| 728 | 
         
            +
                                    for i, move in enumerate(candidates)
         
     | 
| 729 | 
         
            +
                                    if move["pointsLost"] < max_loss or i == 0
         
     | 
| 730 | 
         
            +
                                ]
         
     | 
| 731 | 
         
            +
                                move_info = weighted_selection_without_replacement(weighted_cands, 1)[0][0]
         
     | 
| 732 | 
         
            +
                                for move, wt in weighted_cands:
         
     | 
| 733 | 
         
            +
                                    self.katrain.log(
         
     | 
| 734 | 
         
            +
                                        f"{'* ' if move_info == move else '  '} {move['move']} {move['scoreLead']} {wt}",
         
     | 
| 735 | 
         
            +
                                        OUTPUT_EXTRA_DEBUG,
         
     | 
| 736 | 
         
            +
                                    )
         
     | 
| 737 | 
         
            +
                                    ai_thoughts += f"Move option: {move['move']} score {move['scoreLead']:.2f} loss {move['pointsLost']:.2f} weight {wt:.3e}\n"
         
     | 
| 738 | 
         
            +
                            else:  # we're a bit lost, far away from target, just push it closer
         
     | 
| 739 | 
         
            +
                                move_info = min(candidates, key=lambda move: abs(move["scoreLead"] - target_score))
         
     | 
| 740 | 
         
            +
                                self.katrain.log(
         
     | 
| 741 | 
         
            +
                                    f"* Played {move_info['move']} {move_info['scoreLead']} because score deviation between current score {node.score} and target score {target_score} > {3*stddev}",
         
     | 
| 742 | 
         
            +
                                    OUTPUT_EXTRA_DEBUG,
         
     | 
| 743 | 
         
            +
                                )
         
     | 
| 744 | 
         
            +
                                ai_thoughts += f"Move played to close difference between score {node.score:.1f} and target {target_score:.1f} quickly."
         
     | 
| 745 | 
         
            +
             
     | 
| 746 | 
         
            +
                            self.katrain.log(
         
     | 
| 747 | 
         
            +
                                f"Self-play until {until_move} target {target_b_advantage}: {len(candidates)} candidates -> move {move_info['move']} score {move_info['scoreLead']} point loss {move_info['pointsLost']}",
         
     | 
| 748 | 
         
            +
                                OUTPUT_DEBUG,
         
     | 
| 749 | 
         
            +
                            )
         
     | 
| 750 | 
         
            +
                            move = Move.from_gtp(move_info["move"], player=node.next_player)
         
     | 
| 751 | 
         
            +
                        elif candidates:  # just selfplay to end
         
     | 
| 752 | 
         
            +
                            move = Move.from_gtp(candidates[0]["move"], player=node.next_player)
         
     | 
| 753 | 
         
            +
                        else:  # 1 visit etc
         
     | 
| 754 | 
         
            +
                            polmoves = node.policy_ranking
         
     | 
| 755 | 
         
            +
                            move = polmoves[0][1] if polmoves else Move(None)
         
     | 
| 756 | 
         
            +
                        if move.is_pass:
         
     | 
| 757 | 
         
            +
                            if self.current_node == cn:
         
     | 
| 758 | 
         
            +
                                self.set_current_node(node)
         
     | 
| 759 | 
         
            +
                            return
         
     | 
| 760 | 
         
            +
                        new_node = GameNode(parent=node, move=move)
         
     | 
| 761 | 
         
            +
                        new_node.ai_thoughts = ai_thoughts
         
     | 
| 762 | 
         
            +
                        if until_move != "end" and target_b_advantage is not None:
         
     | 
| 763 | 
         
            +
                            self.set_current_node(new_node)
         
     | 
| 764 | 
         
            +
                            self.katrain.controls.set_status(
         
     | 
| 765 | 
         
            +
                                i18n._("setup game status message").format(move=new_node.depth, until_move=until_move),
         
     | 
| 766 | 
         
            +
                                STATUS_INFO,
         
     | 
| 767 | 
         
            +
                            )
         
     | 
| 768 | 
         
            +
                        else:
         
     | 
| 769 | 
         
            +
                            if node != cn:
         
     | 
| 770 | 
         
            +
                                node.remove_shortcut()
         
     | 
| 771 | 
         
            +
                            cn.add_shortcut(new_node)
         
     | 
| 772 | 
         
            +
             
     | 
| 773 | 
         
            +
                        self.katrain.controls.move_tree.redraw_tree_trigger()
         
     | 
| 774 | 
         
            +
                        request_analysis_for_node(new_node)
         
     | 
| 775 | 
         
            +
             
     | 
| 776 | 
         
            +
                    request_analysis_for_node(cn)
         
     | 
| 777 | 
         
            +
             
     | 
| 778 | 
         
            +
                def analyze_undo(self, node):
         
     | 
| 779 | 
         
            +
                    train_config = self.katrain.config("trainer")
         
     | 
| 780 | 
         
            +
                    move = node.move
         
     | 
| 781 | 
         
            +
                    if node != self.current_node or node.auto_undo is not None or not node.analysis_complete or not move:
         
     | 
| 782 | 
         
            +
                        return
         
     | 
| 783 | 
         
            +
                    points_lost = node.points_lost
         
     | 
| 784 | 
         
            +
                    thresholds = train_config["eval_thresholds"]
         
     | 
| 785 | 
         
            +
                    num_undo_prompts = train_config["num_undo_prompts"]
         
     | 
| 786 | 
         
            +
                    i = 0
         
     | 
| 787 | 
         
            +
                    while i < len(thresholds) and points_lost < thresholds[i]:
         
     | 
| 788 | 
         
            +
                        i += 1
         
     | 
| 789 | 
         
            +
                    num_undos = num_undo_prompts[i] if i < len(num_undo_prompts) else 0
         
     | 
| 790 | 
         
            +
                    if num_undos == 0:
         
     | 
| 791 | 
         
            +
                        undo = False
         
     | 
| 792 | 
         
            +
                    elif num_undos < 1:  # probability
         
     | 
| 793 | 
         
            +
                        undo = int(node.undo_threshold < num_undos) and len(node.parent.children) == 1
         
     | 
| 794 | 
         
            +
                    else:
         
     | 
| 795 | 
         
            +
                        undo = len(node.parent.children) <= num_undos
         
     | 
| 796 | 
         
            +
             
     | 
| 797 | 
         
            +
                    node.auto_undo = undo
         
     | 
| 798 | 
         
            +
                    if undo:
         
     | 
| 799 | 
         
            +
                        self.undo(1)
         
     | 
| 800 | 
         
            +
                        self.katrain.controls.set_status(
         
     | 
| 801 | 
         
            +
                            i18n._("teaching undo message").format(move=move.gtp(), points_lost=points_lost), STATUS_TEACHING
         
     | 
| 802 | 
         
            +
                        )
         
     | 
| 803 | 
         
            +
                        self.katrain.update_state()
         
     | 
| 804 | 
         
            +
                        
         
     | 
| 805 | 
         
            +
                def get_score(self):
         
     | 
| 806 | 
         
            +
                    if hasattr(self.engine, 'get_score'):  # 우리 엔진에 get_score가 있는지 확인
         
     | 
| 807 | 
         
            +
                        score_data = self.engine.get_score(self.current_node)
         
     | 
| 808 | 
         
            +
                        if score_data:
         
     | 
| 809 | 
         
            +
                            self._score = score_data
         
     | 
| 810 | 
         
            +
                            self.end_result = f'B+R' if score_data['winner'] == 'B' else 'W+R' # 임시 결과 문자열
         
     | 
| 811 | 
         
            +
                            if 'score' in score_data:
         
     | 
| 812 | 
         
            +
                                self.end_result = f"{score_data['winner']}+{score_data['score']}"
         
     | 
| 813 | 
         
            +
                            return self._score
         
     | 
| 814 | 
         
            +
                    
         
     | 
| 815 | 
         
            +
                    # 만약 우리 엔진에 기능이 없으면 원래 로직을 수행 (안전장치)
         
     | 
| 816 | 
         
            +
                    if self.engine:
         
     | 
| 817 | 
         
            +
                         return self.engine.get_score(self.current_node)
         
     | 
| 818 | 
         
            +
                    return self._score
         
     | 
    	
        katrain/katrain/core/game_node.py
    ADDED
    
    | 
         @@ -0,0 +1,466 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import base64
         
     | 
| 2 | 
         
            +
            import copy
         
     | 
| 3 | 
         
            +
            import gzip
         
     | 
| 4 | 
         
            +
            import json
         
     | 
| 5 | 
         
            +
            import random
         
     | 
| 6 | 
         
            +
            from typing import Dict, List, Optional, Tuple
         
     | 
| 7 | 
         
            +
             
     | 
| 8 | 
         
            +
            from katrain.core.constants import (
         
     | 
| 9 | 
         
            +
                ANALYSIS_FORMAT_VERSION,
         
     | 
| 10 | 
         
            +
                PROGRAM_NAME,
         
     | 
| 11 | 
         
            +
                REPORT_DT,
         
     | 
| 12 | 
         
            +
                SGF_INTERNAL_COMMENTS_MARKER,
         
     | 
| 13 | 
         
            +
                SGF_SEPARATOR_MARKER,
         
     | 
| 14 | 
         
            +
                VERSION,
         
     | 
| 15 | 
         
            +
                PRIORITY_DEFAULT,
         
     | 
| 16 | 
         
            +
                ADDITIONAL_MOVE_ORDER,
         
     | 
| 17 | 
         
            +
            )
         
     | 
| 18 | 
         
            +
            from katrain.core.lang import i18n
         
     | 
| 19 | 
         
            +
            from katrain.core.sgf_parser import Move, SGFNode
         
     | 
| 20 | 
         
            +
            from katrain.core.utils import evaluation_class, pack_floats, unpack_floats, var_to_grid
         
     | 
| 21 | 
         
            +
            from katrain.gui.theme import Theme
         
     | 
| 22 | 
         
            +
             
     | 
| 23 | 
         
            +
             
     | 
| 24 | 
         
            +
            def analysis_dumps(analysis):
         
     | 
| 25 | 
         
            +
                analysis = copy.deepcopy(analysis)
         
     | 
| 26 | 
         
            +
                for movedict in analysis["moves"].values():
         
     | 
| 27 | 
         
            +
                    if "ownership" in movedict:  # per-move ownership rarely used
         
     | 
| 28 | 
         
            +
                        del movedict["ownership"]
         
     | 
| 29 | 
         
            +
                ownership_data = pack_floats(analysis.pop("ownership"))
         
     | 
| 30 | 
         
            +
                policy_data = pack_floats(analysis.pop("policy"))
         
     | 
| 31 | 
         
            +
                main_data = json.dumps(analysis).encode("utf-8")
         
     | 
| 32 | 
         
            +
                return [
         
     | 
| 33 | 
         
            +
                    base64.standard_b64encode(gzip.compress(data)).decode("utf-8")
         
     | 
| 34 | 
         
            +
                    for data in [ownership_data, policy_data, main_data]
         
     | 
| 35 | 
         
            +
                ]
         
     | 
| 36 | 
         
            +
             
     | 
| 37 | 
         
            +
             
     | 
| 38 | 
         
            +
            class GameNode(SGFNode):
         
     | 
| 39 | 
         
            +
                """Represents a single game node, with one or more moves and placements."""
         
     | 
| 40 | 
         
            +
             
     | 
| 41 | 
         
            +
                def __init__(self, parent=None, properties=None, move=None):
         
     | 
| 42 | 
         
            +
                    super().__init__(parent=parent, properties=properties, move=move)
         
     | 
| 43 | 
         
            +
                    self.auto_undo = None  # None = not analyzed. False: not undone (good move). True: undone (bad move)
         
     | 
| 44 | 
         
            +
                    self.played_mistake_sound = None
         
     | 
| 45 | 
         
            +
                    self.ai_thoughts = ""
         
     | 
| 46 | 
         
            +
                    self.note = ""
         
     | 
| 47 | 
         
            +
                    self.move_number = 0
         
     | 
| 48 | 
         
            +
                    self.time_used = 0
         
     | 
| 49 | 
         
            +
                    self.undo_threshold = random.random()  # for fractional undos
         
     | 
| 50 | 
         
            +
                    self.end_state = None
         
     | 
| 51 | 
         
            +
                    self.shortcuts_to = []
         
     | 
| 52 | 
         
            +
                    self.shortcut_from = None
         
     | 
| 53 | 
         
            +
                    self.analysis_from_sgf = None
         
     | 
| 54 | 
         
            +
                    self.clear_analysis()
         
     | 
| 55 | 
         
            +
             
     | 
| 56 | 
         
            +
                def add_shortcut(self, to_node):  # collapses the branch between them
         
     | 
| 57 | 
         
            +
                    nodes = [to_node]
         
     | 
| 58 | 
         
            +
                    while nodes[-1].parent and nodes[-1] != self:  # ensure on path
         
     | 
| 59 | 
         
            +
                        nodes.append(nodes[-1].parent)
         
     | 
| 60 | 
         
            +
                    if nodes[-1] == self and len(nodes) > 2:
         
     | 
| 61 | 
         
            +
                        via = nodes[-2]
         
     | 
| 62 | 
         
            +
                        self.shortcuts_to.append((to_node, via))  # and first child
         
     | 
| 63 | 
         
            +
                        to_node.shortcut_from = self
         
     | 
| 64 | 
         
            +
             
     | 
| 65 | 
         
            +
                def remove_shortcut(self):
         
     | 
| 66 | 
         
            +
                    from_node = self.shortcut_from
         
     | 
| 67 | 
         
            +
                    if from_node:
         
     | 
| 68 | 
         
            +
                        from_node.shortcuts_to = [(m, v) for m, v in from_node.shortcuts_to if m != self]
         
     | 
| 69 | 
         
            +
                        self.shortcut_from = None
         
     | 
| 70 | 
         
            +
             
     | 
| 71 | 
         
            +
                def load_analysis(self):
         
     | 
| 72 | 
         
            +
                    if not self.analysis_from_sgf:
         
     | 
| 73 | 
         
            +
                        return False
         
     | 
| 74 | 
         
            +
                    try:
         
     | 
| 75 | 
         
            +
                        szx, szy = self.root.board_size
         
     | 
| 76 | 
         
            +
                        board_squares = szx * szy
         
     | 
| 77 | 
         
            +
                        version = self.root.get_property("KTV", ANALYSIS_FORMAT_VERSION)
         
     | 
| 78 | 
         
            +
                        if version > ANALYSIS_FORMAT_VERSION:
         
     | 
| 79 | 
         
            +
                            raise ValueError(f"Can not decode analysis data with version {version}, please update {PROGRAM_NAME}")
         
     | 
| 80 | 
         
            +
                        ownership_data, policy_data, main_data, *_ = [
         
     | 
| 81 | 
         
            +
                            gzip.decompress(base64.standard_b64decode(data)) for data in self.analysis_from_sgf
         
     | 
| 82 | 
         
            +
                        ]
         
     | 
| 83 | 
         
            +
                        self.analysis = {
         
     | 
| 84 | 
         
            +
                            **json.loads(main_data),
         
     | 
| 85 | 
         
            +
                            "policy": unpack_floats(policy_data, board_squares + 1),
         
     | 
| 86 | 
         
            +
                            "ownership": unpack_floats(ownership_data, board_squares),
         
     | 
| 87 | 
         
            +
                        }
         
     | 
| 88 | 
         
            +
                        return True
         
     | 
| 89 | 
         
            +
                    except Exception as e:
         
     | 
| 90 | 
         
            +
                        print(f"Error in loading analysis: {e}")
         
     | 
| 91 | 
         
            +
                        return False
         
     | 
| 92 | 
         
            +
             
     | 
| 93 | 
         
            +
                def add_list_property(self, property: str, values: List):
         
     | 
| 94 | 
         
            +
                    if property == "KT":
         
     | 
| 95 | 
         
            +
                        self.analysis_from_sgf = values
         
     | 
| 96 | 
         
            +
                    elif property == "C":
         
     | 
| 97 | 
         
            +
                        comments = [  # strip out all previously auto generated comments
         
     | 
| 98 | 
         
            +
                            c
         
     | 
| 99 | 
         
            +
                            for v in values
         
     | 
| 100 | 
         
            +
                            for c in v.split(SGF_SEPARATOR_MARKER)
         
     | 
| 101 | 
         
            +
                            if c.strip() and SGF_INTERNAL_COMMENTS_MARKER not in c
         
     | 
| 102 | 
         
            +
                        ]
         
     | 
| 103 | 
         
            +
                        self.note = "".join(comments).strip()  # no super call intended, just save as note to be editable
         
     | 
| 104 | 
         
            +
                    else:
         
     | 
| 105 | 
         
            +
                        return super().add_list_property(property, values)
         
     | 
| 106 | 
         
            +
             
     | 
| 107 | 
         
            +
                def clear_analysis(self):
         
     | 
| 108 | 
         
            +
                    self.analysis_visits_requested = 0
         
     | 
| 109 | 
         
            +
                    self.analysis = {"moves": {}, "root": None, "ownership": None, "policy": None, "completed": False}
         
     | 
| 110 | 
         
            +
             
     | 
| 111 | 
         
            +
                def sgf_properties(
         
     | 
| 112 | 
         
            +
                    self,
         
     | 
| 113 | 
         
            +
                    save_comments_player=None,
         
     | 
| 114 | 
         
            +
                    save_comments_class=None,
         
     | 
| 115 | 
         
            +
                    eval_thresholds=None,
         
     | 
| 116 | 
         
            +
                    save_analysis=False,
         
     | 
| 117 | 
         
            +
                    save_marks=False,
         
     | 
| 118 | 
         
            +
                ):
         
     | 
| 119 | 
         
            +
                    properties = copy.copy(super().sgf_properties())
         
     | 
| 120 | 
         
            +
                    note = self.note.strip()
         
     | 
| 121 | 
         
            +
                    if save_analysis and self.analysis_complete:
         
     | 
| 122 | 
         
            +
                        try:
         
     | 
| 123 | 
         
            +
                            properties["KT"] = analysis_dumps(self.analysis)
         
     | 
| 124 | 
         
            +
                        except Exception as e:
         
     | 
| 125 | 
         
            +
                            print(f"Error in saving analysis: {e}")
         
     | 
| 126 | 
         
            +
                    if self.points_lost and save_comments_class is not None and eval_thresholds is not None:
         
     | 
| 127 | 
         
            +
                        show_class = save_comments_class[evaluation_class(self.points_lost, eval_thresholds)]
         
     | 
| 128 | 
         
            +
                    else:
         
     | 
| 129 | 
         
            +
                        show_class = False
         
     | 
| 130 | 
         
            +
                    comments = properties.get("C", [])
         
     | 
| 131 | 
         
            +
                    if (
         
     | 
| 132 | 
         
            +
                        self.parent
         
     | 
| 133 | 
         
            +
                        and self.parent.analysis_exists
         
     | 
| 134 | 
         
            +
                        and self.analysis_exists
         
     | 
| 135 | 
         
            +
                        and (note or ((save_comments_player or {}).get(self.player, False) and show_class))
         
     | 
| 136 | 
         
            +
                    ):
         
     | 
| 137 | 
         
            +
                        if save_marks:
         
     | 
| 138 | 
         
            +
                            candidate_moves = self.parent.candidate_moves
         
     | 
| 139 | 
         
            +
                            top_x = Move.from_gtp(candidate_moves[0]["move"]).sgf(self.board_size)
         
     | 
| 140 | 
         
            +
                            best_sq = [
         
     | 
| 141 | 
         
            +
                                Move.from_gtp(d["move"]).sgf(self.board_size)
         
     | 
| 142 | 
         
            +
                                for d in candidate_moves
         
     | 
| 143 | 
         
            +
                                if d["pointsLost"] <= 0.5 and d["move"] != "pass" and d["order"] != 0
         
     | 
| 144 | 
         
            +
                            ]
         
     | 
| 145 | 
         
            +
                            if best_sq and "SQ" not in properties:
         
     | 
| 146 | 
         
            +
                                properties["SQ"] = best_sq
         
     | 
| 147 | 
         
            +
                            if top_x and "MA" not in properties:
         
     | 
| 148 | 
         
            +
                                properties["MA"] = [top_x]
         
     | 
| 149 | 
         
            +
                        comments.append("\n" + self.comment(sgf=True, interactive=False) + SGF_INTERNAL_COMMENTS_MARKER)
         
     | 
| 150 | 
         
            +
                    if self.is_root:
         
     | 
| 151 | 
         
            +
                        if save_marks:
         
     | 
| 152 | 
         
            +
                            comments = [i18n._("SGF start message") + SGF_INTERNAL_COMMENTS_MARKER + "\n"]
         
     | 
| 153 | 
         
            +
                        else:
         
     | 
| 154 | 
         
            +
                            comments = []
         
     | 
| 155 | 
         
            +
                        comments += [
         
     | 
| 156 | 
         
            +
                            *comments,
         
     | 
| 157 | 
         
            +
                            f"\nSGF generated by {PROGRAM_NAME} {VERSION}{SGF_INTERNAL_COMMENTS_MARKER}\n",
         
     | 
| 158 | 
         
            +
                        ]
         
     | 
| 159 | 
         
            +
                        properties["CA"] = ["UTF-8"]
         
     | 
| 160 | 
         
            +
                        properties["AP"] = [f"{PROGRAM_NAME}:{VERSION}"]
         
     | 
| 161 | 
         
            +
                        properties["KTV"] = [ANALYSIS_FORMAT_VERSION]
         
     | 
| 162 | 
         
            +
                    if self.shortcut_from:
         
     | 
| 163 | 
         
            +
                        properties["KTSF"] = [id(self.shortcut_from)]
         
     | 
| 164 | 
         
            +
                    elif "KTSF" in properties:
         
     | 
| 165 | 
         
            +
                        del properties["KTSF"]
         
     | 
| 166 | 
         
            +
                    if self.shortcuts_to:
         
     | 
| 167 | 
         
            +
                        properties["KTSID"] = [id(self)]
         
     | 
| 168 | 
         
            +
                    elif "KTSID" in properties:
         
     | 
| 169 | 
         
            +
                        del properties["KTSID"]
         
     | 
| 170 | 
         
            +
                    if note:
         
     | 
| 171 | 
         
            +
                        comments.insert(0, f"{self.note}\n")  # user notes at top!
         
     | 
| 172 | 
         
            +
                    if comments:
         
     | 
| 173 | 
         
            +
                        properties["C"] = [SGF_SEPARATOR_MARKER.join(comments).strip("\n")]
         
     | 
| 174 | 
         
            +
                    elif "C" in properties:
         
     | 
| 175 | 
         
            +
                        del properties["C"]
         
     | 
| 176 | 
         
            +
                    return properties
         
     | 
| 177 | 
         
            +
                
         
     | 
| 178 | 
         
            +
                @property
         
     | 
| 179 | 
         
            +
                def board_size(self):
         
     | 
| 180 | 
         
            +
                    # ★★★ 핵심 수정: SZ 속성이 없을 경우 기본값 19를 사용합니다. ★★★
         
     | 
| 181 | 
         
            +
                    sz = self.get_property("SZ", 19)
         
     | 
| 182 | 
         
            +
                    try:
         
     | 
| 183 | 
         
            +
                        if isinstance(sz, str) and ":" in sz:
         
     | 
| 184 | 
         
            +
                            x, y = sz.split(":")
         
     | 
| 185 | 
         
            +
                            return int(x), int(y)
         
     | 
| 186 | 
         
            +
                        return int(sz), int(sz)
         
     | 
| 187 | 
         
            +
                    except (ValueError, TypeError):
         
     | 
| 188 | 
         
            +
                        return 19, 19 # 혹시 모를 다른 에러에도 대비
         
     | 
| 189 | 
         
            +
                    
         
     | 
| 190 | 
         
            +
                @staticmethod
         
     | 
| 191 | 
         
            +
                def order_children(children):
         
     | 
| 192 | 
         
            +
                    return sorted(
         
     | 
| 193 | 
         
            +
                        children, key=lambda c: 0.5 if c.auto_undo is None else int(c.auto_undo)
         
     | 
| 194 | 
         
            +
                    )  # analyzed/not undone main, non-teach second, undone last
         
     | 
| 195 | 
         
            +
             
     | 
| 196 | 
         
            +
                # various analysis functions
         
     | 
| 197 | 
         
            +
                def analyze(
         
     | 
| 198 | 
         
            +
                    self,
         
     | 
| 199 | 
         
            +
                    engine,
         
     | 
| 200 | 
         
            +
                    priority=PRIORITY_DEFAULT,
         
     | 
| 201 | 
         
            +
                    visits=None,
         
     | 
| 202 | 
         
            +
                    ponder=False,
         
     | 
| 203 | 
         
            +
                    time_limit=True,
         
     | 
| 204 | 
         
            +
                    refine_move=None,
         
     | 
| 205 | 
         
            +
                    analyze_fast=False,
         
     | 
| 206 | 
         
            +
                    find_alternatives=False,
         
     | 
| 207 | 
         
            +
                    region_of_interest=None,
         
     | 
| 208 | 
         
            +
                    report_every=REPORT_DT,
         
     | 
| 209 | 
         
            +
                ):
         
     | 
| 210 | 
         
            +
                    engine.request_analysis(
         
     | 
| 211 | 
         
            +
                        self,
         
     | 
| 212 | 
         
            +
                        callback=lambda result, partial_result: self.set_analysis(
         
     | 
| 213 | 
         
            +
                            result, refine_move, find_alternatives, region_of_interest, partial_result
         
     | 
| 214 | 
         
            +
                        ),
         
     | 
| 215 | 
         
            +
                        priority=priority,
         
     | 
| 216 | 
         
            +
                        visits=visits,
         
     | 
| 217 | 
         
            +
                        ponder=ponder,
         
     | 
| 218 | 
         
            +
                        analyze_fast=analyze_fast,
         
     | 
| 219 | 
         
            +
                        time_limit=time_limit,
         
     | 
| 220 | 
         
            +
                        next_move=refine_move,
         
     | 
| 221 | 
         
            +
                        find_alternatives=find_alternatives,
         
     | 
| 222 | 
         
            +
                        region_of_interest=region_of_interest,
         
     | 
| 223 | 
         
            +
                        report_every=report_every,
         
     | 
| 224 | 
         
            +
                    )
         
     | 
| 225 | 
         
            +
             
     | 
| 226 | 
         
            +
                def update_move_analysis(self, move_analysis, move_gtp):
         
     | 
| 227 | 
         
            +
                    cur = self.analysis["moves"].get(move_gtp)
         
     | 
| 228 | 
         
            +
                    if cur is None:
         
     | 
| 229 | 
         
            +
                        self.analysis["moves"][move_gtp] = {
         
     | 
| 230 | 
         
            +
                            "move": move_gtp,
         
     | 
| 231 | 
         
            +
                            "order": ADDITIONAL_MOVE_ORDER,
         
     | 
| 232 | 
         
            +
                            **move_analysis,
         
     | 
| 233 | 
         
            +
                        }  # some default values for keys missing in rootInfo
         
     | 
| 234 | 
         
            +
                    else:
         
     | 
| 235 | 
         
            +
                        cur["order"] = min(
         
     | 
| 236 | 
         
            +
                            cur["order"], move_analysis.get("order", ADDITIONAL_MOVE_ORDER)
         
     | 
| 237 | 
         
            +
                        )  # parent arriving after child
         
     | 
| 238 | 
         
            +
                        if cur["visits"] < move_analysis["visits"]:
         
     | 
| 239 | 
         
            +
                            cur.update(move_analysis)
         
     | 
| 240 | 
         
            +
                        else:  # prior etc only
         
     | 
| 241 | 
         
            +
                            cur.update({k: v for k, v in move_analysis.items() if k not in cur})
         
     | 
| 242 | 
         
            +
             
     | 
| 243 | 
         
            +
                def set_analysis(
         
     | 
| 244 | 
         
            +
                    self,
         
     | 
| 245 | 
         
            +
                    analysis_json: Dict,
         
     | 
| 246 | 
         
            +
                    refine_move: Optional[Move] = None,
         
     | 
| 247 | 
         
            +
                    additional_moves: bool = False,
         
     | 
| 248 | 
         
            +
                    region_of_interest=None,
         
     | 
| 249 | 
         
            +
                    partial_result: bool = False,
         
     | 
| 250 | 
         
            +
                ):
         
     | 
| 251 | 
         
            +
                    if refine_move:
         
     | 
| 252 | 
         
            +
                        pvtail = analysis_json["moveInfos"][0]["pv"] if analysis_json["moveInfos"] else []
         
     | 
| 253 | 
         
            +
                        self.update_move_analysis(
         
     | 
| 254 | 
         
            +
                            {"pv": [refine_move.gtp()] + pvtail, **analysis_json["rootInfo"]}, refine_move.gtp()
         
     | 
| 255 | 
         
            +
                        )
         
     | 
| 256 | 
         
            +
                    else:
         
     | 
| 257 | 
         
            +
                        if additional_moves:  # additional moves: old order matters, ignore new order
         
     | 
| 258 | 
         
            +
                            for m in analysis_json["moveInfos"]:
         
     | 
| 259 | 
         
            +
                                del m["order"]
         
     | 
| 260 | 
         
            +
                        elif refine_move is None:  # normal update: old moves to end, new order matters. also for region?
         
     | 
| 261 | 
         
            +
                            for move_dict in self.analysis["moves"].values():
         
     | 
| 262 | 
         
            +
                                move_dict["order"] = ADDITIONAL_MOVE_ORDER  # old moves to end
         
     | 
| 263 | 
         
            +
                        for move_analysis in analysis_json["moveInfos"]:
         
     | 
| 264 | 
         
            +
                            self.update_move_analysis(move_analysis, move_analysis["move"])
         
     | 
| 265 | 
         
            +
                        self.analysis["ownership"] = analysis_json.get("ownership")
         
     | 
| 266 | 
         
            +
                        self.analysis["policy"] = analysis_json.get("policy")
         
     | 
| 267 | 
         
            +
                        if not additional_moves and not region_of_interest:
         
     | 
| 268 | 
         
            +
                            self.analysis["root"] = analysis_json["rootInfo"]
         
     | 
| 269 | 
         
            +
                            if self.parent and self.move:
         
     | 
| 270 | 
         
            +
                                analysis_json["rootInfo"]["pv"] = [self.move.gtp()] + (
         
     | 
| 271 | 
         
            +
                                    analysis_json["moveInfos"][0]["pv"] if analysis_json["moveInfos"] else []
         
     | 
| 272 | 
         
            +
                                )
         
     | 
| 273 | 
         
            +
                                self.parent.update_move_analysis(
         
     | 
| 274 | 
         
            +
                                    analysis_json["rootInfo"], self.move.gtp()
         
     | 
| 275 | 
         
            +
                                )  # update analysis in parent for consistency
         
     | 
| 276 | 
         
            +
                        is_normal_query = refine_move is None and not additional_moves
         
     | 
| 277 | 
         
            +
                        self.analysis["completed"] = self.analysis["completed"] or (is_normal_query and not partial_result)
         
     | 
| 278 | 
         
            +
             
     | 
| 279 | 
         
            +
                @property
         
     | 
| 280 | 
         
            +
                def ownership(self):
         
     | 
| 281 | 
         
            +
                    return self.analysis.get("ownership")
         
     | 
| 282 | 
         
            +
             
     | 
| 283 | 
         
            +
                @property
         
     | 
| 284 | 
         
            +
                def policy(self):
         
     | 
| 285 | 
         
            +
                    return self.analysis.get("policy")
         
     | 
| 286 | 
         
            +
             
     | 
| 287 | 
         
            +
                @property
         
     | 
| 288 | 
         
            +
                def analysis_exists(self):
         
     | 
| 289 | 
         
            +
                    return self.analysis.get("root") is not None
         
     | 
| 290 | 
         
            +
             
     | 
| 291 | 
         
            +
                @property
         
     | 
| 292 | 
         
            +
                def analysis_complete(self):
         
     | 
| 293 | 
         
            +
                    return self.analysis["completed"] and self.analysis["root"] is not None
         
     | 
| 294 | 
         
            +
             
     | 
| 295 | 
         
            +
                @property
         
     | 
| 296 | 
         
            +
                def root_visits(self):
         
     | 
| 297 | 
         
            +
                    return ((self.analysis or {}).get("root") or {}).get("visits", 0)
         
     | 
| 298 | 
         
            +
             
     | 
| 299 | 
         
            +
                @property
         
     | 
| 300 | 
         
            +
                def score(self) -> Optional[float]:
         
     | 
| 301 | 
         
            +
                    if self.analysis_exists:
         
     | 
| 302 | 
         
            +
                        return self.analysis["root"].get("scoreLead")
         
     | 
| 303 | 
         
            +
             
     | 
| 304 | 
         
            +
                def format_score(self, score=None):
         
     | 
| 305 | 
         
            +
                    score = score or self.score
         
     | 
| 306 | 
         
            +
                    if score is not None:
         
     | 
| 307 | 
         
            +
                        return f"{'B' if score >= 0 else 'W'}+{abs(score):.1f}"
         
     | 
| 308 | 
         
            +
             
     | 
| 309 | 
         
            +
                @property
         
     | 
| 310 | 
         
            +
                def winrate(self) -> Optional[float]:
         
     | 
| 311 | 
         
            +
                    if self.analysis_exists:
         
     | 
| 312 | 
         
            +
                        return self.analysis["root"].get("winrate")
         
     | 
| 313 | 
         
            +
             
     | 
| 314 | 
         
            +
                def format_winrate(self, win_rate=None):
         
     | 
| 315 | 
         
            +
                    win_rate = win_rate or self.winrate
         
     | 
| 316 | 
         
            +
                    if win_rate is not None:
         
     | 
| 317 | 
         
            +
                        return f"{'B' if win_rate > 0.5 else 'W'} {max(win_rate,1-win_rate):.1%}"
         
     | 
| 318 | 
         
            +
             
     | 
| 319 | 
         
            +
                def move_policy_stats(self) -> Tuple[Optional[int], float, List]:
         
     | 
| 320 | 
         
            +
                    single_move = self.move
         
     | 
| 321 | 
         
            +
                    if single_move and self.parent:
         
     | 
| 322 | 
         
            +
                        policy_ranking = self.parent.policy_ranking
         
     | 
| 323 | 
         
            +
                        if policy_ranking:
         
     | 
| 324 | 
         
            +
                            for ix, (p, m) in enumerate(policy_ranking):
         
     | 
| 325 | 
         
            +
                                if m == single_move:
         
     | 
| 326 | 
         
            +
                                    return ix + 1, p, policy_ranking
         
     | 
| 327 | 
         
            +
                    return None, 0.0, []
         
     | 
| 328 | 
         
            +
             
     | 
| 329 | 
         
            +
                def make_pv(self, player, pv, interactive):
         
     | 
| 330 | 
         
            +
                    pvtext = f"{player}{' '.join(pv)}"
         
     | 
| 331 | 
         
            +
                    if interactive:
         
     | 
| 332 | 
         
            +
                        pvtext = f"[u][ref={pvtext}][color={Theme.INFO_PV_COLOR}]{pvtext}[/color][/ref][/u]"
         
     | 
| 333 | 
         
            +
                    return pvtext
         
     | 
| 334 | 
         
            +
             
     | 
| 335 | 
         
            +
                def comment(self, sgf=False, teach=False, details=False, interactive=True):
         
     | 
| 336 | 
         
            +
                    single_move = self.move
         
     | 
| 337 | 
         
            +
                    if not self.parent or not single_move:  # root
         
     | 
| 338 | 
         
            +
                        if self.root:
         
     | 
| 339 | 
         
            +
                            rules = self.get_property("RU", "Japanese")
         
     | 
| 340 | 
         
            +
                            if isinstance(rules, str):  # else katago dict
         
     | 
| 341 | 
         
            +
                                rules = i18n._(rules.lower())
         
     | 
| 342 | 
         
            +
                            return f"{i18n._('komi')}: {self.komi:.1f}\n{i18n._('ruleset')}: {rules}\n"
         
     | 
| 343 | 
         
            +
                        return ""
         
     | 
| 344 | 
         
            +
             
     | 
| 345 | 
         
            +
                    text = i18n._("move").format(number=self.depth) + f": {single_move.player} {single_move.gtp()}\n"
         
     | 
| 346 | 
         
            +
                    if self.analysis_exists:
         
     | 
| 347 | 
         
            +
                        score = self.score
         
     | 
| 348 | 
         
            +
                        if sgf:
         
     | 
| 349 | 
         
            +
                            text += i18n._("Info:score").format(score=self.format_score(score)) + "\n"
         
     | 
| 350 | 
         
            +
                            text += i18n._("Info:winrate").format(winrate=self.format_winrate()) + "\n"
         
     | 
| 351 | 
         
            +
                        if self.parent and self.parent.analysis_exists:
         
     | 
| 352 | 
         
            +
                            previous_top_move = self.parent.candidate_moves[0]
         
     | 
| 353 | 
         
            +
                            if sgf or details:
         
     | 
| 354 | 
         
            +
                                if previous_top_move["move"] != single_move.gtp():
         
     | 
| 355 | 
         
            +
                                    points_lost = self.points_lost
         
     | 
| 356 | 
         
            +
                                    if sgf and points_lost > 0.5:
         
     | 
| 357 | 
         
            +
                                        text += i18n._("Info:point loss").format(points_lost=points_lost) + "\n"
         
     | 
| 358 | 
         
            +
                                    top_move = previous_top_move["move"]
         
     | 
| 359 | 
         
            +
                                    score = self.format_score(previous_top_move["scoreLead"])
         
     | 
| 360 | 
         
            +
                                    text += (
         
     | 
| 361 | 
         
            +
                                        i18n._("Info:top move").format(
         
     | 
| 362 | 
         
            +
                                            top_move=top_move,
         
     | 
| 363 | 
         
            +
                                            score=score,
         
     | 
| 364 | 
         
            +
                                        )
         
     | 
| 365 | 
         
            +
                                        + "\n"
         
     | 
| 366 | 
         
            +
                                    )
         
     | 
| 367 | 
         
            +
                                else:
         
     | 
| 368 | 
         
            +
                                    text += i18n._("Info:best move") + "\n"
         
     | 
| 369 | 
         
            +
                                if previous_top_move.get("pv") and (sgf or details):
         
     | 
| 370 | 
         
            +
                                    pv = self.make_pv(single_move.player, previous_top_move["pv"], interactive)
         
     | 
| 371 | 
         
            +
                                    text += i18n._("Info:PV").format(pv=pv) + "\n"
         
     | 
| 372 | 
         
            +
                            if sgf or details or teach:
         
     | 
| 373 | 
         
            +
                                currmove_pol_rank, currmove_pol_prob, policy_ranking = self.move_policy_stats()
         
     | 
| 374 | 
         
            +
                                if currmove_pol_rank is not None:
         
     | 
| 375 | 
         
            +
                                    policy_rank_msg = i18n._("Info:policy rank")
         
     | 
| 376 | 
         
            +
                                    text += policy_rank_msg.format(rank=currmove_pol_rank, probability=currmove_pol_prob) + "\n"
         
     | 
| 377 | 
         
            +
                                if currmove_pol_rank != 1 and policy_ranking and (sgf or details):
         
     | 
| 378 | 
         
            +
                                    policy_best_msg = i18n._("Info:policy best")
         
     | 
| 379 | 
         
            +
                                    pol_move, pol_prob = policy_ranking[0][1].gtp(), policy_ranking[0][0]
         
     | 
| 380 | 
         
            +
                                    text += policy_best_msg.format(move=pol_move, probability=pol_prob) + "\n"
         
     | 
| 381 | 
         
            +
                        if self.auto_undo and sgf:
         
     | 
| 382 | 
         
            +
                            text += i18n._("Info:teaching undo") + "\n"
         
     | 
| 383 | 
         
            +
                            top_pv = self.analysis_exists and self.candidate_moves[0].get("pv")
         
     | 
| 384 | 
         
            +
                            if top_pv:
         
     | 
| 385 | 
         
            +
                                text += i18n._("Info:undo predicted PV").format(pv=f"{self.next_player}{' '.join(top_pv)}") + "\n"
         
     | 
| 386 | 
         
            +
                    else:
         
     | 
| 387 | 
         
            +
                        text = i18n._("No analysis available") if sgf else i18n._("Analyzing move...")
         
     | 
| 388 | 
         
            +
             
     | 
| 389 | 
         
            +
                    if self.ai_thoughts and (sgf or details):
         
     | 
| 390 | 
         
            +
                        text += "\n" + i18n._("Info:AI thoughts").format(thoughts=self.ai_thoughts)
         
     | 
| 391 | 
         
            +
             
     | 
| 392 | 
         
            +
                    if "C" in self.properties:
         
     | 
| 393 | 
         
            +
                        text += "\n[u]SGF Comments:[/u]\n" + "\n".join(self.properties["C"])
         
     | 
| 394 | 
         
            +
             
     | 
| 395 | 
         
            +
                    return text
         
     | 
| 396 | 
         
            +
             
     | 
| 397 | 
         
            +
                @property
         
     | 
| 398 | 
         
            +
                def points_lost(self) -> Optional[float]:
         
     | 
| 399 | 
         
            +
                    single_move = self.move
         
     | 
| 400 | 
         
            +
                    if single_move and self.parent and self.analysis_exists and self.parent.analysis_exists:
         
     | 
| 401 | 
         
            +
                        parent_score = self.parent.score
         
     | 
| 402 | 
         
            +
                        score = self.score
         
     | 
| 403 | 
         
            +
                        return self.player_sign(single_move.player) * (parent_score - score)
         
     | 
| 404 | 
         
            +
             
     | 
| 405 | 
         
            +
                @property
         
     | 
| 406 | 
         
            +
                def parent_realized_points_lost(self) -> Optional[float]:
         
     | 
| 407 | 
         
            +
                    single_move = self.move
         
     | 
| 408 | 
         
            +
                    if (
         
     | 
| 409 | 
         
            +
                        single_move
         
     | 
| 410 | 
         
            +
                        and self.parent
         
     | 
| 411 | 
         
            +
                        and self.parent.parent
         
     | 
| 412 | 
         
            +
                        and self.analysis_exists
         
     | 
| 413 | 
         
            +
                        and self.parent.parent.analysis_exists
         
     | 
| 414 | 
         
            +
                    ):
         
     | 
| 415 | 
         
            +
                        parent_parent_score = self.parent.parent.score
         
     | 
| 416 | 
         
            +
                        score = self.score
         
     | 
| 417 | 
         
            +
                        return self.player_sign(single_move.player) * (score - parent_parent_score)
         
     | 
| 418 | 
         
            +
             
     | 
| 419 | 
         
            +
                @staticmethod
         
     | 
| 420 | 
         
            +
                def player_sign(player):
         
     | 
| 421 | 
         
            +
                    return {"B": 1, "W": -1, None: 0}[player]
         
     | 
| 422 | 
         
            +
             
     | 
| 423 | 
         
            +
                @property
         
     | 
| 424 | 
         
            +
                def candidate_moves(self) -> List[Dict]:
         
     | 
| 425 | 
         
            +
                    if not self.analysis_exists:
         
     | 
| 426 | 
         
            +
                        return []
         
     | 
| 427 | 
         
            +
                    if not self.analysis["moves"]:
         
     | 
| 428 | 
         
            +
                        polmoves = self.policy_ranking
         
     | 
| 429 | 
         
            +
                        top_polmove = polmoves[0][1] if polmoves else Move(None)  # if no info at all, pass
         
     | 
| 430 | 
         
            +
                        return [
         
     | 
| 431 | 
         
            +
                            {
         
     | 
| 432 | 
         
            +
                                **self.analysis["root"],
         
     | 
| 433 | 
         
            +
                                "pointsLost": 0,
         
     | 
| 434 | 
         
            +
                                "winrateLost": 0,
         
     | 
| 435 | 
         
            +
                                "order": 0,
         
     | 
| 436 | 
         
            +
                                "move": top_polmove.gtp(),
         
     | 
| 437 | 
         
            +
                                "pv": [top_polmove.gtp()],
         
     | 
| 438 | 
         
            +
                            }
         
     | 
| 439 | 
         
            +
                        ]  # single visit -> go by policy/root
         
     | 
| 440 | 
         
            +
             
     | 
| 441 | 
         
            +
                    root_score = self.analysis["root"]["scoreLead"]
         
     | 
| 442 | 
         
            +
                    root_winrate = self.analysis["root"]["winrate"]
         
     | 
| 443 | 
         
            +
                    move_dicts = list(self.analysis["moves"].values())  # prevent incoming analysis from causing crash
         
     | 
| 444 | 
         
            +
                    top_move = [d for d in move_dicts if d["order"] == 0]
         
     | 
| 445 | 
         
            +
                    top_score_lead = top_move[0]["scoreLead"] if top_move else root_score
         
     | 
| 446 | 
         
            +
                    return sorted(
         
     | 
| 447 | 
         
            +
                        [
         
     | 
| 448 | 
         
            +
                            {
         
     | 
| 449 | 
         
            +
                                "pointsLost": self.player_sign(self.next_player) * (root_score - d["scoreLead"]),
         
     | 
| 450 | 
         
            +
                                "relativePointsLost": self.player_sign(self.next_player) * (top_score_lead - d["scoreLead"]),
         
     | 
| 451 | 
         
            +
                                "winrateLost": self.player_sign(self.next_player) * (root_winrate - d["winrate"]),
         
     | 
| 452 | 
         
            +
                                **d,
         
     | 
| 453 | 
         
            +
                            }
         
     | 
| 454 | 
         
            +
                            for d in move_dicts
         
     | 
| 455 | 
         
            +
                        ],
         
     | 
| 456 | 
         
            +
                        key=lambda d: (d["order"], d["pointsLost"]),
         
     | 
| 457 | 
         
            +
                    )
         
     | 
| 458 | 
         
            +
             
     | 
| 459 | 
         
            +
                @property
         
     | 
| 460 | 
         
            +
                def policy_ranking(self) -> Optional[List[Tuple[float, Move]]]:  # return moves from highest policy value to lowest
         
     | 
| 461 | 
         
            +
                    if self.policy:
         
     | 
| 462 | 
         
            +
                        szx, szy = self.board_size
         
     | 
| 463 | 
         
            +
                        policy_grid = var_to_grid(self.policy, size=(szx, szy))
         
     | 
| 464 | 
         
            +
                        moves = [(policy_grid[y][x], Move((x, y), player=self.next_player)) for x in range(szx) for y in range(szy)]
         
     | 
| 465 | 
         
            +
                        moves.append((self.policy[-1], Move(None, player=self.next_player)))
         
     | 
| 466 | 
         
            +
                        return sorted(moves, key=lambda mp: -mp[0])
         
     | 
    	
        katrain/katrain/core/lang.py
    ADDED
    
    | 
         @@ -0,0 +1,89 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import gettext
         
     | 
| 2 | 
         
            +
            import os
         
     | 
| 3 | 
         
            +
            import sys
         
     | 
| 4 | 
         
            +
             
     | 
| 5 | 
         
            +
            from kivy._event import Observable
         
     | 
| 6 | 
         
            +
             
     | 
| 7 | 
         
            +
            from katrain.core.utils import find_package_resource
         
     | 
| 8 | 
         
            +
            from katrain.gui.theme import Theme
         
     | 
| 9 | 
         
            +
             
     | 
| 10 | 
         
            +
             
     | 
| 11 | 
         
            +
            class Lang(Observable):
         
     | 
| 12 | 
         
            +
                observers = []
         
     | 
| 13 | 
         
            +
                callbacks = []
         
     | 
| 14 | 
         
            +
                FONTS = {"jp": "NotoSansJP-Regular.otf", "tr": "NotoSans-Regular.ttf"}
         
     | 
| 15 | 
         
            +
             
     | 
| 16 | 
         
            +
                def __init__(self, lang):
         
     | 
| 17 | 
         
            +
                    super(Lang, self).__init__()
         
     | 
| 18 | 
         
            +
                    self.lang = None
         
     | 
| 19 | 
         
            +
                    self.switch_lang(lang)
         
     | 
| 20 | 
         
            +
             
     | 
| 21 | 
         
            +
                def _(self, text):
         
     | 
| 22 | 
         
            +
                    return self.ugettext(text)
         
     | 
| 23 | 
         
            +
             
     | 
| 24 | 
         
            +
                def set_widget_font(self, widget):
         
     | 
| 25 | 
         
            +
                    widget.font_name = self.font_name
         
     | 
| 26 | 
         
            +
                    for sub_widget in [getattr(widget, "_hint_lbl", None), getattr(widget, "_msg_lbl", None)]:  # MDText
         
     | 
| 27 | 
         
            +
                        if sub_widget:
         
     | 
| 28 | 
         
            +
                            sub_widget.font_name = self.font_name
         
     | 
| 29 | 
         
            +
             
     | 
| 30 | 
         
            +
                def fbind(self, name, func, *args):
         
     | 
| 31 | 
         
            +
                    if name == "_":
         
     | 
| 32 | 
         
            +
                        widget, property, *_ = args[0]
         
     | 
| 33 | 
         
            +
                        self.observers.append((widget, func, args))
         
     | 
| 34 | 
         
            +
                        try:
         
     | 
| 35 | 
         
            +
                            self.set_widget_font(widget)
         
     | 
| 36 | 
         
            +
                        except Exception as e:
         
     | 
| 37 | 
         
            +
                            print(e)
         
     | 
| 38 | 
         
            +
                            # pass
         
     | 
| 39 | 
         
            +
                    else:
         
     | 
| 40 | 
         
            +
                        return super(Lang, self).fbind(name, func, *args)
         
     | 
| 41 | 
         
            +
             
     | 
| 42 | 
         
            +
                def funbind(self, name, func, *args):
         
     | 
| 43 | 
         
            +
                    if name == "_":
         
     | 
| 44 | 
         
            +
                        widget, *_ = args[0]
         
     | 
| 45 | 
         
            +
                        key = (widget, func, args)
         
     | 
| 46 | 
         
            +
                        if key in self.observers:
         
     | 
| 47 | 
         
            +
                            self.observers.remove(key)
         
     | 
| 48 | 
         
            +
                    else:
         
     | 
| 49 | 
         
            +
                        return super(Lang, self).funbind(name, func, *args)
         
     | 
| 50 | 
         
            +
             
     | 
| 51 | 
         
            +
                def switch_lang(self, lang):
         
     | 
| 52 | 
         
            +
                    if lang == self.lang:
         
     | 
| 53 | 
         
            +
                        return
         
     | 
| 54 | 
         
            +
                    # get the right locales directory, and instantiate a gettext
         
     | 
| 55 | 
         
            +
                    self.lang = lang
         
     | 
| 56 | 
         
            +
                    self.font_name = self.FONTS.get(lang) or Theme.DEFAULT_FONT
         
     | 
| 57 | 
         
            +
                    i18n_dir, _ = os.path.split(find_package_resource("katrain/i18n/__init__.py"))
         
     | 
| 58 | 
         
            +
                    locale_dir = os.path.join(i18n_dir, "locales")
         
     | 
| 59 | 
         
            +
                    locales = gettext.translation("katrain", locale_dir, languages=[lang, DEFAULT_LANGUAGE])
         
     | 
| 60 | 
         
            +
                    self.ugettext = locales.gettext
         
     | 
| 61 | 
         
            +
             
     | 
| 62 | 
         
            +
                    # update all the kv rules attached to this text
         
     | 
| 63 | 
         
            +
                    for widget, func, args in self.observers:
         
     | 
| 64 | 
         
            +
                        try:
         
     | 
| 65 | 
         
            +
                            func(args[0], None, None)
         
     | 
| 66 | 
         
            +
                            self.set_widget_font(widget)
         
     | 
| 67 | 
         
            +
                        except ReferenceError:
         
     | 
| 68 | 
         
            +
                            pass  # proxy no longer exists
         
     | 
| 69 | 
         
            +
                        except Exception as e:
         
     | 
| 70 | 
         
            +
                            print("Error in switching languages", e)
         
     | 
| 71 | 
         
            +
                    for cb in self.callbacks:
         
     | 
| 72 | 
         
            +
                        try:
         
     | 
| 73 | 
         
            +
                            cb(self)
         
     | 
| 74 | 
         
            +
                        except Exception as e:
         
     | 
| 75 | 
         
            +
                            print(f"Failed callback on language change: {e}", file=sys.stderr)
         
     | 
| 76 | 
         
            +
             
     | 
| 77 | 
         
            +
             
     | 
| 78 | 
         
            +
            DEFAULT_LANGUAGE = "en"
         
     | 
| 79 | 
         
            +
            i18n = Lang(DEFAULT_LANGUAGE)
         
     | 
| 80 | 
         
            +
             
     | 
| 81 | 
         
            +
             
     | 
| 82 | 
         
            +
            def rank_label(rank):
         
     | 
| 83 | 
         
            +
                if rank is None:
         
     | 
| 84 | 
         
            +
                    return "??k"
         
     | 
| 85 | 
         
            +
             
     | 
| 86 | 
         
            +
                if rank >= 0.5:
         
     | 
| 87 | 
         
            +
                    return f"{rank:.0f}{i18n._('strength:dan')}"
         
     | 
| 88 | 
         
            +
                else:
         
     | 
| 89 | 
         
            +
                    return f"{1-rank:.0f}{i18n._('strength:kyu')}"
         
     | 
    	
        katrain/katrain/core/sgf_parser.py
    ADDED
    
    | 
         @@ -0,0 +1,714 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import copy
         
     | 
| 2 | 
         
            +
            import chardet
         
     | 
| 3 | 
         
            +
            import math
         
     | 
| 4 | 
         
            +
            import re
         
     | 
| 5 | 
         
            +
            from collections import defaultdict
         
     | 
| 6 | 
         
            +
            from typing import Any, Dict, List, Optional, Tuple
         
     | 
| 7 | 
         
            +
             
     | 
| 8 | 
         
            +
             
     | 
| 9 | 
         
            +
            class ParseError(Exception):
         
     | 
| 10 | 
         
            +
                """Exception raised on a parse error"""
         
     | 
| 11 | 
         
            +
             
     | 
| 12 | 
         
            +
                pass
         
     | 
| 13 | 
         
            +
             
     | 
| 14 | 
         
            +
             
     | 
| 15 | 
         
            +
            class Move:
         
     | 
| 16 | 
         
            +
                GTP_COORD = list("ABCDEFGHJKLMNOPQRSTUVWXYZ") + [
         
     | 
| 17 | 
         
            +
                    xa + c for xa in "ABCDEFGH" for c in "ABCDEFGHJKLMNOPQRSTUVWXYZ"
         
     | 
| 18 | 
         
            +
                ]  # board size 52+ support
         
     | 
| 19 | 
         
            +
                PLAYERS = "BW"
         
     | 
| 20 | 
         
            +
                SGF_COORD = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ".lower()) + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")  # sgf goes to 52
         
     | 
| 21 | 
         
            +
             
     | 
| 22 | 
         
            +
                @classmethod
         
     | 
| 23 | 
         
            +
                def from_gtp(cls, gtp_coords, player="B"):
         
     | 
| 24 | 
         
            +
                    """Initialize a move from GTP coordinates and player"""
         
     | 
| 25 | 
         
            +
                    if "pass" in gtp_coords.lower():
         
     | 
| 26 | 
         
            +
                        return cls(coords=None, player=player)
         
     | 
| 27 | 
         
            +
                    match = re.match(r"([A-Z]+)(\d+)", gtp_coords)
         
     | 
| 28 | 
         
            +
                    return cls(coords=(Move.GTP_COORD.index(match[1]), int(match[2]) - 1), player=player)
         
     | 
| 29 | 
         
            +
             
     | 
| 30 | 
         
            +
                @classmethod
         
     | 
| 31 | 
         
            +
                def from_sgf(cls, sgf_coords, board_size, player="B"):
         
     | 
| 32 | 
         
            +
                    """Initialize a move from SGF coordinates and player"""
         
     | 
| 33 | 
         
            +
                    if sgf_coords == "" or (
         
     | 
| 34 | 
         
            +
                        sgf_coords == "tt" and board_size[0] <= 19 and board_size[1] <= 19
         
     | 
| 35 | 
         
            +
                    ):  # [tt] can be used as "pass" for <= 19x19 board
         
     | 
| 36 | 
         
            +
                        return cls(coords=None, player=player)
         
     | 
| 37 | 
         
            +
                    return cls(
         
     | 
| 38 | 
         
            +
                        coords=(Move.SGF_COORD.index(sgf_coords[0]), board_size[1] - Move.SGF_COORD.index(sgf_coords[1]) - 1),
         
     | 
| 39 | 
         
            +
                        player=player,
         
     | 
| 40 | 
         
            +
                    )
         
     | 
| 41 | 
         
            +
             
     | 
| 42 | 
         
            +
                def __init__(self, coords: Optional[Tuple[int, int]] = None, player: str = "B"):
         
     | 
| 43 | 
         
            +
                    """Initialize a move from zero-based coordinates and player"""
         
     | 
| 44 | 
         
            +
                    self.player = player
         
     | 
| 45 | 
         
            +
                    self.coords = coords
         
     | 
| 46 | 
         
            +
             
     | 
| 47 | 
         
            +
                def __repr__(self):
         
     | 
| 48 | 
         
            +
                    return f"Move({self.player or ''}{self.gtp()})"
         
     | 
| 49 | 
         
            +
             
     | 
| 50 | 
         
            +
                def __eq__(self, other):
         
     | 
| 51 | 
         
            +
                    return self.coords == other.coords and self.player == other.player
         
     | 
| 52 | 
         
            +
             
     | 
| 53 | 
         
            +
                def __hash__(self):
         
     | 
| 54 | 
         
            +
                    return hash((self.coords, self.player))
         
     | 
| 55 | 
         
            +
             
     | 
| 56 | 
         
            +
                def gtp(self):
         
     | 
| 57 | 
         
            +
                    """Returns GTP coordinates of the move"""
         
     | 
| 58 | 
         
            +
                    if self.is_pass:
         
     | 
| 59 | 
         
            +
                        return "pass"
         
     | 
| 60 | 
         
            +
                    return Move.GTP_COORD[self.coords[0]] + str(self.coords[1] + 1)
         
     | 
| 61 | 
         
            +
             
     | 
| 62 | 
         
            +
                def sgf(self, board_size):
         
     | 
| 63 | 
         
            +
                    """Returns SGF coordinates of the move"""
         
     | 
| 64 | 
         
            +
                    if self.is_pass:
         
     | 
| 65 | 
         
            +
                        return ""
         
     | 
| 66 | 
         
            +
                    return f"{Move.SGF_COORD[self.coords[0]]}{Move.SGF_COORD[board_size[1] - self.coords[1] - 1]}"
         
     | 
| 67 | 
         
            +
             
     | 
| 68 | 
         
            +
                @property
         
     | 
| 69 | 
         
            +
                def is_pass(self):
         
     | 
| 70 | 
         
            +
                    """Returns True if the move is a pass"""
         
     | 
| 71 | 
         
            +
                    return self.coords is None
         
     | 
| 72 | 
         
            +
             
     | 
| 73 | 
         
            +
                @staticmethod
         
     | 
| 74 | 
         
            +
                def opponent_player(player):
         
     | 
| 75 | 
         
            +
                    """Returns the opposing player, i.e. W <-> B"""
         
     | 
| 76 | 
         
            +
                    return "W" if player == "B" else "B"
         
     | 
| 77 | 
         
            +
             
     | 
| 78 | 
         
            +
                @property
         
     | 
| 79 | 
         
            +
                def opponent(self):
         
     | 
| 80 | 
         
            +
                    """Returns the opposing player, i.e. W <-> B"""
         
     | 
| 81 | 
         
            +
                    return self.opponent_player(self.player)
         
     | 
| 82 | 
         
            +
             
     | 
| 83 | 
         
            +
             
     | 
| 84 | 
         
            +
            class SGFNode:
         
     | 
| 85 | 
         
            +
                def __init__(self, parent=None, properties=None, move=None):
         
     | 
| 86 | 
         
            +
                    self.children = []
         
     | 
| 87 | 
         
            +
                    self.properties = defaultdict(list)
         
     | 
| 88 | 
         
            +
                    if properties:
         
     | 
| 89 | 
         
            +
                        for k, v in properties.items():
         
     | 
| 90 | 
         
            +
                            self.set_property(k, v)
         
     | 
| 91 | 
         
            +
                    self.parent = parent
         
     | 
| 92 | 
         
            +
                    if self.parent:
         
     | 
| 93 | 
         
            +
                        self.parent.children.append(self)
         
     | 
| 94 | 
         
            +
                    if parent and move:
         
     | 
| 95 | 
         
            +
                        self.set_property(move.player, move.sgf(self.board_size))
         
     | 
| 96 | 
         
            +
                    self._clear_cache()
         
     | 
| 97 | 
         
            +
             
     | 
| 98 | 
         
            +
                def _clear_cache(self):
         
     | 
| 99 | 
         
            +
                    self.moves_cache = None
         
     | 
| 100 | 
         
            +
             
     | 
| 101 | 
         
            +
                def __repr__(self):
         
     | 
| 102 | 
         
            +
                    return f"SGFNode({dict(self.properties)})"
         
     | 
| 103 | 
         
            +
             
     | 
| 104 | 
         
            +
                def sgf_properties(self, **xargs) -> Dict:
         
     | 
| 105 | 
         
            +
                    """For hooking into in a subclass and overriding/formatting any additional properties to be output."""
         
     | 
| 106 | 
         
            +
                    return copy.deepcopy(self.properties)
         
     | 
| 107 | 
         
            +
             
     | 
| 108 | 
         
            +
                @staticmethod
         
     | 
| 109 | 
         
            +
                def order_children(children):
         
     | 
| 110 | 
         
            +
                    """For hooking into in a subclass and overriding branch order."""
         
     | 
| 111 | 
         
            +
                    return children
         
     | 
| 112 | 
         
            +
             
     | 
| 113 | 
         
            +
                @property
         
     | 
| 114 | 
         
            +
                def ordered_children(self):
         
     | 
| 115 | 
         
            +
                    return self.order_children(self.children)
         
     | 
| 116 | 
         
            +
             
     | 
| 117 | 
         
            +
                @staticmethod
         
     | 
| 118 | 
         
            +
                def _escape_value(value):
         
     | 
| 119 | 
         
            +
                    return re.sub(r"([\]\\])", r"\\\1", value) if isinstance(value, str) else value  # escape \ and ]
         
     | 
| 120 | 
         
            +
             
     | 
| 121 | 
         
            +
                @staticmethod
         
     | 
| 122 | 
         
            +
                def _unescape_value(value):
         
     | 
| 123 | 
         
            +
                    return re.sub(r"\\([\]\\])", r"\1", value) if isinstance(value, str) else value  # unescape \ and ]
         
     | 
| 124 | 
         
            +
             
     | 
| 125 | 
         
            +
                def sgf(self, **xargs) -> str:
         
     | 
| 126 | 
         
            +
                    """Generates an SGF, calling sgf_properties on each node with the given xargs, so it can filter relevant properties if needed."""
         
     | 
| 127 | 
         
            +
             
     | 
| 128 | 
         
            +
                    def node_sgf_str(node):
         
     | 
| 129 | 
         
            +
                        return ";" + "".join(
         
     | 
| 130 | 
         
            +
                            [
         
     | 
| 131 | 
         
            +
                                prop + "".join(f"[{self._escape_value(v)}]" for v in values)
         
     | 
| 132 | 
         
            +
                                for prop, values in node.sgf_properties(**xargs).items()
         
     | 
| 133 | 
         
            +
                                if values
         
     | 
| 134 | 
         
            +
                            ]
         
     | 
| 135 | 
         
            +
                        )
         
     | 
| 136 | 
         
            +
             
     | 
| 137 | 
         
            +
                    stack = [")", self, "("]
         
     | 
| 138 | 
         
            +
                    sgf_str = ""
         
     | 
| 139 | 
         
            +
                    while stack:
         
     | 
| 140 | 
         
            +
                        item = stack.pop()
         
     | 
| 141 | 
         
            +
                        if isinstance(item, str):
         
     | 
| 142 | 
         
            +
                            sgf_str += item
         
     | 
| 143 | 
         
            +
                        else:
         
     | 
| 144 | 
         
            +
                            sgf_str += node_sgf_str(item)
         
     | 
| 145 | 
         
            +
                            if len(item.children) == 1:
         
     | 
| 146 | 
         
            +
                                stack.append(item.children[0])
         
     | 
| 147 | 
         
            +
                            elif item.children:
         
     | 
| 148 | 
         
            +
                                stack += sum([[")", c, "("] for c in item.ordered_children[::-1]], [])
         
     | 
| 149 | 
         
            +
                    return sgf_str
         
     | 
| 150 | 
         
            +
             
     | 
| 151 | 
         
            +
                def add_list_property(self, property: str, values: List):
         
     | 
| 152 | 
         
            +
                    """Add some values to the property list."""
         
     | 
| 153 | 
         
            +
                    # SiZe[19] ==> SZ[19] etc. for old SGF
         
     | 
| 154 | 
         
            +
                    normalized_property = re.sub("[a-z]", "", property)
         
     | 
| 155 | 
         
            +
                    self._clear_cache()
         
     | 
| 156 | 
         
            +
                    self.properties[normalized_property] += values
         
     | 
| 157 | 
         
            +
             
     | 
| 158 | 
         
            +
                def get_list_property(self, property, default=None) -> Any:
         
     | 
| 159 | 
         
            +
                    """Get the list of values for a property."""
         
     | 
| 160 | 
         
            +
                    return self.properties.get(property, default)
         
     | 
| 161 | 
         
            +
             
     | 
| 162 | 
         
            +
                def set_property(self, property: str, value: Any):
         
     | 
| 163 | 
         
            +
                    """Add some values to the property. If not a list, it will be made into a single-value list."""
         
     | 
| 164 | 
         
            +
                    if not isinstance(value, list):
         
     | 
| 165 | 
         
            +
                        value = [value]
         
     | 
| 166 | 
         
            +
                    self._clear_cache()
         
     | 
| 167 | 
         
            +
                    self.properties[property] = value
         
     | 
| 168 | 
         
            +
             
     | 
| 169 | 
         
            +
                def get_property(self, property, default=None) -> Any:
         
     | 
| 170 | 
         
            +
                    """Get the first value of the property, typically when exactly one is expected."""
         
     | 
| 171 | 
         
            +
                    return self.properties.get(property, [default])[0]
         
     | 
| 172 | 
         
            +
             
     | 
| 173 | 
         
            +
                def clear_property(self, property) -> Any:
         
     | 
| 174 | 
         
            +
                    """Removes property if it exists."""
         
     | 
| 175 | 
         
            +
                    return self.properties.pop(property, None)
         
     | 
| 176 | 
         
            +
             
     | 
| 177 | 
         
            +
                @property
         
     | 
| 178 | 
         
            +
                def parent(self) -> Optional["SGFNode"]:
         
     | 
| 179 | 
         
            +
                    """Returns the parent node"""
         
     | 
| 180 | 
         
            +
                    return self._parent
         
     | 
| 181 | 
         
            +
             
     | 
| 182 | 
         
            +
                @parent.setter
         
     | 
| 183 | 
         
            +
                def parent(self, parent_node):
         
     | 
| 184 | 
         
            +
                    self._parent = parent_node
         
     | 
| 185 | 
         
            +
                    self._root = None
         
     | 
| 186 | 
         
            +
                    self._depth = None
         
     | 
| 187 | 
         
            +
             
     | 
| 188 | 
         
            +
                @property
         
     | 
| 189 | 
         
            +
                def root(self) -> "SGFNode":
         
     | 
| 190 | 
         
            +
                    """Returns the root of the tree, cached for speed"""
         
     | 
| 191 | 
         
            +
                    if self._root is None:
         
     | 
| 192 | 
         
            +
                        self._root = self.parent.root if self.parent else self
         
     | 
| 193 | 
         
            +
                    return self._root
         
     | 
| 194 | 
         
            +
             
     | 
| 195 | 
         
            +
                @property
         
     | 
| 196 | 
         
            +
                def depth(self) -> int:
         
     | 
| 197 | 
         
            +
                    """Returns the depth of this node, where root is 0, cached for speed"""
         
     | 
| 198 | 
         
            +
                    if self._depth is None:
         
     | 
| 199 | 
         
            +
                        moves = self.moves
         
     | 
| 200 | 
         
            +
                        if self.is_root:
         
     | 
| 201 | 
         
            +
                            self._depth = 0
         
     | 
| 202 | 
         
            +
                        else:  # no increase on placements etc
         
     | 
| 203 | 
         
            +
                            self._depth = self.parent.depth + len(moves)
         
     | 
| 204 | 
         
            +
                    return self._depth
         
     | 
| 205 | 
         
            +
             
     | 
| 206 | 
         
            +
                @property
         
     | 
| 207 | 
         
            +
                def board_size(self) -> Tuple[int, int]:
         
     | 
| 208 | 
         
            +
                    """Retrieves the root's SZ property, or 19 if missing. Parses it, and returns board size as a tuple x,y"""
         
     | 
| 209 | 
         
            +
                    size = str(self.root.get_property("SZ", "19"))
         
     | 
| 210 | 
         
            +
                    if ":" in size:
         
     | 
| 211 | 
         
            +
                        x, y = map(int, size.split(":"))
         
     | 
| 212 | 
         
            +
                    else:
         
     | 
| 213 | 
         
            +
                        x = int(size)
         
     | 
| 214 | 
         
            +
                        y = x
         
     | 
| 215 | 
         
            +
                    return x, y
         
     | 
| 216 | 
         
            +
             
     | 
| 217 | 
         
            +
                @property
         
     | 
| 218 | 
         
            +
                def komi(self) -> float:
         
     | 
| 219 | 
         
            +
                    """Retrieves the root's KM property, or 6.5 if missing"""
         
     | 
| 220 | 
         
            +
                    try:
         
     | 
| 221 | 
         
            +
                        km_value = self.root.get_property("KM")
         
     | 
| 222 | 
         
            +
                        km = float(km_value or 6.5)
         
     | 
| 223 | 
         
            +
                    except ValueError:
         
     | 
| 224 | 
         
            +
                        km = 6.5
         
     | 
| 225 | 
         
            +
             
     | 
| 226 | 
         
            +
                    return km
         
     | 
| 227 | 
         
            +
             
     | 
| 228 | 
         
            +
                @property
         
     | 
| 229 | 
         
            +
                def handicap(self) -> int:
         
     | 
| 230 | 
         
            +
                    try:
         
     | 
| 231 | 
         
            +
                        return int(self.root.get_property("HA", 0))
         
     | 
| 232 | 
         
            +
                    except ValueError:
         
     | 
| 233 | 
         
            +
                        return 0
         
     | 
| 234 | 
         
            +
             
     | 
| 235 | 
         
            +
                @property
         
     | 
| 236 | 
         
            +
                def ruleset(self) -> str:
         
     | 
| 237 | 
         
            +
                    """Retrieves the root's RU property, or 'japanese' if missing"""
         
     | 
| 238 | 
         
            +
                    return self.root.get_property("RU", "japanese")
         
     | 
| 239 | 
         
            +
             
     | 
| 240 | 
         
            +
                @property
         
     | 
| 241 | 
         
            +
                def moves(self) -> List[Move]:
         
     | 
| 242 | 
         
            +
                    """Returns all moves in the node - typically 'move' will be better."""
         
     | 
| 243 | 
         
            +
                    if self.moves_cache is None:
         
     | 
| 244 | 
         
            +
                        self.moves_cache = [
         
     | 
| 245 | 
         
            +
                            Move.from_sgf(move, player=pl, board_size=self.board_size)
         
     | 
| 246 | 
         
            +
                            for pl in Move.PLAYERS
         
     | 
| 247 | 
         
            +
                            for move in self.get_list_property(pl, [])
         
     | 
| 248 | 
         
            +
                        ]
         
     | 
| 249 | 
         
            +
                    return self.moves_cache
         
     | 
| 250 | 
         
            +
             
     | 
| 251 | 
         
            +
                def _expanded_placements(self, player):
         
     | 
| 252 | 
         
            +
                    sgf_pl = player if player is not None else "E"  # AE
         
     | 
| 253 | 
         
            +
                    placements = self.get_list_property("A" + sgf_pl, [])
         
     | 
| 254 | 
         
            +
                    if not placements:
         
     | 
| 255 | 
         
            +
                        return []
         
     | 
| 256 | 
         
            +
                    to_be_expanded = [p for p in placements if ":" in p]
         
     | 
| 257 | 
         
            +
                    board_size = self.board_size
         
     | 
| 258 | 
         
            +
                    if to_be_expanded:
         
     | 
| 259 | 
         
            +
                        coords = {
         
     | 
| 260 | 
         
            +
                            Move.from_sgf(sgf_coord, player=player, board_size=board_size)
         
     | 
| 261 | 
         
            +
                            for sgf_coord in placements
         
     | 
| 262 | 
         
            +
                            if ":" not in sgf_coord
         
     | 
| 263 | 
         
            +
                        }
         
     | 
| 264 | 
         
            +
                        for p in to_be_expanded:
         
     | 
| 265 | 
         
            +
                            from_coord, to_coord = [Move.from_sgf(c, board_size=board_size) for c in p.split(":")[:2]]
         
     | 
| 266 | 
         
            +
                            for x in range(from_coord.coords[0], to_coord.coords[0] + 1):
         
     | 
| 267 | 
         
            +
                                for y in range(to_coord.coords[1], from_coord.coords[1] + 1):  # sgf upside dn
         
     | 
| 268 | 
         
            +
                                    if 0 <= x < board_size[0] and 0 <= y < board_size[1]:
         
     | 
| 269 | 
         
            +
                                        coords.add(Move((x, y), player=player))
         
     | 
| 270 | 
         
            +
                        return list(coords)
         
     | 
| 271 | 
         
            +
                    else:
         
     | 
| 272 | 
         
            +
                        return [Move.from_sgf(sgf_coord, player=player, board_size=board_size) for sgf_coord in placements]
         
     | 
| 273 | 
         
            +
             
     | 
| 274 | 
         
            +
                @property
         
     | 
| 275 | 
         
            +
                def placements(self) -> List[Move]:
         
     | 
| 276 | 
         
            +
                    """Returns all placements (AB/AW) in the node."""
         
     | 
| 277 | 
         
            +
                    return [coord for pl in Move.PLAYERS for coord in self._expanded_placements(pl)]
         
     | 
| 278 | 
         
            +
             
     | 
| 279 | 
         
            +
                @property
         
     | 
| 280 | 
         
            +
                def clear_placements(self) -> List[Move]:
         
     | 
| 281 | 
         
            +
                    """Returns all AE clear square commends in the node."""
         
     | 
| 282 | 
         
            +
                    return self._expanded_placements(None)
         
     | 
| 283 | 
         
            +
             
     | 
| 284 | 
         
            +
                @property
         
     | 
| 285 | 
         
            +
                def move_with_placements(self) -> List[Move]:
         
     | 
| 286 | 
         
            +
                    """Returns all moves (B/W) and placements (AB/AW) in the node."""
         
     | 
| 287 | 
         
            +
                    return self.placements + self.moves
         
     | 
| 288 | 
         
            +
             
     | 
| 289 | 
         
            +
                @property
         
     | 
| 290 | 
         
            +
                def move(self) -> Optional[Move]:
         
     | 
| 291 | 
         
            +
                    """Returns the single move for the node if one exists, or None if no moves (or multiple ones) exist."""
         
     | 
| 292 | 
         
            +
                    moves = self.moves
         
     | 
| 293 | 
         
            +
                    if len(moves) == 1:
         
     | 
| 294 | 
         
            +
                        return moves[0]
         
     | 
| 295 | 
         
            +
             
     | 
| 296 | 
         
            +
                @property
         
     | 
| 297 | 
         
            +
                def is_root(self) -> bool:
         
     | 
| 298 | 
         
            +
                    """Returns true if node is a root"""
         
     | 
| 299 | 
         
            +
                    return self.parent is None
         
     | 
| 300 | 
         
            +
             
     | 
| 301 | 
         
            +
                @property
         
     | 
| 302 | 
         
            +
                def is_pass(self) -> bool:
         
     | 
| 303 | 
         
            +
                    """Returns true if associated move is pass"""
         
     | 
| 304 | 
         
            +
                    return not self.placements and self.move and self.move.is_pass
         
     | 
| 305 | 
         
            +
             
     | 
| 306 | 
         
            +
                @property
         
     | 
| 307 | 
         
            +
                def empty(self) -> bool:
         
     | 
| 308 | 
         
            +
                    """Returns true if node has no children or properties"""
         
     | 
| 309 | 
         
            +
                    return not self.children and not self.properties
         
     | 
| 310 | 
         
            +
             
     | 
| 311 | 
         
            +
                @property
         
     | 
| 312 | 
         
            +
                def nodes_in_tree(self) -> List:
         
     | 
| 313 | 
         
            +
                    """Returns all nodes in the tree rooted at this node"""
         
     | 
| 314 | 
         
            +
                    stack = [self]
         
     | 
| 315 | 
         
            +
                    nodes = []
         
     | 
| 316 | 
         
            +
                    while stack:
         
     | 
| 317 | 
         
            +
                        item = stack.pop(0)
         
     | 
| 318 | 
         
            +
                        nodes.append(item)
         
     | 
| 319 | 
         
            +
                        stack += item.children
         
     | 
| 320 | 
         
            +
                    return nodes
         
     | 
| 321 | 
         
            +
             
     | 
| 322 | 
         
            +
                @property
         
     | 
| 323 | 
         
            +
                def nodes_from_root(self) -> List:
         
     | 
| 324 | 
         
            +
                    """Returns all nodes from the root up to this node, i.e. the moves played in the current branch of the game"""
         
     | 
| 325 | 
         
            +
                    nodes = [self]
         
     | 
| 326 | 
         
            +
                    n = self
         
     | 
| 327 | 
         
            +
                    while not n.is_root:
         
     | 
| 328 | 
         
            +
                        n = n.parent
         
     | 
| 329 | 
         
            +
                        nodes.append(n)
         
     | 
| 330 | 
         
            +
                    return nodes[::-1]
         
     | 
| 331 | 
         
            +
             
     | 
| 332 | 
         
            +
                def play(self, move) -> "SGFNode":
         
     | 
| 333 | 
         
            +
                    """Either find an existing child or create a new one with the given move."""
         
     | 
| 334 | 
         
            +
                    for c in self.children:
         
     | 
| 335 | 
         
            +
                        if c.move and c.move == move:
         
     | 
| 336 | 
         
            +
                            return c
         
     | 
| 337 | 
         
            +
                    return self.__class__(parent=self, move=move)
         
     | 
| 338 | 
         
            +
             
     | 
| 339 | 
         
            +
                @property
         
     | 
| 340 | 
         
            +
                def initial_player(self):  # player for first node
         
     | 
| 341 | 
         
            +
                    root = self.root
         
     | 
| 342 | 
         
            +
                    if "PL" in root.properties:  # explicit
         
     | 
| 343 | 
         
            +
                        return "B" if self.root.get_property("PL").upper().strip() == "B" else "W"
         
     | 
| 344 | 
         
            +
                    elif root.children:  # child exist, use it if not placement
         
     | 
| 345 | 
         
            +
                        for child in root.children:
         
     | 
| 346 | 
         
            +
                            for color in "BW":
         
     | 
| 347 | 
         
            +
                                if color in child.properties:
         
     | 
| 348 | 
         
            +
                                    return color
         
     | 
| 349 | 
         
            +
                    # b move or setup with only black moves like handicap
         
     | 
| 350 | 
         
            +
                    if "AB" in self.properties and "AW" not in self.properties:
         
     | 
| 351 | 
         
            +
                        return "W"
         
     | 
| 352 | 
         
            +
                    else:
         
     | 
| 353 | 
         
            +
                        return "B"
         
     | 
| 354 | 
         
            +
             
     | 
| 355 | 
         
            +
                @property
         
     | 
| 356 | 
         
            +
                def next_player(self):
         
     | 
| 357 | 
         
            +
                    """Returns player to move"""
         
     | 
| 358 | 
         
            +
                    if self.is_root:
         
     | 
| 359 | 
         
            +
                        return self.initial_player
         
     | 
| 360 | 
         
            +
                    elif "B" in self.properties:
         
     | 
| 361 | 
         
            +
                        return "W"
         
     | 
| 362 | 
         
            +
                    elif "W" in self.properties:
         
     | 
| 363 | 
         
            +
                        return "B"
         
     | 
| 364 | 
         
            +
                    else:  # only placements, find a parent node with a real move. TODO: better placement support
         
     | 
| 365 | 
         
            +
                        return self.parent.next_player
         
     | 
| 366 | 
         
            +
             
     | 
| 367 | 
         
            +
                @property
         
     | 
| 368 | 
         
            +
                def player(self):
         
     | 
| 369 | 
         
            +
                    """Returns player that moved last. nb root is considered white played if no handicap stones are placed"""
         
     | 
| 370 | 
         
            +
                    if "B" in self.properties or ("AB" in self.properties and "W" not in self.properties):
         
     | 
| 371 | 
         
            +
                        return "B"
         
     | 
| 372 | 
         
            +
                    else:
         
     | 
| 373 | 
         
            +
                        return "W"
         
     | 
| 374 | 
         
            +
             
     | 
| 375 | 
         
            +
                def place_handicap_stones(self, n_handicaps, tygem=False):
         
     | 
| 376 | 
         
            +
                    board_size_x, board_size_y = self.board_size
         
     | 
| 377 | 
         
            +
                    if min(board_size_x, board_size_y) < 3:
         
     | 
| 378 | 
         
            +
                        return  # No
         
     | 
| 379 | 
         
            +
                    near_x = 3 if board_size_x >= 13 else min(2, board_size_x - 1)
         
     | 
| 380 | 
         
            +
                    near_y = 3 if board_size_y >= 13 else min(2, board_size_y - 1)
         
     | 
| 381 | 
         
            +
                    far_x = board_size_x - 1 - near_x
         
     | 
| 382 | 
         
            +
                    far_y = board_size_y - 1 - near_y
         
     | 
| 383 | 
         
            +
                    middle_x = board_size_x // 2  # what for even sizes?
         
     | 
| 384 | 
         
            +
                    middle_y = board_size_y // 2
         
     | 
| 385 | 
         
            +
                    if n_handicaps > 9 and board_size_x == board_size_y:
         
     | 
| 386 | 
         
            +
                        stones_per_row = math.ceil(math.sqrt(n_handicaps))
         
     | 
| 387 | 
         
            +
                        spacing = (far_x - near_x) / (stones_per_row - 1)
         
     | 
| 388 | 
         
            +
                        if spacing < near_x:
         
     | 
| 389 | 
         
            +
                            far_x += 1
         
     | 
| 390 | 
         
            +
                            near_x -= 1
         
     | 
| 391 | 
         
            +
                            spacing = (far_x - near_x) / (stones_per_row - 1)
         
     | 
| 392 | 
         
            +
                        coords = list({math.floor(0.5 + near_x + i * spacing) for i in range(stones_per_row)})
         
     | 
| 393 | 
         
            +
                        stones = sorted(
         
     | 
| 394 | 
         
            +
                            [(x, y) for x in coords for y in coords],
         
     | 
| 395 | 
         
            +
                            key=lambda xy: -((xy[0] - (board_size_x - 1) / 2) ** 2 + (xy[1] - (board_size_y - 1) / 2) ** 2),
         
     | 
| 396 | 
         
            +
                        )
         
     | 
| 397 | 
         
            +
                    else:  # max 9
         
     | 
| 398 | 
         
            +
                        stones = [(far_x, far_y), (near_x, near_y), (far_x, near_y), (near_x, far_y)]
         
     | 
| 399 | 
         
            +
                        if n_handicaps % 2 == 1:
         
     | 
| 400 | 
         
            +
                            stones.append((middle_x, middle_y))
         
     | 
| 401 | 
         
            +
                        stones += [(near_x, middle_y), (far_x, middle_y), (middle_x, near_y), (middle_x, far_y)]
         
     | 
| 402 | 
         
            +
                    if tygem:
         
     | 
| 403 | 
         
            +
                        stones[2], stones[3] = stones[3], stones[2]
         
     | 
| 404 | 
         
            +
                    self.set_property(
         
     | 
| 405 | 
         
            +
                        "AB", list({Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]})
         
     | 
| 406 | 
         
            +
                    )
         
     | 
| 407 | 
         
            +
             
     | 
| 408 | 
         
            +
             
     | 
| 409 | 
         
            +
            class SGF:
         
     | 
| 410 | 
         
            +
                DEFAULT_ENCODING = "UTF-8"
         
     | 
| 411 | 
         
            +
             
     | 
| 412 | 
         
            +
                _NODE_CLASS = SGFNode  # Class used for SGF Nodes, can change this to something that inherits from SGFNode
         
     | 
| 413 | 
         
            +
                # https://xkcd.com/1171/
         
     | 
| 414 | 
         
            +
                SGFPROP_PAT = re.compile(r"\s*(?:\(|\)|;|(\w+)((\s*\[([^\]\\]|\\.)*\])+))", flags=re.DOTALL)
         
     | 
| 415 | 
         
            +
                SGF_PAT = re.compile(r"\(;.*\)", flags=re.DOTALL)
         
     | 
| 416 | 
         
            +
             
     | 
| 417 | 
         
            +
                @classmethod
         
     | 
| 418 | 
         
            +
                def parse_sgf(cls, input_str) -> SGFNode:
         
     | 
| 419 | 
         
            +
                    """Parse a string as SGF."""
         
     | 
| 420 | 
         
            +
                    match = re.search(cls.SGF_PAT, input_str)
         
     | 
| 421 | 
         
            +
                    clipped_str = match.group() if match else input_str
         
     | 
| 422 | 
         
            +
                    root = cls(clipped_str).root
         
     | 
| 423 | 
         
            +
                    # Fix weird FoxGo server KM values
         
     | 
| 424 | 
         
            +
                    if "foxwq" in root.get_list_property("AP", []):
         
     | 
| 425 | 
         
            +
                        if int(root.get_property("HA", 0)) >= 1:
         
     | 
| 426 | 
         
            +
                            corrected_komi = 0.5
         
     | 
| 427 | 
         
            +
                        elif root.get_property("RU").lower() in ["chinese", "cn"]:
         
     | 
| 428 | 
         
            +
                            corrected_komi = 7.5
         
     | 
| 429 | 
         
            +
                        else:
         
     | 
| 430 | 
         
            +
                            corrected_komi = 6.5
         
     | 
| 431 | 
         
            +
                        root.set_property("KM", corrected_komi)
         
     | 
| 432 | 
         
            +
                    return root
         
     | 
| 433 | 
         
            +
             
     | 
| 434 | 
         
            +
                @classmethod
         
     | 
| 435 | 
         
            +
                def parse_file(cls, filename, encoding=None) -> SGFNode:
         
     | 
| 436 | 
         
            +
                    is_gib = filename.lower().endswith(".gib")
         
     | 
| 437 | 
         
            +
                    is_ngf = filename.lower().endswith(".ngf")
         
     | 
| 438 | 
         
            +
             
     | 
| 439 | 
         
            +
                    """Parse a file as SGF, encoding will be detected if not given."""
         
     | 
| 440 | 
         
            +
                    with open(filename, "rb") as f:
         
     | 
| 441 | 
         
            +
                        bin_contents = f.read()
         
     | 
| 442 | 
         
            +
                        if not encoding:
         
     | 
| 443 | 
         
            +
                            if is_gib or is_ngf or b"AP[foxwq]" in bin_contents:
         
     | 
| 444 | 
         
            +
                                encoding = "utf8"
         
     | 
| 445 | 
         
            +
                            else:  # sgf
         
     | 
| 446 | 
         
            +
                                match = re.search(rb"CA\[(.*?)\]", bin_contents)
         
     | 
| 447 | 
         
            +
                                if match:
         
     | 
| 448 | 
         
            +
                                    encoding = match[1].decode("ascii", errors="ignore")
         
     | 
| 449 | 
         
            +
                                else:
         
     | 
| 450 | 
         
            +
                                    encoding = chardet.detect(bin_contents[:300])["encoding"]
         
     | 
| 451 | 
         
            +
                                    # workaround for some compatibility issues for Windows-1252 and GB2312 encodings
         
     | 
| 452 | 
         
            +
                                    if encoding == "Windows-1252" or encoding == "GB2312":
         
     | 
| 453 | 
         
            +
                                        encoding = "GBK"
         
     | 
| 454 | 
         
            +
                        try:
         
     | 
| 455 | 
         
            +
                            decoded = bin_contents.decode(encoding=encoding, errors="ignore")
         
     | 
| 456 | 
         
            +
                        except LookupError:
         
     | 
| 457 | 
         
            +
                            decoded = bin_contents.decode(encoding=cls.DEFAULT_ENCODING, errors="ignore")
         
     | 
| 458 | 
         
            +
                        if is_ngf:
         
     | 
| 459 | 
         
            +
                            return cls.parse_ngf(decoded)
         
     | 
| 460 | 
         
            +
                        if is_gib:
         
     | 
| 461 | 
         
            +
                            return cls.parse_gib(decoded)
         
     | 
| 462 | 
         
            +
                        else:  # sgf
         
     | 
| 463 | 
         
            +
                            return cls.parse_sgf(decoded)
         
     | 
| 464 | 
         
            +
             
     | 
| 465 | 
         
            +
                def __init__(self, contents):
         
     | 
| 466 | 
         
            +
                    self.contents = contents
         
     | 
| 467 | 
         
            +
                    try:
         
     | 
| 468 | 
         
            +
                        self.ix = self.contents.index("(") + 1
         
     | 
| 469 | 
         
            +
                    except ValueError:
         
     | 
| 470 | 
         
            +
                        raise ParseError(f"Parse error: Expected '(' at start, found {self.contents[:50]}")
         
     | 
| 471 | 
         
            +
                    self.root = self._NODE_CLASS()
         
     | 
| 472 | 
         
            +
                    self._parse_branch(self.root)
         
     | 
| 473 | 
         
            +
             
     | 
| 474 | 
         
            +
                def _parse_branch(self, current_move: SGFNode):
         
     | 
| 475 | 
         
            +
                    while self.ix < len(self.contents):
         
     | 
| 476 | 
         
            +
                        match = re.match(self.SGFPROP_PAT, self.contents[self.ix :])
         
     | 
| 477 | 
         
            +
                        if not match:
         
     | 
| 478 | 
         
            +
                            break
         
     | 
| 479 | 
         
            +
                        self.ix += len(match[0])
         
     | 
| 480 | 
         
            +
                        matched_item = match[0].strip()
         
     | 
| 481 | 
         
            +
                        if matched_item == ")":
         
     | 
| 482 | 
         
            +
                            return
         
     | 
| 483 | 
         
            +
                        if matched_item == "(":
         
     | 
| 484 | 
         
            +
                            self._parse_branch(self._NODE_CLASS(parent=current_move))
         
     | 
| 485 | 
         
            +
                        elif matched_item == ";":
         
     | 
| 486 | 
         
            +
                            # ignore ;) for old SGF
         
     | 
| 487 | 
         
            +
                            useless = self.ix < len(self.contents) and self.contents[self.ix :].strip() == ")"
         
     | 
| 488 | 
         
            +
                            # ignore ; that generate empty nodes
         
     | 
| 489 | 
         
            +
                            if not (current_move.empty or useless):
         
     | 
| 490 | 
         
            +
                                current_move = self._NODE_CLASS(parent=current_move)
         
     | 
| 491 | 
         
            +
                        else:
         
     | 
| 492 | 
         
            +
                            property, value = match[1], match[2].strip()[1:-1]
         
     | 
| 493 | 
         
            +
                            values = re.split(r"\]\s*\[", value)
         
     | 
| 494 | 
         
            +
                            current_move.add_list_property(property, [SGFNode._unescape_value(v) for v in values])
         
     | 
| 495 | 
         
            +
                    if self.ix < len(self.contents):
         
     | 
| 496 | 
         
            +
                        raise ParseError(f"Parse Error: unexpected character at {self.contents[self.ix:self.ix+25]}")
         
     | 
| 497 | 
         
            +
                    raise ParseError("Parse Error: expected ')' at end of input.")
         
     | 
| 498 | 
         
            +
             
     | 
| 499 | 
         
            +
                # NGF parser adapted from https://github.com/fohristiwhirl/gofish/
         
     | 
| 500 | 
         
            +
                @classmethod
         
     | 
| 501 | 
         
            +
                def parse_ngf(cls, ngf):
         
     | 
| 502 | 
         
            +
                    ngf = ngf.strip()
         
     | 
| 503 | 
         
            +
                    lines = ngf.split("\n")
         
     | 
| 504 | 
         
            +
             
     | 
| 505 | 
         
            +
                    try:
         
     | 
| 506 | 
         
            +
                        boardsize = int(lines[1])
         
     | 
| 507 | 
         
            +
                        handicap = int(lines[5])
         
     | 
| 508 | 
         
            +
                        pw = lines[2].split()[0]
         
     | 
| 509 | 
         
            +
                        pb = lines[3].split()[0]
         
     | 
| 510 | 
         
            +
                        rawdate = lines[8][0:8]
         
     | 
| 511 | 
         
            +
                        komi = float(lines[7])
         
     | 
| 512 | 
         
            +
             
     | 
| 513 | 
         
            +
                        if handicap == 0 and int(komi) == komi:
         
     | 
| 514 | 
         
            +
                            komi += 0.5
         
     | 
| 515 | 
         
            +
             
     | 
| 516 | 
         
            +
                    except (IndexError, ValueError):
         
     | 
| 517 | 
         
            +
                        boardsize = 19
         
     | 
| 518 | 
         
            +
                        handicap = 0
         
     | 
| 519 | 
         
            +
                        pw = ""
         
     | 
| 520 | 
         
            +
                        pb = ""
         
     | 
| 521 | 
         
            +
                        rawdate = ""
         
     | 
| 522 | 
         
            +
                        komi = 0
         
     | 
| 523 | 
         
            +
             
     | 
| 524 | 
         
            +
                    re = ""
         
     | 
| 525 | 
         
            +
                    try:
         
     | 
| 526 | 
         
            +
                        if "hite win" in lines[10]:
         
     | 
| 527 | 
         
            +
                            re = "W+"
         
     | 
| 528 | 
         
            +
                        elif "lack win" in lines[10]:
         
     | 
| 529 | 
         
            +
                            re = "B+"
         
     | 
| 530 | 
         
            +
                    except IndexError:
         
     | 
| 531 | 
         
            +
                        pass
         
     | 
| 532 | 
         
            +
             
     | 
| 533 | 
         
            +
                    if handicap < 0 or handicap > 9:
         
     | 
| 534 | 
         
            +
                        raise ParseError(f"Handicap {handicap} out of range")
         
     | 
| 535 | 
         
            +
             
     | 
| 536 | 
         
            +
                    root = cls._NODE_CLASS()
         
     | 
| 537 | 
         
            +
                    node = root
         
     | 
| 538 | 
         
            +
             
     | 
| 539 | 
         
            +
                    # Set root values...
         
     | 
| 540 | 
         
            +
             
     | 
| 541 | 
         
            +
                    root.set_property("SZ", boardsize)
         
     | 
| 542 | 
         
            +
             
     | 
| 543 | 
         
            +
                    if handicap >= 2:
         
     | 
| 544 | 
         
            +
                        root.set_property("HA", handicap)
         
     | 
| 545 | 
         
            +
                        root.place_handicap_stones(handicap, tygem=True)  # While this isn't Tygem, it uses the same layout
         
     | 
| 546 | 
         
            +
             
     | 
| 547 | 
         
            +
                    if komi:
         
     | 
| 548 | 
         
            +
                        root.set_property("KM", komi)
         
     | 
| 549 | 
         
            +
             
     | 
| 550 | 
         
            +
                    if len(rawdate) == 8:
         
     | 
| 551 | 
         
            +
                        ok = True
         
     | 
| 552 | 
         
            +
                        for n in range(8):
         
     | 
| 553 | 
         
            +
                            if rawdate[n] not in "0123456789":
         
     | 
| 554 | 
         
            +
                                ok = False
         
     | 
| 555 | 
         
            +
                        if ok:
         
     | 
| 556 | 
         
            +
                            date = rawdate[0:4] + "-" + rawdate[4:6] + "-" + rawdate[6:8]
         
     | 
| 557 | 
         
            +
                            root.set_property("DT", date)
         
     | 
| 558 | 
         
            +
             
     | 
| 559 | 
         
            +
                    if pw:
         
     | 
| 560 | 
         
            +
                        root.set_property("PW", pw)
         
     | 
| 561 | 
         
            +
                    if pb:
         
     | 
| 562 | 
         
            +
                        root.set_property("PB", pb)
         
     | 
| 563 | 
         
            +
             
     | 
| 564 | 
         
            +
                    if re:
         
     | 
| 565 | 
         
            +
                        root.set_property("RE", re)
         
     | 
| 566 | 
         
            +
             
     | 
| 567 | 
         
            +
                    # Main parser...
         
     | 
| 568 | 
         
            +
             
     | 
| 569 | 
         
            +
                    for line in lines:
         
     | 
| 570 | 
         
            +
                        line = line.strip().upper()
         
     | 
| 571 | 
         
            +
             
     | 
| 572 | 
         
            +
                        if len(line) >= 7:
         
     | 
| 573 | 
         
            +
                            if line[0:2] == "PM":
         
     | 
| 574 | 
         
            +
                                if line[4] in ["B", "W"]:
         
     | 
| 575 | 
         
            +
             
     | 
| 576 | 
         
            +
                                    # move format is similar to SGF, but uppercase and out-by-1
         
     | 
| 577 | 
         
            +
             
     | 
| 578 | 
         
            +
                                    key = line[4]
         
     | 
| 579 | 
         
            +
                                    raw_move = line[5:7].lower()
         
     | 
| 580 | 
         
            +
                                    if raw_move == "aa":
         
     | 
| 581 | 
         
            +
                                        value = "" # pass
         
     | 
| 582 | 
         
            +
                                    else:
         
     | 
| 583 | 
         
            +
                                        value = chr(ord(raw_move[0]) - 1) + chr(ord(raw_move[1]) - 1)
         
     | 
| 584 | 
         
            +
             
     | 
| 585 | 
         
            +
                                    node = cls._NODE_CLASS(parent=node)
         
     | 
| 586 | 
         
            +
                                    node.set_property(key, value)
         
     | 
| 587 | 
         
            +
             
     | 
| 588 | 
         
            +
                    if len(root.children) == 0:  # We'll assume we failed in this case
         
     | 
| 589 | 
         
            +
                        raise ParseError("Found no moves")
         
     | 
| 590 | 
         
            +
             
     | 
| 591 | 
         
            +
                    return root
         
     | 
| 592 | 
         
            +
             
     | 
| 593 | 
         
            +
                # GIB parser adapted from https://github.com/fohristiwhirl/gofish/
         
     | 
| 594 | 
         
            +
                @classmethod
         
     | 
| 595 | 
         
            +
                def parse_gib(cls, gib):
         
     | 
| 596 | 
         
            +
                    def parse_player_name(raw):
         
     | 
| 597 | 
         
            +
                        name = raw
         
     | 
| 598 | 
         
            +
                        rank = ""
         
     | 
| 599 | 
         
            +
                        foo = raw.split("(")
         
     | 
| 600 | 
         
            +
                        if len(foo) == 2:
         
     | 
| 601 | 
         
            +
                            if foo[1][-1] == ")":
         
     | 
| 602 | 
         
            +
                                name = foo[0].strip()
         
     | 
| 603 | 
         
            +
                                rank = foo[1][0:-1]
         
     | 
| 604 | 
         
            +
                        return name, rank
         
     | 
| 605 | 
         
            +
             
     | 
| 606 | 
         
            +
                    def gib_make_result(grlt, zipsu):
         
     | 
| 607 | 
         
            +
                        easycases = {3: "B+R", 4: "W+R", 7: "B+T", 8: "W+T"}
         
     | 
| 608 | 
         
            +
             
     | 
| 609 | 
         
            +
                        if grlt in easycases:
         
     | 
| 610 | 
         
            +
                            return easycases[grlt]
         
     | 
| 611 | 
         
            +
             
     | 
| 612 | 
         
            +
                        if grlt in [0, 1]:
         
     | 
| 613 | 
         
            +
                            return "{}+{}".format("B" if grlt == 0 else "W", zipsu / 10)
         
     | 
| 614 | 
         
            +
             
     | 
| 615 | 
         
            +
                        return ""
         
     | 
| 616 | 
         
            +
             
     | 
| 617 | 
         
            +
                    def gib_get_result(line, grlt_regex, zipsu_regex):
         
     | 
| 618 | 
         
            +
                        try:
         
     | 
| 619 | 
         
            +
                            grlt = int(re.search(grlt_regex, line).group(1))
         
     | 
| 620 | 
         
            +
                            zipsu = int(re.search(zipsu_regex, line).group(1))
         
     | 
| 621 | 
         
            +
                        except:  # noqa E722
         
     | 
| 622 | 
         
            +
                            return ""
         
     | 
| 623 | 
         
            +
                        return gib_make_result(grlt, zipsu)
         
     | 
| 624 | 
         
            +
             
     | 
| 625 | 
         
            +
                    root = cls._NODE_CLASS()
         
     | 
| 626 | 
         
            +
                    node = root
         
     | 
| 627 | 
         
            +
             
     | 
| 628 | 
         
            +
                    lines = gib.split("\n")
         
     | 
| 629 | 
         
            +
                    for line in lines:
         
     | 
| 630 | 
         
            +
                        line = line.strip()
         
     | 
| 631 | 
         
            +
                        if line.startswith("\\[GAMEBLACKNAME=") and line.endswith("\\]"):
         
     | 
| 632 | 
         
            +
                            s = line[16:-2]
         
     | 
| 633 | 
         
            +
                            name, rank = parse_player_name(s)
         
     | 
| 634 | 
         
            +
                            if name:
         
     | 
| 635 | 
         
            +
                                root.set_property("PB", name)
         
     | 
| 636 | 
         
            +
                            if rank:
         
     | 
| 637 | 
         
            +
                                root.set_property("BR", rank)
         
     | 
| 638 | 
         
            +
             
     | 
| 639 | 
         
            +
                        if line.startswith("\\[GAMEWHITENAME=") and line.endswith("\\]"):
         
     | 
| 640 | 
         
            +
                            s = line[16:-2]
         
     | 
| 641 | 
         
            +
                            name, rank = parse_player_name(s)
         
     | 
| 642 | 
         
            +
                            if name:
         
     | 
| 643 | 
         
            +
                                root.set_property("PW", name)
         
     | 
| 644 | 
         
            +
                            if rank:
         
     | 
| 645 | 
         
            +
                                root.set_property("WR", rank)
         
     | 
| 646 | 
         
            +
             
     | 
| 647 | 
         
            +
                        if line.startswith("\\[GAMEINFOMAIN="):
         
     | 
| 648 | 
         
            +
                            result = gib_get_result(line, r"GRLT:(\d+),", r"ZIPSU:(\d+),")
         
     | 
| 649 | 
         
            +
                            if result:
         
     | 
| 650 | 
         
            +
                                root.set_property("RE", result)
         
     | 
| 651 | 
         
            +
                                try:
         
     | 
| 652 | 
         
            +
                                    komi = int(re.search(r"GONGJE:(\d+),", line).group(1)) / 10
         
     | 
| 653 | 
         
            +
                                    if komi:
         
     | 
| 654 | 
         
            +
                                        root.set_property("KM", komi)
         
     | 
| 655 | 
         
            +
                                except:  # noqa E722
         
     | 
| 656 | 
         
            +
                                    pass
         
     | 
| 657 | 
         
            +
             
     | 
| 658 | 
         
            +
                        if line.startswith("\\[GAMETAG="):
         
     | 
| 659 | 
         
            +
                            if "DT" not in root.properties:
         
     | 
| 660 | 
         
            +
                                try:
         
     | 
| 661 | 
         
            +
                                    match = re.search(r"C(\d\d\d\d):(\d\d):(\d\d)", line)
         
     | 
| 662 | 
         
            +
                                    date = "{}-{}-{}".format(match.group(1), match.group(2), match.group(3))
         
     | 
| 663 | 
         
            +
                                    root.set_property("DT", date)
         
     | 
| 664 | 
         
            +
                                except:  # noqa E722
         
     | 
| 665 | 
         
            +
                                    pass
         
     | 
| 666 | 
         
            +
             
     | 
| 667 | 
         
            +
                            if "RE" not in root.properties:
         
     | 
| 668 | 
         
            +
                                result = gib_get_result(line, r",W(\d+),", r",Z(\d+),")
         
     | 
| 669 | 
         
            +
                                if result:
         
     | 
| 670 | 
         
            +
                                    root.set_property("RE", result)
         
     | 
| 671 | 
         
            +
             
     | 
| 672 | 
         
            +
                            if "KM" not in root.properties:
         
     | 
| 673 | 
         
            +
                                try:
         
     | 
| 674 | 
         
            +
                                    komi = int(re.search(r",G(\d+),", line).group(1)) / 10
         
     | 
| 675 | 
         
            +
                                    if komi:
         
     | 
| 676 | 
         
            +
                                        root.set_property("KM", komi)
         
     | 
| 677 | 
         
            +
                                except:  # noqa E722
         
     | 
| 678 | 
         
            +
                                    pass
         
     | 
| 679 | 
         
            +
             
     | 
| 680 | 
         
            +
                        if line[0:3] == "INI":
         
     | 
| 681 | 
         
            +
                            if node is not root:
         
     | 
| 682 | 
         
            +
                                raise ParseError("Node is not root")
         
     | 
| 683 | 
         
            +
                            setup = line.split()
         
     | 
| 684 | 
         
            +
                            try:
         
     | 
| 685 | 
         
            +
                                handicap = int(setup[3])
         
     | 
| 686 | 
         
            +
                            except ParseError:
         
     | 
| 687 | 
         
            +
                                continue
         
     | 
| 688 | 
         
            +
             
     | 
| 689 | 
         
            +
                            if handicap < 0 or handicap > 9:
         
     | 
| 690 | 
         
            +
                                raise ParseError(f"Handicap {handicap} out of range")
         
     | 
| 691 | 
         
            +
             
     | 
| 692 | 
         
            +
                            if handicap >= 2:
         
     | 
| 693 | 
         
            +
                                root.set_property("HA", handicap)
         
     | 
| 694 | 
         
            +
                                root.place_handicap_stones(handicap, tygem=True)
         
     | 
| 695 | 
         
            +
             
     | 
| 696 | 
         
            +
                        if line[0:3] == "STO":
         
     | 
| 697 | 
         
            +
                            move = line.split()
         
     | 
| 698 | 
         
            +
                            key = "B" if move[3] == "1" else "W"
         
     | 
| 699 | 
         
            +
                            try:
         
     | 
| 700 | 
         
            +
                                x = int(move[4])
         
     | 
| 701 | 
         
            +
                                y = 18 - int(move[5])
         
     | 
| 702 | 
         
            +
                                if not (0 <= x < 19 and 0 <= y < 19):
         
     | 
| 703 | 
         
            +
                                    raise ParseError(f"Coordinates for move ({x},{y}) out of range on line {line}")
         
     | 
| 704 | 
         
            +
                                value = Move(coords=(x, y)).sgf(board_size=(19, 19))
         
     | 
| 705 | 
         
            +
                            except IndexError:
         
     | 
| 706 | 
         
            +
                                continue
         
     | 
| 707 | 
         
            +
             
     | 
| 708 | 
         
            +
                            node = cls._NODE_CLASS(parent=node)
         
     | 
| 709 | 
         
            +
                            node.set_property(key, value)
         
     | 
| 710 | 
         
            +
             
     | 
| 711 | 
         
            +
                    if len(root.children) == 0:  # We'll assume we failed in this case
         
     | 
| 712 | 
         
            +
                        raise ParseError("No valid nodes found")
         
     | 
| 713 | 
         
            +
             
     | 
| 714 | 
         
            +
                    return root
         
     | 
    	
        katrain/katrain/core/tsumego_frame.py
    ADDED
    
    | 
         @@ -0,0 +1,289 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            from katrain.core.game_node import GameNode
         
     | 
| 2 | 
         
            +
            from katrain.core.sgf_parser import Move
         
     | 
| 3 | 
         
            +
             
     | 
| 4 | 
         
            +
            # tsumego frame ported from lizgoban by kaorahi
         
     | 
| 5 | 
         
            +
            # note: coords = (j, i) in katrain
         
     | 
| 6 | 
         
            +
             
     | 
| 7 | 
         
            +
            near_to_edge = 2
         
     | 
| 8 | 
         
            +
            offence_to_win = 5
         
     | 
| 9 | 
         
            +
             
     | 
| 10 | 
         
            +
            BLACK = "B"
         
     | 
| 11 | 
         
            +
            WHITE = "W"
         
     | 
| 12 | 
         
            +
             
     | 
| 13 | 
         
            +
             
     | 
| 14 | 
         
            +
            def tsumego_frame_from_katrain_game(game, komi, black_to_play_p, ko_p, margin):
         
     | 
| 15 | 
         
            +
                current_node = game.current_node
         
     | 
| 16 | 
         
            +
                bw_board = [[game.chains[c][0].player if c >= 0 else "-" for c in line] for line in game.board]
         
     | 
| 17 | 
         
            +
                isize, jsize = ij_sizes(bw_board)
         
     | 
| 18 | 
         
            +
                blacks, whites, analysis_region = tsumego_frame(bw_board, komi, black_to_play_p, ko_p, margin)
         
     | 
| 19 | 
         
            +
                sgf_blacks = katrain_sgf_from_ijs(blacks, isize, jsize, "B")
         
     | 
| 20 | 
         
            +
                sgf_whites = katrain_sgf_from_ijs(whites, isize, jsize, "W")
         
     | 
| 21 | 
         
            +
             
     | 
| 22 | 
         
            +
                played_node = GameNode(parent=current_node, properties={"AB": sgf_blacks, "AW": sgf_whites})  # this inserts
         
     | 
| 23 | 
         
            +
             
     | 
| 24 | 
         
            +
                katrain_region = analysis_region and (analysis_region[1], analysis_region[0])
         
     | 
| 25 | 
         
            +
                return (played_node, katrain_region)
         
     | 
| 26 | 
         
            +
             
     | 
| 27 | 
         
            +
             
     | 
| 28 | 
         
            +
            def katrain_sgf_from_ijs(ijs, isize, jsize, player):
         
     | 
| 29 | 
         
            +
                return [Move((j, i)).sgf((jsize, isize)) for i, j in ijs]
         
     | 
| 30 | 
         
            +
             
     | 
| 31 | 
         
            +
             
     | 
| 32 | 
         
            +
            def tsumego_frame(bw_board, komi, black_to_play_p, ko_p, margin):
         
     | 
| 33 | 
         
            +
                stones = stones_from_bw_board(bw_board)
         
     | 
| 34 | 
         
            +
                filled_stones = tsumego_frame_stones(stones, komi, black_to_play_p, ko_p, margin)
         
     | 
| 35 | 
         
            +
                region_pos = pick_all(filled_stones, "tsumego_frame_region_mark")
         
     | 
| 36 | 
         
            +
                bw = pick_all(filled_stones, "tsumego_frame")
         
     | 
| 37 | 
         
            +
                blacks = [(i, j) for i, j, black in bw if black]
         
     | 
| 38 | 
         
            +
                whites = [(i, j) for i, j, black in bw if not black]
         
     | 
| 39 | 
         
            +
                return (blacks, whites, get_analysis_region(region_pos))
         
     | 
| 40 | 
         
            +
             
     | 
| 41 | 
         
            +
             
     | 
| 42 | 
         
            +
            def pick_all(stones, key):
         
     | 
| 43 | 
         
            +
                return [[i, j, s.get("black")] for i, row in enumerate(stones) for j, s in enumerate(row) if s.get(key)]
         
     | 
| 44 | 
         
            +
             
     | 
| 45 | 
         
            +
             
     | 
| 46 | 
         
            +
            def get_analysis_region(region_pos):
         
     | 
| 47 | 
         
            +
                if len(region_pos) == 0:
         
     | 
| 48 | 
         
            +
                    return None
         
     | 
| 49 | 
         
            +
                ai, aj, dummy = tuple(zip(*region_pos))
         
     | 
| 50 | 
         
            +
                ri = (min(ai), max(ai))
         
     | 
| 51 | 
         
            +
                rj = (min(aj), max(aj))
         
     | 
| 52 | 
         
            +
                return ri[0] < ri[1] and rj[0] < rj[1] and (ri, rj)
         
     | 
| 53 | 
         
            +
             
     | 
| 54 | 
         
            +
             
     | 
| 55 | 
         
            +
            def tsumego_frame_stones(stones, komi, black_to_play_p, ko_p, margin):
         
     | 
| 56 | 
         
            +
                sizes = ij_sizes(stones)
         
     | 
| 57 | 
         
            +
                isize, jsize = sizes
         
     | 
| 58 | 
         
            +
                ijs = [
         
     | 
| 59 | 
         
            +
                    {"i": i, "j": j, "black": h.get("black")}
         
     | 
| 60 | 
         
            +
                    for i, row in enumerate(stones)
         
     | 
| 61 | 
         
            +
                    for j, h in enumerate(row)
         
     | 
| 62 | 
         
            +
                    if h.get("stone")
         
     | 
| 63 | 
         
            +
                ]
         
     | 
| 64 | 
         
            +
             
     | 
| 65 | 
         
            +
                if len(ijs) == 0:
         
     | 
| 66 | 
         
            +
                    return []
         
     | 
| 67 | 
         
            +
                # find range of problem
         
     | 
| 68 | 
         
            +
                top = min_by(ijs, "i", +1)
         
     | 
| 69 | 
         
            +
                left = min_by(ijs, "j", +1)
         
     | 
| 70 | 
         
            +
                bottom = min_by(ijs, "i", -1)
         
     | 
| 71 | 
         
            +
                right = min_by(ijs, "j", -1)
         
     | 
| 72 | 
         
            +
                imin = snap0(top["i"])
         
     | 
| 73 | 
         
            +
                jmin = snap0(left["j"])
         
     | 
| 74 | 
         
            +
                imax = snapS(bottom["i"], isize)
         
     | 
| 75 | 
         
            +
                jmax = snapS(right["j"], jsize)
         
     | 
| 76 | 
         
            +
                # flip/rotate for standard position
         
     | 
| 77 | 
         
            +
                # don't mix flip and swap (FF = SS = identity, but SFSF != identity)
         
     | 
| 78 | 
         
            +
                flip_spec = (
         
     | 
| 79 | 
         
            +
                    [False, False, True] if imin < jmin else [need_flip_p(imin, imax, isize), need_flip_p(jmin, jmax, jsize), False]
         
     | 
| 80 | 
         
            +
                )
         
     | 
| 81 | 
         
            +
                if True in flip_spec:
         
     | 
| 82 | 
         
            +
                    flipped = flip_stones(stones, flip_spec)
         
     | 
| 83 | 
         
            +
                    filled = tsumego_frame_stones(flipped, komi, black_to_play_p, ko_p, margin)
         
     | 
| 84 | 
         
            +
                    return flip_stones(filled, flip_spec)
         
     | 
| 85 | 
         
            +
                # put outside stones
         
     | 
| 86 | 
         
            +
                i0 = imin - margin
         
     | 
| 87 | 
         
            +
                i1 = imax + margin
         
     | 
| 88 | 
         
            +
                j0 = jmin - margin
         
     | 
| 89 | 
         
            +
                j1 = jmax + margin
         
     | 
| 90 | 
         
            +
                frame_range = [i0, i1, j0, j1]
         
     | 
| 91 | 
         
            +
                black_to_attack_p = guess_black_to_attack([top, bottom, left, right], sizes)
         
     | 
| 92 | 
         
            +
                put_border(stones, sizes, frame_range, black_to_attack_p)
         
     | 
| 93 | 
         
            +
                put_outside(stones, sizes, frame_range, black_to_attack_p, black_to_play_p, komi)
         
     | 
| 94 | 
         
            +
                put_ko_threat(stones, sizes, frame_range, black_to_attack_p, black_to_play_p, ko_p)
         
     | 
| 95 | 
         
            +
                return stones
         
     | 
| 96 | 
         
            +
             
     | 
| 97 | 
         
            +
             
     | 
| 98 | 
         
            +
            # detect corner/edge/center problems
         
     | 
| 99 | 
         
            +
            # (avoid putting border stones on the first lines)
         
     | 
| 100 | 
         
            +
            def snap(k, to):
         
     | 
| 101 | 
         
            +
                return to if abs(k - to) <= near_to_edge else k
         
     | 
| 102 | 
         
            +
             
     | 
| 103 | 
         
            +
             
     | 
| 104 | 
         
            +
            def snap0(k):
         
     | 
| 105 | 
         
            +
                return snap(k, 0)
         
     | 
| 106 | 
         
            +
             
     | 
| 107 | 
         
            +
             
     | 
| 108 | 
         
            +
            def snapS(k, size):
         
     | 
| 109 | 
         
            +
                return snap(k, size - 1)
         
     | 
| 110 | 
         
            +
             
     | 
| 111 | 
         
            +
             
     | 
| 112 | 
         
            +
            def min_by(ary, key, sign):
         
     | 
| 113 | 
         
            +
                by = [sign * z[key] for z in ary]
         
     | 
| 114 | 
         
            +
                return ary[by.index(min(by))]
         
     | 
| 115 | 
         
            +
             
     | 
| 116 | 
         
            +
             
     | 
| 117 | 
         
            +
            def need_flip_p(kmin, kmax, size):
         
     | 
| 118 | 
         
            +
                return kmin < size - kmax - 1
         
     | 
| 119 | 
         
            +
             
     | 
| 120 | 
         
            +
             
     | 
| 121 | 
         
            +
            def guess_black_to_attack(extrema, sizes):
         
     | 
| 122 | 
         
            +
                return sum([sign_of_color(z) * height2(z, sizes) for z in extrema]) > 0
         
     | 
| 123 | 
         
            +
             
     | 
| 124 | 
         
            +
             
     | 
| 125 | 
         
            +
            def sign_of_color(z):
         
     | 
| 126 | 
         
            +
                return 1 if z["black"] else -1
         
     | 
| 127 | 
         
            +
             
     | 
| 128 | 
         
            +
             
     | 
| 129 | 
         
            +
            def height2(z, sizes):
         
     | 
| 130 | 
         
            +
                isize, jsize = sizes
         
     | 
| 131 | 
         
            +
                return height(z["i"], isize) + height(z["j"], jsize)
         
     | 
| 132 | 
         
            +
             
     | 
| 133 | 
         
            +
             
     | 
| 134 | 
         
            +
            def height(k, size):
         
     | 
| 135 | 
         
            +
                return size - abs(k - (size - 1) / 2)
         
     | 
| 136 | 
         
            +
             
     | 
| 137 | 
         
            +
             
     | 
| 138 | 
         
            +
            ######################################
         
     | 
| 139 | 
         
            +
            # sub
         
     | 
| 140 | 
         
            +
             
     | 
| 141 | 
         
            +
             
     | 
| 142 | 
         
            +
            def put_border(stones, sizes, frame_range, is_black):
         
     | 
| 143 | 
         
            +
                i0, i1, j0, j1 = frame_range
         
     | 
| 144 | 
         
            +
                put_twin(stones, sizes, i0, i1, j0, j1, is_black, False)
         
     | 
| 145 | 
         
            +
                put_twin(stones, sizes, j0, j1, i0, i1, is_black, True)
         
     | 
| 146 | 
         
            +
             
     | 
| 147 | 
         
            +
             
     | 
| 148 | 
         
            +
            def put_twin(stones, sizes, beg, end, at0, at1, is_black, reverse_p):
         
     | 
| 149 | 
         
            +
                for at in (at0, at1):
         
     | 
| 150 | 
         
            +
                    for k in range(beg, end + 1):
         
     | 
| 151 | 
         
            +
                        i, j = (at, k) if reverse_p else (k, at)
         
     | 
| 152 | 
         
            +
                        put_stone(stones, sizes, i, j, is_black, False, True)
         
     | 
| 153 | 
         
            +
             
     | 
| 154 | 
         
            +
             
     | 
| 155 | 
         
            +
            def put_outside(stones, sizes, frame_range, black_to_attack_p, black_to_play_p, komi):
         
     | 
| 156 | 
         
            +
                isize, jsize = sizes
         
     | 
| 157 | 
         
            +
                count = 0
         
     | 
| 158 | 
         
            +
                offense_komi = (+1 if black_to_attack_p else -1) * komi
         
     | 
| 159 | 
         
            +
                defense_area = (isize * jsize - offense_komi - offence_to_win) / 2
         
     | 
| 160 | 
         
            +
                for i in range(isize):
         
     | 
| 161 | 
         
            +
                    for j in range(jsize):
         
     | 
| 162 | 
         
            +
                        if inside_p(i, j, frame_range):
         
     | 
| 163 | 
         
            +
                            continue
         
     | 
| 164 | 
         
            +
                        count += 1
         
     | 
| 165 | 
         
            +
                        black_p = xor(black_to_attack_p, (count <= defense_area))
         
     | 
| 166 | 
         
            +
                        empty_p = (i + j) % 2 == 0 and abs(count - defense_area) > isize
         
     | 
| 167 | 
         
            +
                        put_stone(stones, sizes, i, j, black_p, empty_p)
         
     | 
| 168 | 
         
            +
             
     | 
| 169 | 
         
            +
             
     | 
| 170 | 
         
            +
            # standard position:
         
     | 
| 171 | 
         
            +
            # ? = problem, X = offense, O = defense
         
     | 
| 172 | 
         
            +
            # OOOOOOOOOOOOO
         
     | 
| 173 | 
         
            +
            # OOOOOOOOOOOOO
         
     | 
| 174 | 
         
            +
            # OOOOOOOOOOOOO
         
     | 
| 175 | 
         
            +
            # XXXXXXXXXXXXX
         
     | 
| 176 | 
         
            +
            # XXXXXXXXXXXXX
         
     | 
| 177 | 
         
            +
            # XXXX.........
         
     | 
| 178 | 
         
            +
            # XXXX.XXXXXXXX
         
     | 
| 179 | 
         
            +
            # XXXX.X???????
         
     | 
| 180 | 
         
            +
            # XXXX.X???????
         
     | 
| 181 | 
         
            +
             
     | 
| 182 | 
         
            +
            # (pattern, top_p, left_p)
         
     | 
| 183 | 
         
            +
            offense_ko_threat = (
         
     | 
| 184 | 
         
            +
                """
         
     | 
| 185 | 
         
            +
            ....OOOX.
         
     | 
| 186 | 
         
            +
            .....XXXX
         
     | 
| 187 | 
         
            +
            """,
         
     | 
| 188 | 
         
            +
                True,
         
     | 
| 189 | 
         
            +
                False,
         
     | 
| 190 | 
         
            +
            )
         
     | 
| 191 | 
         
            +
             
     | 
| 192 | 
         
            +
            defense_ko_threat = (
         
     | 
| 193 | 
         
            +
                """
         
     | 
| 194 | 
         
            +
            ..
         
     | 
| 195 | 
         
            +
            ..
         
     | 
| 196 | 
         
            +
            X.
         
     | 
| 197 | 
         
            +
            XO
         
     | 
| 198 | 
         
            +
            OO
         
     | 
| 199 | 
         
            +
            .O
         
     | 
| 200 | 
         
            +
            """,
         
     | 
| 201 | 
         
            +
                False,
         
     | 
| 202 | 
         
            +
                True,
         
     | 
| 203 | 
         
            +
            )
         
     | 
| 204 | 
         
            +
             
     | 
| 205 | 
         
            +
             
     | 
| 206 | 
         
            +
            def put_ko_threat(stones, sizes, frame_range, black_to_attack_p, black_to_play_p, ko_p):
         
     | 
| 207 | 
         
            +
                isize, jsize = sizes
         
     | 
| 208 | 
         
            +
                for_offense_p = xor(ko_p, xor(black_to_attack_p, black_to_play_p))
         
     | 
| 209 | 
         
            +
                pattern, top_p, left_p = offense_ko_threat if for_offense_p else defense_ko_threat
         
     | 
| 210 | 
         
            +
                aa = [list(line) for line in pattern.splitlines() if len(line) > 0]
         
     | 
| 211 | 
         
            +
                height, width = ij_sizes(aa)
         
     | 
| 212 | 
         
            +
                for i, row in enumerate(aa):
         
     | 
| 213 | 
         
            +
                    for j, ch in enumerate(row):
         
     | 
| 214 | 
         
            +
                        ai = i + (0 if top_p else isize - height)
         
     | 
| 215 | 
         
            +
                        aj = j + (0 if left_p else jsize - width)
         
     | 
| 216 | 
         
            +
                        if inside_p(ai, aj, frame_range):
         
     | 
| 217 | 
         
            +
                            return
         
     | 
| 218 | 
         
            +
                        black = xor(black_to_attack_p, ch == "O")
         
     | 
| 219 | 
         
            +
                        empty = ch == "."
         
     | 
| 220 | 
         
            +
                        put_stone(stones, sizes, ai, aj, black, empty)
         
     | 
| 221 | 
         
            +
             
     | 
| 222 | 
         
            +
             
     | 
| 223 | 
         
            +
            def xor(a, b):
         
     | 
| 224 | 
         
            +
                return bool(a) != bool(b)
         
     | 
| 225 | 
         
            +
             
     | 
| 226 | 
         
            +
             
     | 
| 227 | 
         
            +
            ######################################
         
     | 
| 228 | 
         
            +
            # util
         
     | 
| 229 | 
         
            +
             
     | 
| 230 | 
         
            +
             
     | 
| 231 | 
         
            +
            def flip_stones(stones, flip_spec):
         
     | 
| 232 | 
         
            +
                swap_p = flip_spec[2]
         
     | 
| 233 | 
         
            +
                sizes = ij_sizes(stones)
         
     | 
| 234 | 
         
            +
                isize, jsize = sizes
         
     | 
| 235 | 
         
            +
                new_isize, new_jsize = [jsize, isize] if swap_p else [isize, jsize]
         
     | 
| 236 | 
         
            +
                new_stones = [[None for z in range(new_jsize)] for row in range(new_isize)]
         
     | 
| 237 | 
         
            +
                for i, row in enumerate(stones):
         
     | 
| 238 | 
         
            +
                    for j, z in enumerate(row):
         
     | 
| 239 | 
         
            +
                        new_i, new_j = flip_ij((i, j), sizes, flip_spec)
         
     | 
| 240 | 
         
            +
                        new_stones[new_i][new_j] = z
         
     | 
| 241 | 
         
            +
                return new_stones
         
     | 
| 242 | 
         
            +
             
     | 
| 243 | 
         
            +
             
     | 
| 244 | 
         
            +
            def put_stone(stones, sizes, i, j, black, empty, tsumego_frame_region_mark=False):
         
     | 
| 245 | 
         
            +
                isize, jsize = sizes
         
     | 
| 246 | 
         
            +
                if i < 0 or isize <= i or j < 0 or jsize <= j:
         
     | 
| 247 | 
         
            +
                    return
         
     | 
| 248 | 
         
            +
                stones[i][j] = (
         
     | 
| 249 | 
         
            +
                    {}
         
     | 
| 250 | 
         
            +
                    if empty
         
     | 
| 251 | 
         
            +
                    else {
         
     | 
| 252 | 
         
            +
                        "stone": True,
         
     | 
| 253 | 
         
            +
                        "tsumego_frame": True,
         
     | 
| 254 | 
         
            +
                        "black": black,
         
     | 
| 255 | 
         
            +
                        "tsumego_frame_region_mark": tsumego_frame_region_mark,
         
     | 
| 256 | 
         
            +
                    }
         
     | 
| 257 | 
         
            +
                )
         
     | 
| 258 | 
         
            +
             
     | 
| 259 | 
         
            +
             
     | 
| 260 | 
         
            +
            def inside_p(i, j, region):
         
     | 
| 261 | 
         
            +
                i0, i1, j0, j1 = region
         
     | 
| 262 | 
         
            +
                return i0 <= i and i <= i1 and j0 <= j and j <= j1
         
     | 
| 263 | 
         
            +
             
     | 
| 264 | 
         
            +
             
     | 
| 265 | 
         
            +
            def stones_from_bw_board(bw_board):
         
     | 
| 266 | 
         
            +
                return [[stone_from_str(s) for s in row] for row in bw_board]
         
     | 
| 267 | 
         
            +
             
     | 
| 268 | 
         
            +
             
     | 
| 269 | 
         
            +
            def stone_from_str(s):
         
     | 
| 270 | 
         
            +
                black = s == BLACK
         
     | 
| 271 | 
         
            +
                white = s == WHITE
         
     | 
| 272 | 
         
            +
                return {"stone": True, "black": black} if (black or white) else {}
         
     | 
| 273 | 
         
            +
             
     | 
| 274 | 
         
            +
             
     | 
| 275 | 
         
            +
            def ij_sizes(stones):
         
     | 
| 276 | 
         
            +
                return (len(stones), len(stones[0]))
         
     | 
| 277 | 
         
            +
             
     | 
| 278 | 
         
            +
             
     | 
| 279 | 
         
            +
            def flip_ij(ij, sizes, flip_spec):
         
     | 
| 280 | 
         
            +
                i, j = ij
         
     | 
| 281 | 
         
            +
                isize, jsize = sizes
         
     | 
| 282 | 
         
            +
                flip_i, flip_j, swap_ij = flip_spec
         
     | 
| 283 | 
         
            +
                fi = flip1(i, isize, flip_i)
         
     | 
| 284 | 
         
            +
                fj = flip1(j, jsize, flip_j)
         
     | 
| 285 | 
         
            +
                return (fj, fi) if swap_ij else (fi, fj)
         
     | 
| 286 | 
         
            +
             
     | 
| 287 | 
         
            +
             
     | 
| 288 | 
         
            +
            def flip1(k, size, flag):
         
     | 
| 289 | 
         
            +
                return size - 1 - k if flag else k
         
     | 
    	
        katrain/katrain/core/utils.py
    ADDED
    
    | 
         @@ -0,0 +1,99 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import heapq
         
     | 
| 2 | 
         
            +
            import math
         
     | 
| 3 | 
         
            +
            import os
         
     | 
| 4 | 
         
            +
            import random
         
     | 
| 5 | 
         
            +
            import struct
         
     | 
| 6 | 
         
            +
            import sys
         
     | 
| 7 | 
         
            +
            from typing import List, Tuple, TypeVar
         
     | 
| 8 | 
         
            +
             
     | 
| 9 | 
         
            +
            try:
         
     | 
| 10 | 
         
            +
                import importlib.resources as pkg_resources
         
     | 
| 11 | 
         
            +
            except ImportError:
         
     | 
| 12 | 
         
            +
                import importlib_resources as pkg_resources
         
     | 
| 13 | 
         
            +
             
     | 
| 14 | 
         
            +
            T = TypeVar("T")
         
     | 
| 15 | 
         
            +
             
     | 
| 16 | 
         
            +
             
     | 
| 17 | 
         
            +
            def var_to_grid(array_var: List[T], size: Tuple[int, int]) -> List[List[T]]:
         
     | 
| 18 | 
         
            +
                """convert ownership/policy to grid format such that grid[y][x] is for move with coords x,y"""
         
     | 
| 19 | 
         
            +
                ix = 0
         
     | 
| 20 | 
         
            +
                grid = [[]] * size[1]
         
     | 
| 21 | 
         
            +
                for y in range(size[1] - 1, -1, -1):
         
     | 
| 22 | 
         
            +
                    grid[y] = array_var[ix : ix + size[0]]
         
     | 
| 23 | 
         
            +
                    ix += size[0]
         
     | 
| 24 | 
         
            +
                return grid
         
     | 
| 25 | 
         
            +
             
     | 
| 26 | 
         
            +
             
     | 
| 27 | 
         
            +
            def evaluation_class(points_lost, eval_thresholds):
         
     | 
| 28 | 
         
            +
                i = 0
         
     | 
| 29 | 
         
            +
                while i < len(eval_thresholds) - 1 and points_lost > eval_thresholds[i + 1]:
         
     | 
| 30 | 
         
            +
                    i += 1
         
     | 
| 31 | 
         
            +
                return i
         
     | 
| 32 | 
         
            +
             
     | 
| 33 | 
         
            +
            def check_thread(tb=False):  # for checking if draws occur in correct thread
         
     | 
| 34 | 
         
            +
                import threading
         
     | 
| 35 | 
         
            +
             
     | 
| 36 | 
         
            +
                print("build in ", threading.current_thread().ident)
         
     | 
| 37 | 
         
            +
                if tb:
         
     | 
| 38 | 
         
            +
                    import traceback
         
     | 
| 39 | 
         
            +
             
     | 
| 40 | 
         
            +
                    traceback.print_stack()
         
     | 
| 41 | 
         
            +
             
     | 
| 42 | 
         
            +
             
     | 
| 43 | 
         
            +
            PATHS = {}
         
     | 
| 44 | 
         
            +
             
     | 
| 45 | 
         
            +
             
     | 
| 46 | 
         
            +
            def find_package_resource(path, silent_errors=False):
         
     | 
| 47 | 
         
            +
                global PATHS
         
     | 
| 48 | 
         
            +
                if path.startswith("katrain"):
         
     | 
| 49 | 
         
            +
                    if not PATHS.get("PACKAGE"):
         
     | 
| 50 | 
         
            +
                        try:
         
     | 
| 51 | 
         
            +
                            with pkg_resources.path("katrain", "gui.kv") as p:
         
     | 
| 52 | 
         
            +
                                PATHS["PACKAGE"] = os.path.split(str(p))[0]
         
     | 
| 53 | 
         
            +
                        except (ModuleNotFoundError, FileNotFoundError, ValueError) as e:
         
     | 
| 54 | 
         
            +
                            print(f"Package path not found, installation possibly broken. Error: {e}", file=sys.stderr)
         
     | 
| 55 | 
         
            +
                            return f"FILENOTFOUND/{path}"
         
     | 
| 56 | 
         
            +
                    return os.path.join(PATHS["PACKAGE"], path.replace("katrain\\", "katrain/").replace("katrain/", ""))
         
     | 
| 57 | 
         
            +
                else:
         
     | 
| 58 | 
         
            +
                    return os.path.abspath(os.path.expanduser(path))  # absolute path
         
     | 
| 59 | 
         
            +
             
     | 
| 60 | 
         
            +
             
     | 
| 61 | 
         
            +
            def pack_floats(float_list):
         
     | 
| 62 | 
         
            +
                if float_list is None:
         
     | 
| 63 | 
         
            +
                    return b""
         
     | 
| 64 | 
         
            +
                return struct.pack(f"{len(float_list)}e", *float_list)
         
     | 
| 65 | 
         
            +
             
     | 
| 66 | 
         
            +
             
     | 
| 67 | 
         
            +
            def unpack_floats(str, num):
         
     | 
| 68 | 
         
            +
                if not str:
         
     | 
| 69 | 
         
            +
                    return None
         
     | 
| 70 | 
         
            +
                return struct.unpack(f"{num}e", str)
         
     | 
| 71 | 
         
            +
             
     | 
| 72 | 
         
            +
             
     | 
| 73 | 
         
            +
            def format_visits(n):
         
     | 
| 74 | 
         
            +
                if n < 1000:
         
     | 
| 75 | 
         
            +
                    return str(n)
         
     | 
| 76 | 
         
            +
                if n < 1e5:
         
     | 
| 77 | 
         
            +
                    return f"{n/1000:.1f}k"
         
     | 
| 78 | 
         
            +
                if n < 1e6:
         
     | 
| 79 | 
         
            +
                    return f"{n/1000:.0f}k"
         
     | 
| 80 | 
         
            +
                return f"{n/1e6:.0f}M"
         
     | 
| 81 | 
         
            +
             
     | 
| 82 | 
         
            +
             
     | 
| 83 | 
         
            +
            def json_truncate_arrays(data, lim=20):
         
     | 
| 84 | 
         
            +
                if isinstance(data, list):
         
     | 
| 85 | 
         
            +
                    if data and isinstance(data[0], dict):
         
     | 
| 86 | 
         
            +
                        return [json_truncate_arrays(d) for d in data]
         
     | 
| 87 | 
         
            +
                    if len(data) > lim:
         
     | 
| 88 | 
         
            +
                        data = [f"{len(data)} x {type(data[0]).__name__}"]
         
     | 
| 89 | 
         
            +
                    return data
         
     | 
| 90 | 
         
            +
                elif isinstance(data, dict):
         
     | 
| 91 | 
         
            +
                    return {k: json_truncate_arrays(v) for k, v in data.items()}
         
     | 
| 92 | 
         
            +
                else:
         
     | 
| 93 | 
         
            +
                    return data
         
     | 
| 94 | 
         
            +
             
     | 
| 95 | 
         
            +
             
     | 
| 96 | 
         
            +
            def weighted_selection_without_replacement(items: List[Tuple], pick_n: int) -> List[Tuple]:
         
     | 
| 97 | 
         
            +
                """For a list of tuples where the second element is a weight, returns random items with those weights, without replacement."""
         
     | 
| 98 | 
         
            +
                elt = [(math.log(random.random()) / (item[1] + 1e-18), item) for item in items]  # magic
         
     | 
| 99 | 
         
            +
                return [e[1] for e in heapq.nlargest(pick_n, elt)]  # NB fine if too small
         
     |