[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\nko_fi: cmdrdats\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n/classes\n/checkouts\npom.xml\npom.xml.asc\n*.jar\n*.class\n/.lein-*\n/.nrepl-port\n/.creds\ncapture\nresources/*.edn\n*.log\nnode_modules\nresources/samples\n/.idea\n/igoki.iml\n/ogs.edn\n/winbuild/"
  },
  {
    "path": "LICENSE",
    "content": "THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC\nLICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM\nCONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n1. DEFINITIONS\n\n\"Contribution\" means:\n\na) in the case of the initial Contributor, the initial code and\ndocumentation distributed under this Agreement, and\n\nb) in the case of each subsequent Contributor:\n\ni) changes to the Program, and\n\nii) additions to the Program;\n\nwhere such changes and/or additions to the Program originate from and are\ndistributed by that particular Contributor. A Contribution 'originates' from\na Contributor if it was added to the Program by such Contributor itself or\nanyone acting on such Contributor's behalf. Contributions do not include\nadditions to the Program which: (i) are separate modules of software\ndistributed in conjunction with the Program under their own license\nagreement, and (ii) are not derivative works of the Program.\n\n\"Contributor\" means any person or entity that distributes the Program.\n\n\"Licensed Patents\" mean patent claims licensable by a Contributor which are\nnecessarily infringed by the use or sale of its Contribution alone or when\ncombined with the Program.\n\n\"Program\" means the Contributions distributed in accordance with this\nAgreement.\n\n\"Recipient\" means anyone who receives the Program under this Agreement,\nincluding all Contributors.\n\n2. GRANT OF RIGHTS\n\na) Subject to the terms of this Agreement, each Contributor hereby grants\nRecipient a non-exclusive, worldwide, royalty-free copyright license to\nreproduce, prepare derivative works of, publicly display, publicly perform,\ndistribute and sublicense the Contribution of such Contributor, if any, and\nsuch derivative works, in source code and object code form.\n\nb) Subject to the terms of this Agreement, each Contributor hereby grants\nRecipient a non-exclusive, worldwide, royalty-free patent license under\nLicensed Patents to make, use, sell, offer to sell, import and otherwise\ntransfer the Contribution of such Contributor, if any, in source code and\nobject code form.  This patent license shall apply to the combination of the\nContribution and the Program if, at the time the Contribution is added by the\nContributor, such addition of the Contribution causes such combination to be\ncovered by the Licensed Patents. The patent license shall not apply to any\nother combinations which include the Contribution. No hardware per se is\nlicensed hereunder.\n\nc) Recipient understands that although each Contributor grants the licenses\nto its Contributions set forth herein, no assurances are provided by any\nContributor that the Program does not infringe the patent or other\nintellectual property rights of any other entity. Each Contributor disclaims\nany liability to Recipient for claims brought by any other entity based on\ninfringement of intellectual property rights or otherwise. As a condition to\nexercising the rights and licenses granted hereunder, each Recipient hereby\nassumes sole responsibility to secure any other intellectual property rights\nneeded, if any. For example, if a third party patent license is required to\nallow Recipient to distribute the Program, it is Recipient's responsibility\nto acquire that license before distributing the Program.\n\nd) Each Contributor represents that to its knowledge it has sufficient\ncopyright rights in its Contribution, if any, to grant the copyright license\nset forth in this Agreement.\n\n3. REQUIREMENTS\n\nA Contributor may choose to distribute the Program in object code form under\nits own license agreement, provided that:\n\na) it complies with the terms and conditions of this Agreement; and\n\nb) its license agreement:\n\ni) effectively disclaims on behalf of all Contributors all warranties and\nconditions, express and implied, including warranties or conditions of title\nand non-infringement, and implied warranties or conditions of merchantability\nand fitness for a particular purpose;\n\nii) effectively excludes on behalf of all Contributors all liability for\ndamages, including direct, indirect, special, incidental and consequential\ndamages, such as lost profits;\n\niii) states that any provisions which differ from this Agreement are offered\nby that Contributor alone and not by any other party; and\n\niv) states that source code for the Program is available from such\nContributor, and informs licensees how to obtain it in a reasonable manner on\nor through a medium customarily used for software exchange.\n\nWhen the Program is made available in source code form:\n\na) it must be made available under this Agreement; and\n\nb) a copy of this Agreement must be included with each copy of the Program.\n\nContributors may not remove or alter any copyright notices contained within\nthe Program.\n\nEach Contributor must identify itself as the originator of its Contribution,\nif any, in a manner that reasonably allows subsequent Recipients to identify\nthe originator of the Contribution.\n\n4. COMMERCIAL DISTRIBUTION\n\nCommercial distributors of software may accept certain responsibilities with\nrespect to end users, business partners and the like. While this license is\nintended to facilitate the commercial use of the Program, the Contributor who\nincludes the Program in a commercial product offering should do so in a\nmanner which does not create potential liability for other Contributors.\nTherefore, if a Contributor includes the Program in a commercial product\noffering, such Contributor (\"Commercial Contributor\") hereby agrees to defend\nand indemnify every other Contributor (\"Indemnified Contributor\") against any\nlosses, damages and costs (collectively \"Losses\") arising from claims,\nlawsuits and other legal actions brought by a third party against the\nIndemnified Contributor to the extent caused by the acts or omissions of such\nCommercial Contributor in connection with its distribution of the Program in\na commercial product offering.  The obligations in this section do not apply\nto any claims or Losses relating to any actual or alleged intellectual\nproperty infringement. In order to qualify, an Indemnified Contributor must:\na) promptly notify the Commercial Contributor in writing of such claim, and\nb) allow the Commercial Contributor tocontrol, and cooperate with the\nCommercial Contributor in, the defense and any related settlement\nnegotiations. The Indemnified Contributor may participate in any such claim\nat its own expense.\n\nFor example, a Contributor might include the Program in a commercial product\noffering, Product X. That Contributor is then a Commercial Contributor. If\nthat Commercial Contributor then makes performance claims, or offers\nwarranties related to Product X, those performance claims and warranties are\nsuch Commercial Contributor's responsibility alone. Under this section, the\nCommercial Contributor would have to defend claims against the other\nContributors related to those performance claims and warranties, and if a\ncourt requires any other Contributor to pay any damages as a result, the\nCommercial Contributor must pay those damages.\n\n5. NO WARRANTY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON\nAN \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER\nEXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR\nCONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A\nPARTICULAR PURPOSE. Each Recipient is solely responsible for determining the\nappropriateness of using and distributing the Program and assumes all risks\nassociated with its exercise of rights under this Agreement , including but\nnot limited to the risks and costs of program errors, compliance with\napplicable laws, damage to or loss of data, programs or equipment, and\nunavailability or interruption of operations.\n\n6. DISCLAIMER OF LIABILITY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY\nCONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION\nLOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE\nEXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY\nOF SUCH DAMAGES.\n\n7. GENERAL\n\nIf any provision of this Agreement is invalid or unenforceable under\napplicable law, it shall not affect the validity or enforceability of the\nremainder of the terms of this Agreement, and without further action by the\nparties hereto, such provision shall be reformed to the minimum extent\nnecessary to make such provision valid and enforceable.\n\nIf Recipient institutes patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Program itself\n(excluding combinations of the Program with other software or hardware)\ninfringes such Recipient's patent(s), then such Recipient's rights granted\nunder Section 2(b) shall terminate as of the date such litigation is filed.\n\nAll Recipient's rights under this Agreement shall terminate if it fails to\ncomply with any of the material terms or conditions of this Agreement and\ndoes not cure such failure in a reasonable period of time after becoming\naware of such noncompliance. If all Recipient's rights under this Agreement\nterminate, Recipient agrees to cease use and distribution of the Program as\nsoon as reasonably practicable. However, Recipient's obligations under this\nAgreement and any licenses granted by Recipient relating to the Program shall\ncontinue and survive.\n\nEveryone is permitted to copy and distribute copies of this Agreement, but in\norder to avoid inconsistency the Agreement is copyrighted and may only be\nmodified in the following manner. The Agreement Steward reserves the right to\npublish new versions (including revisions) of this Agreement from time to\ntime. No one other than the Agreement Steward has the right to modify this\nAgreement. The Eclipse Foundation is the initial Agreement Steward. The\nEclipse Foundation may assign the responsibility to serve as the Agreement\nSteward to a suitable separate entity. Each new version of the Agreement will\nbe given a distinguishing version number. The Program (including\nContributions) may always be distributed subject to the version of the\nAgreement under which it was received. In addition, after a new version of\nthe Agreement is published, Contributor may elect to distribute the Program\n(including its Contributions) under the new version. Except as expressly\nstated in Sections 2(a) and 2(b) above, Recipient receives no rights or\nlicenses to the intellectual property of any Contributor under this\nAgreement, whether expressly, by implication, estoppel or otherwise. All\nrights in the Program not expressly granted under this Agreement are\nreserved.\n\nThis Agreement is governed by the laws of the State of New York and the\nintellectual property laws of the United States of America. No party to this\nAgreement will bring a legal action under this Agreement more than one year\nafter the cause of action arose. Each party waives its rights to a jury trial\nin any resulting litigation.\n"
  },
  {
    "path": "README.md",
    "content": "# igoki\n\nBridge the gap between playing Go on a physical board and digitally.\n\nReasons for wanting to play on a physical board vary from person to person, for me:\n - I really love the tactile feel of the stones and board and the aesthetic of the game as the stones jostle around.\n - Prefer the serenity of not having to stare at a screen for the game.\n\nigoki lets you do the above while still having the ability to connect and play with people across the internet!\n\nSome things you can do with igoki:\n - Play online (currently online-go.com is implemented, would like to implement more backends)\n - Record a live game between two players\n - Review SGF's\n\n# Support\n\nIf you like this project, I would love if you want to jump in and help out with the codebase most of all - \nigoki started out as a proof of concept for me and then gradually grew into what it is today. That means there's\nplenty of cobwebs and weird design issues that stem from the lack of coherent upfront design :)\n\nTake a look at the project board here: https://github.com/CmdrDats/igoki/projects/1 - I actively use that to\nmanage what to focus my efforts on next.\n\nHowever, if you can't contribute to the codebase, please consider supporting me with a ko-fi donation:\n\n[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/D1D76ML2E)\n\nEvery little bit will help this project along immensely!\n \n# Setup\n\n## Hardware \nIt may seem daunting, but the minimum equipment you _really_ need is a webcam and a way to point it with sufficient angle at\na Go board. You can get creative here, even a laptop webcam will do the trick, albeit not the most comfortable.\n\nHowever, if you want to get the best experience out of the app, you'll want:\n\n - A PC/Laptop with **Windows**, **Linux** or **OSX** operating systems should all work (please report your mileage)\n - A decent **HD webcam** - really, search for 'webcam hd' on Amazon or wherever - it should do the trick.\n - A **portable projector** of some kind - optional, but really neato - search 'portable projector' - make sure it works \n   with your PC - a standard camera mount screw support will make it easier to attach to tripod.\n   I have a rubbish 320x240 low brightness projector, it's a bit a pain, but it works ok.\n - A **tripod** of some description - to mount your webcam and projector. It can be el-cheapo.\n - **USB extension** - usually webcam cables aren't that long, and I find I need an extension to be\n   able to put the camera where I want it.\n\nAnd that's really it, you're good to go.\n\n## Software\n\nHead on over to the [Releases](https://github.com/CmdrDats/igoki/releases) for a universal\njava binary that should run most anywhere-ish. You will need a Java runtime installed in order to run it though:\n\n- For Windows (64bit only), the 0.7 release has a wrapper executable + Java runtime bundled - download the `igoki-windows.zip` **and** the\n`igoki.jar` files - extract the `zip` and place the `jar` file in the same folder, then run the `igoki.exe`\n\n- I have tested with both Java 1.8 and Java 17 - you can head on over to [jdk.java.net](https://jdk.java.net/17/) to \n  download or [java.com](java.com) for java 1.8, though I'll be working off latest Java SDK.\n- Once Java is installed, you should be able to double-click the `igoki.jar` and it should run.\n- Failing that, on windows you can try:\n    - win-r, type `cmd`, hit enter\n    - type `cd Downloads` and hit enter (unless you put it somewhere else than in downloads)\n    - type `java -jar igoki.jar` and hit enter.\n    - It should start. if not, you should get some kind of error. Ping me with a new issue on github\n      with that message if that is the case.\n- On linux/osx, if you can `java -version` and it gives you the Java version, then you can just:\n    - `cd` to the installation folder and `java -jar igoki.jar` \n\n## Process\n\nTo get going, you'll go through the following steps:\n\n1. Setup camera, select the board corners and check that it's reading alright.\n2. For Online-Go: Setup API keys, then username/password to login\n3. For Manual integration: Setup frame, enter details, start recording\n4. For Game Review: Load SGF file\n5. For Game recording... just play and Save SGF when you're done!\n\n---\n\nWhen you start the app, you'll be greeted with:\n\n![Initial Screen](doc/images/screen1.png)\n\nGoing through the various panels:\n\n - **Camera setup screen** (Top left)\n    - Here you will select your board size, camera source \n    - You're also able to open the projector window (more on that later, in the 'Project Setup' section)\n - **Game state screen** (Top right)\n    - Here you will see an overview of the current game state and you'll see highlights where igoki\n      thinks the stones are, or should be placed.\n    - The `Debug ZIP` toggle, when selected, will dump zip files to the `capture` subfolder, which I can use to debug some specific bugs you may encounter\n    - The `<` and `>` lets you move forward or backward in the game, `pass` lets you record a pass.\n    - `Show Branches` will show you next moves if you aren't at the end of the game - useful for reviewing\n    - Announce lets you setup a voice to announce play coordinates. Useful if you don't have a projector, or just want that\n      tournament-ish feel where your every recorded move is spoke aloud.\n - **OGS tab** (Bottom left)\n    - Online-go integration - we'll cover these in a bit.\n - **Simulation tab** (Bottom left)\n    - You can specify a simulation camera in the camera dropdown, and then this will let you\n      pretend to have a real camera board. This is useful for when I do dev on various features\n      so that I don't have to set everything up.\n    - I don't imagine it's particularly useful for anything else, so you can ignore.\n - **Tree tab** (Bottom right\n    - Just a tree showing the current game with branches. Nothing fancy.\n\n### Camera calibration\n\nSelect a camera in the dropdown until you see your camera feed where you see your board.\n\nThen, point your `finger` to the upper left corner of the board. Click on that corner on the camera\nview and then the other 3 corners in a clockwise fashion. I find that provides the most sensible results.\n\nYou can click and drag the corners to fine tune them - sometimes you may want to make the view larger\nso that you can be a bit more accurate here.\n\n![Camera Grid Image](doc/images/camera.png)\n\nYou should see a grid pretty much exactly over your board lines.\n\nPut a bunch of stones down on the intersections around the board and adjust a little so that the\nintersections roughly lies on the center on _top_ of the stones. I find that gives a super solid reading result.\n\n![Camera Grid Image with Stones](doc/images/camera_stones.png)\n\n### Record your game\n\nIf you wanted to simply record your game, that's it, go to `File` -> `New SGF` in case it recorded\nsome stuff during caliration, and you're all ready to go. `File` -> `Save SGF...` when you're done\n\nIf you want to announce your moves while you're playing, set those up in the game panel (upper right)\n\n\n### Projector setup (optional)\n\n![Projector setup](doc/images/projector.jpg)\n\nConnect up your projector and get it working in your OS as an extended display - \nsome guidance if you need, for windows or OSX: [At this link](https://www.bu.edu/comtech/faculty-staff/classroom-av/instructor-station-desktop-mirroring/)\n\nOnce that is setup, make sure the projector is pointed at and covering your entire board with a bit\nof space to spare.\n\nNow click the `Projector` tab (next to OGS) - you'll see a 'Projector Window button'. Click that,\nand it should open a new blank window - move that to your projector monitor and maximize it. \n\nThen, click the 'Calibration Grid' button and you'll have something like this showing:\n\n![Projector Checkerboard](doc/images/projector-checker.png)\n\nthe checkerboard should ideally fit on your game board and be in the camera frame\n\nGet a blank white sheet of paper and place it so that the checkerboard pattern is fully on the page - \nthis will let the camera correctly see the checkerboard pattern easiest.\n\nYou should see the corners of the checkerboard highlighted in many colours in the camera window - if it looks like it's aligned correctly, hit the 'Accept Calibration' button.\n\nThe window should go black and it's all ready to roll!\n\n### Announcer\n\n![Announcer options](doc/images/announcer.png)\n\nNot a hardware setup, but if you want to hear coordinates,\nremember to select the color you want to announce for and the language. Particularly useful if no\nprojector.\n\n### Review a game\n\nAt this point, you can very effectively review games.\n\nIf you want to review a game, you can `File` -> `Load SGF...` to open the SGF you'd like to review.\nClick `Show Branches` for the best experience here.\n\n**A feature to note here:** when you pull stones off the board, igoki will do a backward search for\na previous game state like the one you've gone to, and automatically jump to that point in the game\nif it can. If you play a different way, it'll start branching in the SGF. Quite handy for reviewing and\nrecording variations, I would say!\n\n### Play online on online-go.com\n\n![OGS](doc/images/ogs.png)\n\nFor this you'll need to setup your igoki instance on OGS's API thingy. Click the 'Open Browser' or browse\nto [OGS Applications](https://online-go.com/oauth2/applications/) to set this up. I have tried to \nkeep the interface consistent with OGS, so the settings will be the same (though I've seen 'Client Type' as 'Secret'\nand sometimes 'Confidential' - I'm not sure - both seem the same)\n\nOnce you have your application setup and you have a Client ID and Client Secret, plug those in, along\nwith your normal Username/Password - igoki will remember all the settings, besides password unless you check\nthat option.\n\nIf that's successful, you'll see the game list:\n\n![OGS game list](doc/images/ogs-gamelist.png)\n\nJust click on one and `Connect to selected` - and you should be good to go - Start playing!\n\n### Play online using manual screen capture (very early implementation)\n\nAs a stopgap to interact with most other Go clients or programs, you can setup a manual frame\nto capture and relay mouse clicks directly on screen.\n\nTo set this up, click on the `Manual` tab at the bottom of the screen, then click the\n`Open capture frame` button. This will create a floating window that you can drag and align with the game\nboard you want to integrate with.\n\nBecause igoki won't be able to tell move order or anything, you need to let it know who the next\nplayer is that needs to go, and who it will be simulating mouse clicks for. There is a few\nmore options mostly for the saved SGF at the end:\n\n![Manual integration](doc/images/manual.png)\n\nYou can pause the capturing or stop it at any time.\n\nA few notes and tips:\n\n[Sabaki](https://sabaki.yichuanshen.de/) highlights the last move quite strongly, the strong black mark\non the white stone confuses igoki's neural net. I found I get better mileage when I make the sabaki window\nquite small if that is a problem.\n\n[Katrain](https://github.com/sanderland/katrain) on windows really wants to have focus before the move\nis made, this makes testing with the simulation mode somewhat tricky (it doesn't work unless I jimmy a double click of sorts xD)\n- You might find this is the case with different platforms - so focus the app after setup before you start playing.\n\nOn jdk 8, there's a bug where the first 'mouse move' command doesn't go to remotely the right place,\nso I've just worked around it by telling it to move the mouse 5 times. hopefully that's enough to coerce it, but please\nreport back.\n\n## Usage\n \n Head on over to the [Releases](https://github.com/CmdrDats/igoki/releases) for a universal\n java binary. \n\n This project is written in clojure so if you want to build from scratch, you need to install\n [Leiningen](http://leiningen.org) \n \n Once Leiningen is installed, clone this repo and run `lein run`, \n it will start up the frame and guide you through calibration.\n \n Alternatively, if you are doing development on this project, fire up a `lein repl` and it'll\n be all setup to connect an IDE repl to it.\n\nuse Launch4j to build the exe, if you want that.\n\n \n## License\n\nCopyright © 2021 Deon Moolman\n\nDistributed under the Eclipse Public License either version 1.0 or (at\nyour option) any later version.\n"
  },
  {
    "path": "doc/intro.md",
    "content": "# Introduction to badukpro\n\nTODO: write [great documentation](http://jacobian.org/writing/what-to-write/)\n"
  },
  {
    "path": "launch4j.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<launch4jConfig>\n  <dontWrapJar>true</dontWrapJar>\n  <headerType>console</headerType>\n  <jar>igoki.jar</jar>\n  <outfile>C:\\Users\\Deon\\IdeaProjects\\igoki\\winbuild\\igoki.exe</outfile>\n  <errTitle></errTitle>\n  <cmdLine></cmdLine>\n  <chdir>.</chdir>\n  <priority>normal</priority>\n  <downloadUrl>http://java.com/download</downloadUrl>\n  <supportUrl></supportUrl>\n  <stayAlive>false</stayAlive>\n  <restartOnCrash>false</restartOnCrash>\n  <manifest></manifest>\n  <icon>C:\\Users\\Deon\\IdeaProjects\\igoki\\raw\\igoki48.ico</icon>\n  <jre>\n    <path>jdk-17.0.1</path>\n    <bundledJre64Bit>true</bundledJre64Bit>\n    <bundledJreAsFallback>false</bundledJreAsFallback>\n    <minVersion>1.8</minVersion>\n    <maxVersion></maxVersion>\n    <jdkPreference>preferJdk</jdkPreference>\n    <runtimeBits>64/32</runtimeBits>\n  </jre>\n</launch4jConfig>"
  },
  {
    "path": "project.clj",
    "content": "(defproject igoki \"0.8.0\"\n  :description \"Igoki, physical Go board/OGS interface\"\n  :url \"http://github.com/CmdrDats/igoki\"\n  :license\n  {:name \"Eclipse Public License\"\n   :url \"http://www.eclipse.org/legal/epl-v10.html\"}\n\n  :dependencies\n  [[org.clojure/clojure \"1.10.3\"]\n   #_[com.google.guava/guava \"20.0\"]\n   [clj-http \"3.12.3\"]\n   [seesaw \"1.5.0\"]\n   [org.openpnp/opencv \"4.5.1-2\"]\n   [cheshire \"5.10.1\"]\n   [de.schlichtherle.truezip/truezip-file \"7.7.10\"]\n   [de.schlichtherle.truezip/truezip-driver-zip \"7.7.10\"]\n\n   [io.socket/socket.io-client \"0.9.0\"]\n\n   [org.clojure/tools.logging \"1.1.0\"]\n\n   [log4j \"1.2.17\"]\n   [org.slf4j/slf4j-api \"1.7.32\"]\n   [org.slf4j/jul-to-slf4j \"1.7.32\"]\n   [org.slf4j/slf4j-log4j12 \"1.7.32\"]\n\n   [org.nd4j/nd4j \"1.0.0-M1.1\" :extension \"pom\"]\n   [org.nd4j/nd4j-native-platform \"1.0.0-M1.1\"]\n   [org.deeplearning4j/deeplearning4j-core \"1.0.0-M1.1\"]]\n\n  :main igoki.core\n\n  :repl-options\n  {:welcome \"Welcome to igoki\"\n   :init-ns igoki.core\n   :init (-main)}\n\n  #_#_:min-lein-version \"2.5.0\"\n\n  :uberjar-name \"igoki.jar\")\n"
  },
  {
    "path": "raw/sgf/147.sgf",
    "content": "(;\nGM[1]\nSZ[19]\nAW[dd][ci][dk][dl][dn][dp][gp][ip][lp]\nAB[fq][fp][fo][fm][ek][ej][in][pq][pd]\nLB[ln:A][ir:D][np:B][po:C]\nC[Problem 147. Black to play.  \n\nThe white stones at the bottom are weak, so Black must find a good way to attack them. Note that Black gave White territory on the left in order to get a thick  position in the center. ]\n(;B[ln]\nTE[2]\nC[Correct Answer.  \n\nBlack should play a large-scale move by capping at 1. ]\n(;W[dr]\nLB[gq:A]\nC[White 2 is a good move because it prepares for White A, which threatens to link up or help her group at the center bottom to make eyes. <= ]\n)\n(;W[nq]\nC[If White 2 here, ... ]\n;B[po]\nC[... Black 3 has a good feel to it. <= ]\n))\n(;B[np]\nC[Failure.  \n\nBlack 1 is very bad because ... ]\n;W[ln]\nC[... it drives White into the center, compromising Black's thickness there. <= ]\n)\n)\n"
  },
  {
    "path": "raw/sgf/1988-06-20.sgf",
    "content": "(;SO[My Friday Night Files]SZ[19]PW[Liu Xiaoguang]WR[9d]PB[Cho Chikun]BR[9d]EV[1st Tengen/Tianyuan Match]RO[Game 1]DT[1988-06-20]PC[Tokyo]KM[5.5]RE[B+R]US[JBvR];B[qd];W[cq];B[pq];W[dd]\n;B[oc];W[po];B[qo];W[qn];B[qp];W[pm];B[nq];W[qi];B[qg];W[jc];B[cf];W[fd];B[bd]\n;W[cc];B[ci];W[ck];B[ql];W[rm];B[pj];W[qj];B[qk];W[rk];B[rl];W[sl];B[rj];W[sk]\n;B[pi];W[ri];B[qh];W[rh];B[rg];W[fp];B[lc];W[cg];B[bg];W[dg];B[ch];W[bf];B[be]\n;W[df];B[af];W[kd];B[ld];W[ke];B[ei];W[ek];B[le];W[kf];B[cm];W[bj];B[co];W[bc]\n;B[gi];W[fj];B[fi];W[hj];B[em];W[dn];B[do];W[eq];B[fo];W[dl];B[dm];W[dh];B[di]\n;W[go];B[gn];W[ik];B[hi];W[ii];B[ih];W[ji];B[jh];W[kh];B[ki];W[kj];B[li];W[lj]\n;B[kg];W[lh];B[mi];W[lg];B[hg];W[jg];B[he];W[ig];B[hh];W[ge];B[bi];W[mj];B[ho]\n;W[gp];B[ni];W[pk];B[hl];W[gj];B[pl];W[ok];B[ol];W[nj];B[om];W[on];B[mm];W[ro]\n;B[sh];W[sj];B[sg];W[si];B[rp];W[sp];B[sq];W[ph];B[oi];W[ml];B[ll];W[lm];B[jj]\n;W[ij];B[mk];W[kl];B[nl];W[ln];B[mn];W[ce];B[bl];W[oh];B[nh];W[pf];B[ng];W[bk]\n;B[bf];W[ad];B[ah];W[op];B[so];W[rn];B[km];W[jl];B[lo];W[pd];B[pg];W[of];B[og]\n;W[kn];B[no];W[oq];B[or];W[pr];B[pp];W[oo];B[nr];W[qr];B[rr];W[rs];B[ps];W[sr]\n;B[ss];W[pc];B[qs];W[mf];B[od];W[ob];B[nb];W[pb];B[oe];W[qe];B[nf];W[nc];B[mc]\n;W[pe];B[me];W[ko];B[rd];W[lp];B[mo];W[fn];B[gm];W[eo];B[en];W[bp];B[im];W[io]\n;B[bo];W[re];B[se];W[sc];B[rc];W[rb];B[sb];W[sa];B[hp];W[hq];B[ip];W[iq];B[jp]\n;W[kp];B[jq];W[lk];B[ml];W[jr];B[jo];W[qc];B[jm];W[kq];B[lr];W[kr];B[jk]C[This is the record in the Kido yearbook. There exists another game record with 4 more moves.])"
  },
  {
    "path": "resources/log4j.properties",
    "content": "# Active appenders and base log level\nlog4j.rootLogger=ERROR, console\n\n# Console appender\nlog4j.appender.console=org.apache.log4j.ConsoleAppender\nlog4j.appender.console.layout=org.apache.log4j.PatternLayout\nlog4j.appender.console.layout.ConversionPattern=%d{DATE} %-5p %c - %m%n\n\n# Log Levels\nlog4j.global=FINEST\nlog4j.logger.io=FINEST\nlog4j.logger.igoki=TRACE\n\n# 3rd party logging\nlog4j.logger.ring=ERROR\n"
  },
  {
    "path": "resources/logging.properties",
    "content": " handers = org.slf4j.bridge.SLF4JBridgeHandler\n"
  },
  {
    "path": "resources/mycertfile.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIHvDCCBqSgAwIBAgIHB5t1bc/BmTANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE\nBhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE\naWdpdGFsIENlcnRpZmljYXRlIFNpZ25pbmcxODA2BgNVBAMTL1N0YXJ0Q29tIENs\nYXNzIDIgUHJpbWFyeSBJbnRlcm1lZGlhdGUgU2VydmVyIENBMB4XDTE1MDcwNTAz\nNDk1MFoXDTE3MDcwNDIxMDEzN1owgZAxCzAJBgNVBAYTAlVTMRcwFQYDVQQIEw5O\nb3J0aCBDYXJvbGluYTEPMA0GA1UEBxMGRHVyaGFtMRMwEQYDVQQKEwpBa2l0YSBO\nb2VrMRowGAYDVQQDExF3d3cub25saW5lLWdvLmNvbTEmMCQGCSqGSIb3DQEJARYX\nd2VibWFzdGVyQG9ubGluZS1nby5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQCqm4qoq4U9CjchLV6D16aN7xDN4SaODHfsfOPCYHynyXTEp79r8/iA\nmwIqY7uBv8fjKDl8D5AtEOQowaUYQEyvD/TbxY1KKWfq2EYNCslK49/FySbzCv1Y\nVRVhTrQI3qMrstSfse1oprIp76Mw1NvphQfHz4udCLSshLqcMdmKwJQ3KknkdQ01\nBf3M/VyoudvomP+gZxvBbR8buaKFczn0nSA75xcb7y694v8JmIYhG0RM9Axgsc6O\nTj+QxeVZ7DzbaSMs8fQ2VwMcUnzdUje5V3D255IzhUzbNV/gvYgBKYVUKltGnFjz\ndiRimCBxd44pzik5cRUDQqbMplHT6uWtAgMBAAGjggQbMIIEFzAJBgNVHRMEAjAA\nMAsGA1UdDwQEAwIDqDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwHQYD\nVR0OBBYEFBgjlsMpQwe38n6TuZs6s/y4tCx2MB8GA1UdIwQYMBaAFBHbI0X9VMxq\ncW+EigPXvvcBLyaGMIIBVQYDVR0RBIIBTDCCAUiCEXd3dy5vbmxpbmUtZ28uY29t\ngg1vbmxpbmUtZ28uY29tghFnZ3Mub25saW5lLWdvLmNvbYISYmV0YS5vbmxpbmUt\nZ28uY29tghRmb3J1bXMub25saW5lLWdvLmNvbYIVZ2dzYmV0YS5vbmxpbmUtZ28u\nY29tghZ3d3cuYmV0YS5vbmxpbmUtZ28uY29tgg9zLm9ubGluZS1nby5jb22CEnR1\ncm4ub25saW5lLWdvLmNvbYISc3R1bi5vbmxpbmUtZ28uY29tghR3ZWJydGMub25s\naW5lLWdvLmNvbYIXdHJhbnNsYXRlLm9ubGluZS1nby5jb22CEWFwaS5vbmxpbmUt\nZ28uY29tghJhcGkxLm9ubGluZS1nby5jb22CEmFwaTIub25saW5lLWdvLmNvbYIV\na3VyZW50by5vbmxpbmUtZ28uY29tMIIBVgYDVR0gBIIBTTCCAUkwCAYGZ4EMAQIC\nMIIBOwYLKwYBBAGBtTcBAgMwggEqMC4GCCsGAQUFBwIBFiJodHRwOi8vd3d3LnN0\nYXJ0c3NsLmNvbS9wb2xpY3kucGRmMIH3BggrBgEFBQcCAjCB6jAnFiBTdGFydENv\nbSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTADAgEBGoG+VGhpcyBjZXJ0aWZpY2F0\nZSB3YXMgaXNzdWVkIGFjY29yZGluZyB0byB0aGUgQ2xhc3MgMiBWYWxpZGF0aW9u\nIHJlcXVpcmVtZW50cyBvZiB0aGUgU3RhcnRDb20gQ0EgcG9saWN5LCByZWxpYW5j\nZSBvbmx5IGZvciB0aGUgaW50ZW5kZWQgcHVycG9zZSBpbiBjb21wbGlhbmNlIG9m\nIHRoZSByZWx5aW5nIHBhcnR5IG9ibGlnYXRpb25zLjA1BgNVHR8ELjAsMCqgKKAm\nhiRodHRwOi8vY3JsLnN0YXJ0c3NsLmNvbS9jcnQyLWNybC5jcmwwgY4GCCsGAQUF\nBwEBBIGBMH8wOQYIKwYBBQUHMAGGLWh0dHA6Ly9vY3NwLnN0YXJ0c3NsLmNvbS9z\ndWIvY2xhc3MyL3NlcnZlci9jYTBCBggrBgEFBQcwAoY2aHR0cDovL2FpYS5zdGFy\ndHNzbC5jb20vY2VydHMvc3ViLmNsYXNzMi5zZXJ2ZXIuY2EuY3J0MCMGA1UdEgQc\nMBqGGGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tLzANBgkqhkiG9w0BAQsFAAOCAQEA\nFl+7hqCBCUbtZKcdSdJTdCwNtrmCsZ+TOWhmP1ZzkOf+HzizAG9BSsvBe+lw9gZL\ndoMTTVxihUiVFXWp1CR7G6DQo1gHRCH/N2c6WWUrE/n4DJmnLeV3wMBxEaSw5l46\nv0VIpcoYiqDuvjyND4HOXMyIgzRlS2GpFSolFwCs+JebRVqrDVjbQkrg/+o2Dwl7\nZtPUs8JcgZAHeKqi3TcZ+OxI4MuaVCEXwrKRA70/8WeJ8PDSl2+BnZTO6FYTXClm\nbjzF8sKEJbL0GN2NFIMlOhsG9yueiGQXzMdtESXrONifbEeEZFeHP8ZuZl98/HrK\nkcn5yT6QYm1ACSIRCllvBA==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "src/igoki/camera.clj",
    "content": "(ns igoki.camera\n  (:require\n    [igoki.util :as util]\n    [igoki.simulated :as sim]\n    [clojure.java.io :as io])\n  (:import\n    (org.opencv.core MatOfPoint2f Mat Rect Size Core)\n    (org.opencv.videoio Videoio VideoCapture)\n    (org.opencv.imgcodecs Imgcodecs)\n    (java.util LinkedList UUID)\n    (java.io File)\n    (org.opencv.imgproc Imgproc)\n    (org.datavec.image.loader ImageLoader)\n    (org.deeplearning4j.util ModelSerializer)\n    (org.opencv.calib3d Calib3d)\n    (org.nd4j.linalg.api.ndarray INDArray)\n    (org.deeplearning4j.nn.multilayer MultiLayerNetwork)\n    (java.awt.image BufferedImage)))\n\n;; Step 1. Setup camera\n(defn setup-camera [ctx camidx]\n  (let [^VideoCapture video (VideoCapture. ^int camidx Videoio/CAP_ANY)]\n    (swap! ctx update\n      :camera assoc\n      :video video\n      :stopped false)\n    video))\n\n;; Step 2. Read camera\n\n(defn camera-read [ctx video]\n  (let [camera (:camera @ctx)]\n    (cond\n      (or (:stopped camera) (not video)) nil\n\n      (not (.isOpened video))\n      (println \"Error: Camera not opened\")\n\n      :else\n      (try\n        (let [frame (or (:frame camera) (Mat.))]\n          (when (.read video frame)\n\n            (swap!\n              ctx update :camera\n              #(assoc %\n                 :raw frame\n                 ;; TODO: this chows memory - better to have a hook on update for each specific\n                 ;; view - this will only be needed on the first screen.\n                 :pimg (util/mat-to-pimage frame (get-in % [:pimg :bufimg]))))))\n        (Thread/sleep (or (-> @ctx :camera :read-delay) 500))\n        (catch Exception e\n          (println \"exception thrown\")\n          (.printStackTrace e))))))\n\n;; Helpful for debugging\n(defn read-single [ctx camidx]\n  (let [video (VideoCapture. (int camidx) Videoio/CAP_ANY)\n        frame (Mat.)]\n    (Thread/sleep 500)\n    (.read video frame)\n    (swap!\n      ctx\n      update :camera\n      #(assoc %\n         :raw frame\n         :pimg (util/mat-to-pimage frame (get-in % [:pimg :bufimg]))))\n    (.release video)))\n\n;; For simulation stepping (debugging)\n(defn read-file [ctx fname]\n  (let [frame (Imgcodecs/imread (str \"resources/\" fname))]\n    (swap!\n      ctx update :camera\n      #(assoc %\n         :raw frame\n         :pimg (util/mat-to-pimage frame (get-in % [:pimg :bufimg]))))))\n\n\n;; Step 3. Specify points & find homography\n(def block-size 10)\n\n(defn target-points [size]\n  (let [extent (* block-size size)]\n    [[block-size block-size] [extent block-size] [extent extent] [block-size extent]]))\n\n(defn update-homography [ctx]\n  (util/with-release\n    [target (MatOfPoint2f.)\n     origpoints (MatOfPoint2f.)]\n    (let [{{:keys [points size]} :goban :as context} ctx\n          target (util/vec->mat target (target-points size))\n          origpoints (util/vec->mat origpoints points)\n          homography\n          (Calib3d/findHomography ^MatOfPoint2f origpoints ^MatOfPoint2f target\n            Calib3d/FM_RANSAC 3.0)]\n      (if homography\n        (assoc-in ctx [:view :homography] homography)\n        ctx))))\n\n;; Step 4. Flatten image\n(defn sample-points [corners size]\n  (let [[ctl ctr cbr cbl] corners\n        divide\n        (fn [[x1 y1] [x2 y2]]\n          (let [xf (/ (- x2 x1) (dec size))\n                yf (/ (- y2 y1) (dec size))]\n            (map (fn [s] [(+ x1 (* s xf)) (+ y1 (* s yf))]) (range size))))\n        leftedge (divide ctl cbl)\n        rightedge (divide ctr cbr)]\n    (map\n      (fn [left right] (divide left right))\n      leftedge rightedge)))\n\n\n\n(defn gather-reference [context homography]\n  (let [{{:keys [size]} :goban} context\n        samplecorners (target-points size)\n        samplepoints (sample-points samplecorners size)]\n    {:homography homography\n     :shift [0 0]\n     :samplecorners samplecorners\n     :samplepoints samplepoints}))\n\n(defn update-reference [ctx]\n  (let [{{:keys [homography]} :view :as context} @ctx]\n    (when homography\n      (swap! ctx assoc :view (gather-reference context homography)))))\n\n(defn reverse-transform [ctx]\n  (cond\n    (<  (count (-> @ctx :goban :edges)) 4)\n    (swap! ctx\n      (fn [c]\n        (->\n          c\n          (update :view dissoc :homography)\n          (assoc-in [:goban :lines] []))))\n\n    :else\n    (do\n      (swap! ctx update-homography)\n      (let [context @ctx\n            homography (-> context :view :homography)\n            size (-> context :goban :size)\n            d (dec size)\n            [topleft topright bottomright bottomleft] (target-points size)]\n\n        (when homography\n          (util/with-release\n            [ref (MatOfPoint2f.)\n             pts (MatOfPoint2f.)]\n            (util/vec->mat\n              pts\n              (mapcat\n                (fn [t]\n                  [(util/point-along-line [topleft topright] (/ t (dec size)))\n                   (util/point-along-line [bottomleft bottomright] (/ t (dec size)))\n                   (util/point-along-line [topleft bottomleft] (/ t (dec size)))\n                   (util/point-along-line [topright bottomright] (/ t (dec size)))])\n                (range 0 size)))\n            (Core/perspectiveTransform pts ref (.inv (-> @ctx :view :homography)))\n\n            (swap! ctx assoc-in [:goban :lines] (partition 2 (util/mat->seq ref)))\n            (update-reference ctx)))))))\n\n;; Step 5. Read board state.\n(defn load-net [^File nm]\n  (ModelSerializer/restoreMultiLayerNetwork nm))\n\n(def net\n  (let [tmp (File/createTempFile \"igoki\" \"cnet\")\n        cnet (io/input-stream (io/resource \"supersimple.cnet\"))]\n    (io/copy cnet tmp)\n    (let [net (load-net tmp)]\n      (.delete tmp)\n      net)))\n\n(def loader (ImageLoader. 10 10 3))\n\n(defn ref-size-vec [size]\n  [(* block-size (inc size)) (* block-size (inc size))])\n\n(defn ref-size [size]\n  (Size. (* block-size (inc size)) (* block-size (inc size))))\n\n(defn eval-spot [img]\n  (let [^INDArray d (.asMatrix loader img)\n        _ (.divi d 255.0)\n        d (.reshape d (int-array [1 300]))\n        ^INDArray o (.output ^MultiLayerNetwork net d)]\n    (for [i (range 3)]\n      (try\n        (.getFloat o (int i))\n        (catch Exception e\n          (.printStackTrace e))))))\n\n(defn eval-net [flat px py]\n  (let [smat (.submat flat (Rect. (- px (/ block-size 2)) (- py (/ block-size 2)) 10 10))\n        img (util/mat-to-buffered-image smat nil)\n        result (eval-spot img)]\n    (.release smat)\n    result))\n\n(defn read-board [ctx]\n  (let [{{:keys [homography samplepoints]} :view\n         {:keys [raw flattened flattened-pimage]} :camera\n         {:keys [size]} :goban} ctx\n        new-flat (or flattened (Mat.))]\n\n    (cond\n      (not homography) ctx\n\n      :else\n      (do\n        (Imgproc/warpPerspective raw new-flat homography (ref-size size))\n        (let [brightness (/ (apply + (take 3 (seq (.val (Core/mean new-flat))))) 3.0)]\n          (.convertTo new-flat new-flat -1 1 (- 140 brightness))\n          (Core/normalize new-flat new-flat 0 255 Core/NORM_MINMAX))\n\n\n        (let [board\n              (mapv\n                (fn [row]\n                  (mapv\n                    (fn [[px py]]\n                      (let [[b e w] (eval-net new-flat px py)]\n                        (cond\n                          (> b 0.6) :b\n                          (> w 0.8) :w)))\n                    row))\n                samplepoints)]\n          (->\n            ctx\n            (assoc-in [:camera :flattened] new-flat)\n            (assoc-in [:camera :flattened-pimage]\n              (util/mat-to-pimage new-flat\n                (:bufimg flattened-pimage)))\n            (assoc :board board)))))))\n\n(defn read-stones [ctx]\n  (let [{:keys [view projector]} @ctx\n        {:keys [samplepoints homography]} view]\n    (when (and homography (not (:setting-up projector)))\n      ;; TODO: this multiple swap thing, no good.\n      (swap! ctx read-board)\n\n      (util/with-release [src (MatOfPoint2f.) dst (MatOfPoint2f.)]\n        (Core/perspectiveTransform\n          (util/vec->mat src\n            (apply concat samplepoints))\n          dst\n          (.inv homography))\n\n        (swap! ctx assoc-in [:goban :camerapoints] (util/mat->seq dst))))))\n\n\n;; Step 6. Repeat\n(defn read-loop [ctx camidx]\n  (when-not (-> @ctx :camera :stopped)\n    (let [video (setup-camera ctx camidx)]\n      (doto\n        (Thread.\n          ^Runnable\n          #(when-not (-> @ctx :camera :stopped)\n             (camera-read ctx video)\n             (recur)))\n        (.setDaemon true)\n        (.start)))))\n\n(defn stop-read-loop [ctx]\n  (if-let [video ^VideoCapture (-> @ctx :camera :video)]\n    (.release video))\n  (swap! ctx update :camera assoc :stopped true :video nil))\n\n(defn switch-read-loop [ctx camidx]\n  (stop-read-loop ctx)\n  (Thread/sleep (* 2 (or (-> @ctx :camera :read-delay) 1000)))\n  (swap! ctx assoc-in [:camera :stopped] false)\n  (read-loop ctx camidx))\n\n\n(defn camera-updated [wk ctx old new]\n  (try\n    (read-stones ctx)\n    (catch Exception e (.printStackTrace e))))\n\n(defn update-corners [ctx points]\n  (swap! ctx update :goban\n    (fn [goban]\n      (assoc\n        goban\n        :points points\n        :edges (util/update-edges points))))\n  (reverse-transform ctx))\n\n\n(defn reset-board [ctx]\n  (swap! ctx assoc :goban\n    {:points []\n     :size   19}))\n\n(defn start-calibration [ctx]\n  (when-not (:goban @ctx)\n    (reset-board ctx))\n  (util/add-watch-path ctx :goban-camera [:camera] #'camera-updated))\n\n(defn stop-calibration [ctx]\n  (remove-watch ctx :goban-camera))\n\n(defn camera-image [ctx]\n  (get-in @ctx [:camera :pimg :bufimg]))\n\n(defn set-board-size [ctx size]\n  (swap! ctx assoc-in [:goban :size] size)\n  (reverse-transform ctx))\n\n(defn cycle-size [ctx]\n  (swap! ctx update-in [:goban :size]\n    (fn [s]\n      (case s 19 9 9 13 19)))\n  (reverse-transform ctx))\n\n(defn camera-size [ctx]\n  (let [^BufferedImage c (camera-image ctx)]\n    (cond\n      (not c) nil\n      :else [(.getWidth c) (.getHeight c)])))\n\n(defn cycle-corners [ctx]\n  (update-corners ctx (vec (take 4 (drop 1 (cycle (-> @ctx :goban :points)))))))\n\n(defn select-camera [ctx camera-idx]\n  (sim/stop)\n  (stop-read-loop ctx)\n  (update-corners ctx [])\n  (case camera-idx\n    -2 nil\n    -1 (sim/start-simulation ctx)\n    (switch-read-loop ctx camera-idx)))\n\n\n\n\n;; Older testing code...\n\n;; This was a good idea, but need to retrain the network to recognize these, so not\n;; entirely sure it's worthwhile? Would have to measure.\n(defn illuminate-correct [m]\n  (util/with-release [lab-image (Mat.) equalized (Mat.)]\n    (let [planes (LinkedList.)]\n      (Imgproc/cvtColor m lab-image Imgproc/COLOR_BGR2Lab)\n      (Core/split lab-image planes)\n      (Imgproc/equalizeHist (first planes) equalized)\n      (.copyTo equalized (first planes))\n      (Core/merge planes lab-image)\n      (Imgproc/cvtColor lab-image m Imgproc/COLOR_Lab2BGR)\n      m)))\n\n\n;; This is likely used to prep training data?\n(defn dump-points [ctx]\n  (let [flat (-> ctx :camera :flattened)\n        samplepoints (-> ctx :view :samplepoints)\n        board (-> ctx :board)\n        id (first (.split (.toString (UUID/randomUUID)) \"[-]\"))]\n    (when flat\n      (doseq [[py rows] (map-indexed vector samplepoints)]\n        (doseq [[px [x y]] (map-indexed vector rows)]\n          (let [r (Rect. (- x (/ block-size 2)) (- y (/ block-size 2)) block-size block-size)\n                p (get-in board [py px])]\n            (Imgcodecs/imwrite (str \"samples/\" (if p (name p) \"e\") \"-\" px \"-\" py \"-\" id \".png\") (.submat ^Mat flat r)))\n          ))\n      samplepoints)))\n\n;; Again - older code, not sure what it was doing, think it was pre-neural net days\n;; Super naive implementation - needs work.\n(defn closest-samplepoint [samplepoints [x y :as p]]\n  (first\n    (reduce\n      (fn [[w l :as winningpoint] s]\n        (let [length (util/line-length-squared [p s])]\n          (cond\n            (or (nil? w) (< length l)) [s length]\n            :else winningpoint)))\n      nil\n      (mapcat identity samplepoints))))\n\n"
  },
  {
    "path": "src/igoki/core.clj",
    "content": "(ns igoki.core\n  (:require\n    [igoki.ui.main :as ui.main]\n    [igoki.camera :as camera]\n    [igoki.game :as game]\n    [clojure.java.io :as io]\n    [seesaw.core :as s])\n  (:gen-class)\n  (:import\n    (org.slf4j.bridge SLF4JBridgeHandler)\n    (nu.pattern OpenCV)\n    (java.util.logging LogManager Level)))\n\n(OpenCV/loadShared)\n(SLF4JBridgeHandler/install)\n(.setLevel (.getLogger (LogManager/getLogManager) \"\") Level/INFO)\n\n\n(defonce ctx (atom {}))\n(defn start []\n  ;; TODO: these are gross. refactor out these init steps.\n  ;; The spice should just flow.\n  (camera/start-calibration ctx)\n  (game/init ctx)\n\n  (ui.main/main-frame ctx))\n\n(defn -main [& args]\n  (start))\n"
  },
  {
    "path": "src/igoki/game.clj",
    "content": "(ns igoki.game\n  (:require\n    [igoki.util :as util]\n    [igoki.sgf :as sgf]\n    [igoki.inferrence :as inferrence]\n    [igoki.sound.sound :as snd]\n    [igoki.sound.announce :as announce])\n  (:import\n    (java.io File ByteArrayInputStream)\n    (java.util Date UUID)\n    (java.text SimpleDateFormat)\n    (org.opencv.core MatOfByte)\n    (de.schlichtherle.truezip.file TVFS)\n    (org.opencv.imgcodecs Imgcodecs)))\n\n;; ========================================================\n;; TODO: Display sibling branches\n;; TODO: Support swapping last move to a different point (traversing to the applicable branch)\n;; TODO: Display other annotations (circle, mark, selected, square, territory-black, territory-white)\n;; TODO: Cache last n moves for backtracking to prevent rebuilding it every time.\n;; TODO: Mainline variation\n;; TODO: 0 and 1 steps on SGF?\n;; This will immensely speed up the end game performance.\n;; ====================================================\n\n\n\n(defn board-diff [b1 b2]\n  (remove\n    nil?\n    (mapcat\n      (fn [[y b1row] b2row]\n        (map\n          (fn [[x b1i] b2i]\n            (if-not (= b1i b2i)\n              [x y b1i b2i]))\n          (map-indexed vector b1row) b2row))\n      (map-indexed vector b1) b2)))\n\n(defonce captured-boardlist (atom []))\n\n(defn submit-move\n  [ctx]\n  (let [board (:board @ctx)]\n    (println \"Change detected, debouncing\")\n    (swap!\n      ctx\n      (fn [c]\n        (-> c\n            (update :kifu assoc :submit {:latch 2 :board board})\n            ;; TODO: this read-delay.. whaat?? this should be a 'accept-delay' at this level.\n            (update :camera assoc :read-delay 300))))))\n\n(defn board-history [{:keys [current-branch-path movenumber moves] :as game}]\n  (->>\n    (range movenumber)\n    ;; This is a slow operation, so just checking the last few moves.\n    (take-last 20)\n    (map\n      (fn [m]\n        (let [g (inferrence/reconstruct (assoc game :movenumber m))]\n          [(:kifu-board g) g])))\n    (into {})))\n\n(defn board-updated [_ ctx _ board]\n  #_(println \"Board updated.\")\n  (swap! captured-boardlist conj board)\n  (let [{{:keys [kifu-board dirty] :as game} :kifu\n         ogs :ogs} @ctx\n\n        nodes (sgf/current-branch-node-list (:current-branch-path game) (:moves game))\n        lastmove (last nodes)\n        [[_ _ mo mn :as mv] :as diff] (board-diff kifu-board board)\n\n        ;; Disabled temporarily for issues with online integration.\n        ;; TODO: This causes issues for online integration, obviously, so will\n        ;; need to check if undo/move is all\n        history-game\n        (when-not (:gameid ogs)\n          (if (and lastmove (> (count diff) 0)) (get (board-history game) board)))]\n    (cond\n      (and (empty? diff) dirty)\n      (do\n        (println \"Clean state, marking as such.\")\n        (swap! ctx assoc-in [:kifu :dirty] false))\n\n      (and (not (empty? diff)) dirty)\n      (println \"Not actioning board updates until clean state is reached\")\n\n      ;; Special case to undo last move\n      history-game\n      (do\n        (snd/play-sound :undo)\n        (swap! ctx (fn [c] (assoc c :kifu history-game))))\n\n      :else\n      (submit-move ctx))))\n\n(defn dump-camera [filename camidx raw updatelist]\n  (when (and raw filename)\n    (util/with-release [out (MatOfByte.)]\n      (println \"Writing jpg: \" filename \"/\" (str camidx \".jpg\"))\n      (Imgcodecs/imencode \".jpg\" raw out)\n      (util/zip-add-file filename (str camidx \".jpg\") (ByteArrayInputStream. (.toArray out)))\n      (util/zip-add-file-string filename (str camidx \".edn\") (pr-str updatelist))\n      (println \"Done writing jpg: \" filename \"/\" (str camidx \".jpg\")))))\n\n\n(defn camera-updated [wk ctx old new]\n  (let [{{{:keys [latch board] :as submit} :submit\n          :keys [filename camidx last-dump] :as game} :kifu\n         {:keys [raw]} :camera\n         debug-capture :debug-capture\n         cboard :board} @ctx\n\n        updatelist @captured-boardlist\n\n        t (System/nanoTime)]\n    (cond\n      (nil? submit) nil\n\n      (not= cboard board)\n      (do\n        (println \"Debounce dirty - move discarded\")\n        (swap!\n          ctx\n          (fn [c]\n            (-> c\n                (update :kifu dissoc :submit)\n                (update :camera dissoc :read-delay)))))\n\n      (pos? latch)\n      (swap! ctx update-in [:kifu :submit :latch] dec)\n\n      :else\n      (do\n        ;; TODO: Sound playing shouldn't happen here, surely?\n        ;;(snd/play-sound :submit)\n        (println \"Debounce success - move submitted\")\n\n        (let [new (inferrence/infer-moves game updatelist (last updatelist))]\n          (if (and new (not= (:kifu-board new) (:kifu-board game)))\n            (do\n              (snd/play-sound :click)\n\n              (announce/comment-move ctx\n                (last\n                  (sgf/current-branch-node-list\n                    (take (:movenumber new) (:current-branch-path new)) (:moves new)))\n                (:constructed new))\n              (reset! captured-boardlist [])\n              (swap! ctx assoc :kifu (assoc (dissoc new :submit) :cam-update true)))\n            (swap! ctx update :kifu #(assoc (dissoc % :submit) :cam-update true))))\n        (swap! ctx update :camera dissoc :read-delay)))\n\n    ;; Dump camera on a regular basis, ignore time if board is updated.\n    #_(println last-dump t (- t last-dump) (> (- t last-dump 20e9)))\n    (when (or (get-in @ctx [:kifu :cam-update])\n            (nil? last-dump)\n            (> (- t last-dump) 20e9))\n      (when debug-capture\n        (dump-camera filename camidx raw updatelist))\n      (swap! ctx update :kifu\n        #(-> %\n             (assoc :cam-update false :last-dump t)\n             (update :camidx (fnil inc 0)))))))\n\n\n(defn add-initial-points [node board]\n  (let [initial\n        (for [[y row] (map-indexed vector board)\n              [x v] (map-indexed vector row)\n              :when v]\n          [v x y])\n        black (seq (filter (comp #(= :b %) first) initial))\n        white (seq (filter (comp #(= :w %) first) initial))]\n    (cond->\n      node\n      black (assoc :add-black (map (fn [[_ x y]] (sgf/convert-coord x y)) black))\n      white (assoc :add-white (map (fn [[_ x y]] (sgf/convert-coord x y)) white))\n      (> (count black) (count white)) (assoc :player-start [\"W\"]))))\n\n(defn reset-kifu [ctx]\n  (let [context @ctx\n        size\n        (or (-> context :goban :size) 19)\n        board (or (-> context :board) [])\n        camfile (or (-> context :kifu :filename) (str \"capture/\" (.toString (UUID/randomUUID)) \".zip\"))\n        camidx (or (-> context :kifu :camidx) 0)\n        new-game\n        (->\n          {:filename camfile\n           :camidx (inc camidx)\n           :moves\n           (add-initial-points\n             {:branches []\n              :player-start [\"B\"]\n              :application [(str \"igoki v\" (System/getProperty \"igoki.version\"))]\n              :file-format [\"4\"]\n              :gametype [\"1\"]\n              :size [size]\n              :date [(.format (SimpleDateFormat. \"YYYY-MM-dd\") (Date.))]\n              :komi [\"5.5\"]}\n             board)\n\n           :movenumber 0\n           :current-branch-path [[]]}\n          inferrence/reconstruct)]\n\n    (when-not (.exists (File. \"capture\"))\n      (.mkdir (File. \"capture\")))\n\n    (when (:debug-capture ctx)\n      (util/zip-add-file-string\n        (:filename new-game)\n        (str camidx \".config.edn\")\n        (pr-str\n          {:board (:board context)\n           :goban (:goban context)\n           :view (dissoc (:view context) :homography)\n           :kifu new-game}))\n      (dump-camera (:filename new-game) camidx (-> context :camera :raw) [board]))\n    (swap! ctx assoc :kifu new-game :filename \"game.sgf\")))\n\n\n;; In hundreds..\n(defn find-last-move [ctx]\n  (let [{{:keys [movenumber] :as game} :kifu} @ctx\n        visiblepath (if movenumber (take movenumber (mapcat identity (:current-branch-path game))))\n        actionlist (if visiblepath (sgf/current-branch-node-list [visiblepath] (:moves game)))]\n    (last actionlist)))\n\n(defn convert-sgf [ctx]\n  (sgf/sgf (:moves (:kifu @ctx))))\n\n(defn load-sgf [ctx file]\n  (let [sgf-string (slurp file)\n        moves (sgf/read-sgf sgf-string)]\n    (swap! ctx assoc\n      :kifu\n      (inferrence/reconstruct\n        {:moves moves :movenumber 0 :current-branch-path []})\n      :current-file file)))\n\n(defn toggle-branches [ctx show-branches?]\n  (swap! ctx assoc-in [:kifu :show-branches] show-branches?))\n\n(defn move-backward [ctx]\n  (swap! ctx\n    #(->\n       %\n       (update-in [:kifu :movenumber] (fnil (comp (partial max 0) dec) 1))\n       (assoc-in [:kifu :dirty] true)\n       (update-in [:kifu] inferrence/reconstruct))))\n\n(defn move-forward [ctx]\n  (let [{:keys [movenumber current-branch-path moves]} (:kifu @ctx)\n        path (vec (take movenumber (mapcat identity current-branch-path)))\n        {:keys [branches] :as node} (last (sgf/current-branch-node-list [path] moves))\n        new-branch-path (if (<= (count path) movenumber) [(conj path 0)] current-branch-path)]\n    (cond\n      (zero? (count branches))\n      ctx\n\n      :else\n      (swap! ctx update-in [:kifu]\n        #(->\n           %\n           (update :movenumber (fnil inc 1))\n           (assoc :dirty true :current-branch-path new-branch-path)\n           (inferrence/reconstruct))))))\n\n(defn pass [ctx]\n  (swap! ctx\n    (fn [{:keys [kifu] :as state}]\n      (assoc state\n        :kifu\n        (inferrence/play-move kifu\n          [-1 -1 nil\n           ({:white :w :black :b}\n            (-> kifu :constructed :player-turn))])))))\n\n\n;; TODO: Not happy with this\n;; The add-watch creates an implicit binding to data structure which makes the\n;; whole thing hard to reason about.\n(defn init [ctx]\n  (when-not (-> @ctx :kifu)\n    (TVFS/umount)\n    (reset-kifu ctx))\n  (util/add-watch-path ctx :kifu-camera [:camera :raw] #'camera-updated)\n  (util/add-watch-path ctx :kifu-board [:board] #'board-updated))"
  },
  {
    "path": "src/igoki/inferrence.clj",
    "content": "(ns igoki.inferrence\n  (:require\n    [igoki.util :as util]\n    [igoki.sgf :as sgf]))\n\n(defn simple-board-view [{:keys [board size]}]\n  (let [[width height] size]\n    (for [y (range height)]\n      (for [x (range width)]\n        (case (get-in board [(sgf/convert-coord x y) :stone])\n          :white :w :black :b nil)))))\n\n(defn reconstruct [{:keys [moves current-branch-path movenumber] :as game}]\n  (let [visiblepath (vec (take movenumber (mapcat identity current-branch-path)))\n        constructed (sgf/construct-board moves [visiblepath])]\n    (assoc game\n      :constructed constructed\n      :kifu-board (simple-board-view constructed))))\n\n(defn play-move [{:keys [moves current-branch-path movenumber] :as game} [x y o n :as move]]\n  (let [visiblepath (vec (take movenumber (mapcat identity current-branch-path)))\n        [node path] (sgf/collect-node moves {(if (= n :b) :black :white) [(sgf/convert-coord x y)]} [visiblepath])\n        updatedgame\n        (->\n          game\n          (assoc :moves node :dirty false :current-branch-path path)\n          (update :movenumber (fnil inc 0)))]\n    (reconstruct updatedgame)))\n\n(defn print-boards [& boards]\n  (println\n    (apply str\n      (interpose \"\\n\"\n        (apply map\n          #(apply str\n             (interpose \" | \"\n               (for [m %&]\n                 (apply str (map (fn [i] (str \" \" (if i (name i) \".\"))) m)))))\n          boards)))))\n\n(defn walk-boards [cellfn & boards]\n  (apply map\n    (fn [& rs]\n      (apply map cellfn rs))\n    boards))\n\n(defn subtract-board [a b]\n  (walk-boards\n    (fn [ca cb]\n      (if cb nil ca)) a b))\n\n(defn mask-board [a b]\n  (walk-boards\n    (fn [ca cb]\n      (if (and ca cb) ca)) a b))\n\n(defn point-map [board]\n  (for [[y rows] (map-indexed vector board)\n        [x cell] (map-indexed vector rows)\n        :when cell]\n    [x y cell]))\n\n(defn clean-updatelist\n  [initial updatelist final-position]\n  (->>\n    ;; We need to propagate the final-position backward\n    (reverse updatelist)\n\n    ;; Go through the board snapshot, ripping off any cells that were previously nil\n    ;; so that only the stuff that stays put for the duration of the game is left\n    (reduce\n      (fn [[a result] u]\n        (let [f (mask-board a u)]\n          [f (conj result f)]))\n      [(subtract-board final-position initial) []])\n    ;; Get the result\n    second\n    ;; Remove some noise\n    dedupe\n    ;; Back to original direction (probably not needed)\n    reverse\n    ;; Convert into [x y c] point vectors\n    (mapcat point-map)\n    frequencies\n    (group-by #(nth (first %) 2))\n    (map (fn [[k v]] [k (map first (reverse (sort-by second v)))]))\n    (into {})))\n\n(defn infer-moves\n  \"From a base game state, a board updatelist and a final-position, infer the move\n   sequence.\n\n   The meat of the algorithm is the clean-updatelist - Once that's done, it can\n   just interleave the black and white moves and play them to see if it ends up\n   as the final-position. Will return nil if a match didn't come out of the woodwork.\"\n  [game updatelist final-position]\n  (let [{:keys [kifu-board constructed]} game\n        {:keys [b w] :as clean} (clean-updatelist kifu-board updatelist final-position)\n        moves (apply util/interleave-all (if (= (:player-turn constructed) :black) [b w] [w b]))\n        inferred\n        (reduce\n          (fn [g [x y cell]]\n            (if (= (-> g :constructed :player-turn) ({:b :black :w :white} cell))\n              (play-move g [x y nil cell])\n              g))\n          game\n          moves)]\n    (print-boards (:kifu-board game) (:kifu-board inferred) final-position)\n    (if (= (:kifu-board inferred) final-position) inferred)))"
  },
  {
    "path": "src/igoki/integration/ogs.clj",
    "content": "(ns igoki.integration.ogs\n  (:require\n    [clj-http.client :as client]\n    [clojure.tools.logging :as log]\n    [clojure.string :as str]\n    [cheshire.core :as json]\n    [igoki.util.crypto :as crypto]\n    [igoki.inferrence :as inferrence]\n    [igoki.sgf :as sgf]\n    [igoki.sound.sound :as snd]\n    [clojure.edn :as edn]\n    [igoki.sound.announce :as announce])\n\n  (:import\n    (io.socket.client Socket IO Ack IO$Options)\n    (io.socket.emitter Emitter$Listener)\n    (org.json JSONObject)\n    (java.util Date)\n    (java.text SimpleDateFormat)))\n\n;; http://docs.ogs.apiary.io/\n;; https://ogs.readme.io/docs/real-time-api\n\n(def url \"https://online-go.com\")\n(def cm (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 10 :threads 3 :insecure? true}))\n\n(comment\n  (def cm (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 2 :threads 3 :insecure? true})))\n\n\n(defn display-rank [ranking pro?]\n  (cond\n    (nil? ranking)\n    \"??\"\n\n    (< ranking 30)\n    (str (int (- 30 (Math/floor ranking))) \"k\")\n\n    :else\n    (str (int (inc (- (Math/floor ranking) 30))) (if pro? \"p\" \"d\"))))\n\n(defn str-player [{:keys [username ranking rank professional]}]\n  (str username \" [\" (display-rank (or rank ranking) professional) \"]\"))\n\n(defn ogs-auth\n  [conn]\n  (client/post\n    (str url \"/oauth2/token/\")\n    {:connection-manager cm\n     :form-params (-> conn (assoc :grant_type \"password\") (dissoc :url))\n     :insecure? true\n     :as :json}))\n\n(defn ogs-headers\n  [auth]\n  {:connection-manager cm\n   :insecure? true\n   :headers   {\"Authorization\" (str \"Bearer \" (-> auth :body :access_token))}\n   :as :json})\n\n(defn config\n  [auth]\n  (client/get\n    (str url \"/api/v1/ui/config/\")\n    (ogs-headers auth)))\n\n(defn me\n  [auth]\n  (client/get\n    (str url \"/api/v1/me/\")\n    (ogs-headers auth)))\n\n(defn my-settings\n  [auth]\n  (client/get\n    (str url \"/api/v1/me/settings/\")\n    (ogs-headers auth)))\n\n(defn my-games\n  [auth]\n  (client/get\n    (str url \"/api/v1/me/games/\")\n    (ogs-headers auth)))\n\n(defn overview\n  [auth]\n  (:body\n    (client/get\n      (str url \"/api/v1/ui/overview\")\n      (ogs-headers auth))))\n\n\n(defn game-detail\n  [auth id]\n  (client/get\n    (str url \"/api/v1/games/\" id)\n    (ogs-headers auth)))\n\n(defn game-sgf\n  [auth id]\n  (client/get\n    (str url \"/api/v1/games/\" id \"/sgf/\")\n    (dissoc (ogs-headers auth) :as)))\n\n(defn move\n  [auth id coords]\n  (client/post\n    (str url \"/api/v1/games/\" id \"/move/\")\n    (assoc\n      (ogs-headers auth)\n      :form-params {:move coords}\n      :content-type :json)))\n\n(defn socket-echo [& xs]\n  (log/info \"Echo: \" xs))\n\n\n(defn socket-listener [^Socket socket event lfn]\n  (.on socket event\n    (proxy [Emitter$Listener] []\n      (call [xs]\n        (log/info \"Socket event: \" event)\n        #_(apply lfn (seq xs))\n        (apply lfn (map #(if (instance? JSONObject %) (json/decode (.toString %) keyword) %) (seq xs)))))))\n\n(defn socket-emit [sock event msg]\n  (let [m (JSONObject.)]\n    (doseq [[k v] msg]\n      (.put m (name k) v))\n    (.emit sock event\n      (into-array JSONObject [m]))))\n\n(defn socket-callback [sock event msg callback-fn]\n  (let [m (JSONObject.)]\n    (doseq [[k v] msg]\n      (.put m (name k) v))\n    (.emit sock event\n      (into-array JSONObject [m])\n      (proxy [Ack] []\n        (call [xs] (apply callback-fn (seq xs)))))))\n\n(defn socket-blocking [sock event msg callback-fn]\n  (let [m (JSONObject.)\n        response (promise)]\n    (doseq [[k v] msg]\n      (.put m (name k) v))\n    (.emit sock event\n      (into-array JSONObject [m])\n      (proxy [Ack] []\n        (call [xs]\n          (deliver response (seq xs)))))\n    (deref response 5000 :timeout)))\n\n(defn setup-socket []\n  (let [sock (IO/socket \"https://online-go.com/\"\n               (let [options (IO$Options.)]\n                 (set! (.-transports options) (into-array String [\"websocket\"]))\n                 (println (seq (.-transports options)))\n                 options))\n\n        #_(IO/socket \"http://online-go.com/socket.io\")]\n    (doseq [e [Socket/EVENT_CONNECT Socket/EVENT_CONNECT_ERROR\n               Socket/EVENT_CONNECT_TIMEOUT Socket/EVENT_DISCONNECT\n               Socket/EVENT_ERROR Socket/EVENT_MESSAGE\n               Socket/EVENT_RECONNECT Socket/EVENT_RECONNECT_ATTEMPT\n               Socket/EVENT_RECONNECT_ERROR Socket/EVENT_RECONNECT_FAILED\n               Socket/EVENT_RECONNECTING]]\n      (socket-listener sock e socket-echo))\n    (.connect sock)\n    sock))\n\n(defn add-move [game [x y time]]\n  (inferrence/play-move game [x y 0 (case (-> game :constructed :player-turn) :black :b :w)]))\n\n(defn play-move [c data]\n  (let [ogspath (-> c :ogs :current-branch-path)\n        ogsmovenumber (-> c :ogs :movenumber)\n        currentpath (-> c :kifu :current-branch-path)\n        currentmovenumber (-> c :kifu :movenumber)\n        kifu (inferrence/reconstruct (assoc (:kifu c) :current-branch-path ogspath :movenumber ogsmovenumber))\n        ogsgame (add-move kifu (:move data))\n        newpath (:current-branch-path ogsgame)\n        newmovenumber (:movenumber ogsgame)\n        game\n        (if (or (not= ogsmovenumber currentmovenumber) (not= ogspath currentpath))\n          (inferrence/reconstruct (assoc ogsgame :current-branch-path currentpath :movenumber currentmovenumber))\n          ogsgame)]\n    (->\n      c\n      (assoc :kifu game)\n      (update :ogs assoc\n        :game ogsgame\n        :current-branch-path newpath\n        :movenumber newmovenumber))))\n\n(defn initialize-game [c game]\n  (let [initial-node\n        (cond->\n          {:branches []\n           :player-start [(case (:initial_player game) \"white\" \"W\" \"B\")]\n           :application [(str \"igoki v\" (System/getProperty \"igoki.version\"))]\n           :file-format [\"4\"]\n           :gametype [\"1\"]\n           :size [(:width game) (:height game)]\n           :date [(.format (SimpleDateFormat. \"YYYY-MM-dd\") (Date. (* 1000 (:start_time game))))]\n           :game-name [(:game_name game)]\n           :black-rank [(-> game :players :black :rank)]\n           :black-name [(-> game :players :black :name)]\n           :white-rank [(-> game :players :white :rank)]\n           :white-name [(-> game :players :white :name)]}\n          (not (str/blank? (-> game :initial_state :white)))\n          (assoc :add-white (map (partial apply str) (partition 2 (-> game :initial_state :white))))\n\n          (not (str/blank? (-> game :initial_state :black)))\n          (assoc :add-black (map (partial apply str) (partition 2 (-> game :initial_state :black)))))\n\n        game-setup\n        (inferrence/reconstruct\n          {:moves initial-node\n           :current-branch-path [[]]\n           :movenumber 0})\n        game-setup (reduce add-move game-setup (:moves game))]\n    (->\n      c\n      (update :kifu merge game-setup)\n      (update :ogs assoc\n        :gameinfo game\n        :current-branch-path (:current-branch-path game-setup)\n        :movenumber (:movenumber game-setup)))))\n\n(def game-events\n  [\"gamedata\" \"clock\" \"phase\" \"undo_requested\" \"undo_accepted\" \"move\" \"conditional_moves\"\n   \"removed_stones\" \"removed_stones_accepted\" \"chat\" \"error\" \"reset\"])\n\n(defn disconnect-record [ctx]\n  (let [ogs (:ogs @ctx)]\n    (when (:gameid ogs)\n      (doseq [en game-events]\n        (.off (:socket ogs) (str \"game/\" (:gameid ogs) \"/\" en)))\n      (remove-watch ctx (str \"ogs.\" (:gameid ogs)))\n      (socket-emit (:socket ogs) \"game/disconnect\" {:game_id (:gameid ogs)}))))\n\n(defn check-submit-move [ctx old new]\n  (let [ogspath (-> new :ogs :current-branch-path)\n        oldpath (-> old :kifu :current-branch-path)\n        newpath (-> new :kifu :current-branch-path)\n        gameinfo (-> new :ogs :gameinfo)\n        players (-> new :ogs :players)]\n    ;; When either the kifu path or the ogspath changes - check for submission\n    (when-not (and (= oldpath newpath)\n                   (= (-> old :ogs :current-branch-path) ogspath))\n\n      (let [flatogspath (mapcat identity ogspath)\n            flatnewpath (mapcat identity newpath)]\n        (when\n          (and\n            (> (count flatnewpath) (count flatogspath))\n            (= flatogspath (take (count flatogspath) flatnewpath)))\n\n          (let [{:keys [black white]}\n                (->>\n                  (sgf/current-branch-node-list newpath (-> new :kifu :moves))\n                  (drop (inc (count flatogspath)))\n                  (first))\n                player (first (filter #(= (:id (:info %)) (get gameinfo (if black :black_player_id :white_player_id))) players))]\n            (cond\n              (and black player)\n              (do\n                (log/info \"Submitting Black move: \" black player)\n                (socket-emit (-> new :ogs :socket)\n                  \"game/move\"\n                  {:game_id (:game_id gameinfo)\n                   :move (first black)\n                   :player_id (-> player :info :id)\n                   :auth (:auth player)}))\n\n              (and white player)\n              (do\n                (log/info \"Submitting White move: \" white player)\n                (socket-emit (-> new :ogs :socket)\n                  \"game/move\"\n                  {:game_id (:game_id gameinfo)\n                   :move (first white)\n                   :player_id (-> player :info :id)\n                   :auth (:auth player)})))))))))\n\n\n(defn connect-record [ctx socket gameid auth & [auth2]]\n  ;; Disconnect any existing first.\n  (try\n    (disconnect-record ctx)\n    (catch Exception e\n      (.printStackTrace e)))\n\n  (try\n    (let [game (:body (client/get (str url \"/api/v1/games/\" gameid) (ogs-headers auth)))\n          player {:info (:body (me auth)) :auth (:auth game)}\n          player2 (if auth2 {:info (:body (me auth2)) :auth (:body (client/get (str url \"/api/v1/games/\" gameid) (ogs-headers auth2)))})\n          action #(str \"game/\" gameid \"/\" %)\n          listen\n          (fn [eventname]\n            (socket-listener\n              socket (action eventname)\n              #(do\n                 (log/info eventname \":\" %)\n                 (swap! ctx update-in [:ogs :event-stream]\n                   (fnil conj []) {:eventname eventname :data %}))))]\n      (doseq [en game-events]\n        (listen en))\n\n      (socket-listener\n        socket (action \"move\")\n        (fn [data]\n          (snd/play-sound :click)\n          (swap! ctx play-move data)\n          (let [{:keys [ogs kifu]} @ctx]\n            (println \"announcing move :\"\n              (last (sgf/current-branch-node-list (take (:movenumber ogs) (:current-branch-path ogs)) (:moves kifu)))\n              (igoki.inferrence/print-boards (-> ogs :game :kifu-board)))\n            (announce/comment-move ctx\n              (last (sgf/current-branch-node-list (take (:movenumber ogs) (:current-branch-path ogs)) (:moves kifu)))\n              (-> ogs :game :kifu-board)))))\n\n      (socket-listener\n        socket (action \"gamedata\")\n        (fn [data]\n          (cond\n            (= \"play\" (:phase data))\n            (swap! ctx initialize-game data)\n\n\n            (= \"finished\" (:phase data))\n            (do\n              (disconnect-record ctx)\n              (let [game\n                    {:sgf (:body (game-sgf auth gameid))\n                     :event-stream (:event-stream (:ogs @ctx))\n                     :gameid gameid\n                     :auth auth}]\n                (spit (str \"resources/ogs-game.\" gameid \".edn\")\n                  (pr-str game)))))))\n\n      (socket-emit socket \"game/connect\" {:game_id gameid :player_id (:id player) :chat true})\n\n      (add-watch\n        ctx (str \"ogs.\" gameid)\n        (fn [_ c o n]\n          (check-submit-move c o n)))\n\n      (swap! ctx update :ogs assoc\n        :socket socket\n        :gameid gameid\n        :players (if player2 [player player2] [player])\n        :game (:gamedata game))\n      {:success true})\n    (catch Exception e\n      (.printStackTrace e)\n      {:success false :msg (.getMessage e)})))\n\n(defn save-settings [{:keys [client-id client-secret username password remember]}]\n  (let [settings\n        {:client-id client-id\n         :client-secret client-secret\n         :username username\n         :remember remember}\n        settings\n        (if remember\n          (assoc settings :password password)\n          settings)]\n    (spit \"ogs.edn\"\n      (crypto/encrypt\n        (pr-str settings)))))\n\n(defn load-settings []\n  (try\n    (edn/read-string\n      (crypto/decrypt\n        (slurp \"ogs.edn\")))\n    (catch Exception e {})))\n\n(defn disconnect [ctx]\n  (let [ogs (:ogs @ctx)]\n    (when (:socket ogs)\n      (.disconnect (:socket ogs)))\n    (swap! ctx dissoc :ogs)))\n\n(defn refresh-games [ctx]\n  (swap! ctx assoc-in [:ogs :overview]\n    (overview (get-in @ctx [:ogs :auth]))))\n\n(defn connect\n  [ctx {:keys [client-id client-secret username password] :as settings}\n   progress-fn]\n  (disconnect ctx)\n  (save-settings settings)\n  (progress-fn :saved)\n\n  (let [auth\n        (try\n          (ogs-auth\n            {:client_id client-id\n             :client_secret client-secret\n             :username username\n             :password password})\n          (catch Exception e nil))\n\n        _ (progress-fn :logged-in)\n        authconfig (when auth (:body (config auth)))\n        _ (progress-fn :authconfig)\n        socket (when authconfig (setup-socket))\n        _ (progress-fn :socket)\n        player\n        (when socket (:body (me auth)))\n        _ (progress-fn :player-info)\n        overview\n        (when socket\n          (overview auth))\n        _ (progress-fn :overview)]\n\n    (cond\n      (nil? auth)\n      {:success false :message \"Authentication Failure?\"}\n\n      (nil? authconfig)\n      {:success false :message \"Could not fetch config\"}\n\n      (nil? socket)\n      {:success false :message \"Could not connect websocket\"}\n\n      (nil? player)\n      {:success false :message \"Could not fetch player info\"}\n\n      :else\n      (do\n        (socket-emit socket \"authenticate\"\n          {:auth (:chat_auth authconfig)\n           :player_id (:id (:user authconfig))\n           :username (:username (:user authconfig))})\n\n        (progress-fn :socket-auth)\n\n        (swap! ctx assoc :ogs\n          {:settings settings\n           :auth auth\n           :authconfig authconfig\n           :player player\n           :socket socket\n           :overview overview})\n\n        {:success true}))))\n\n(comment\n  (.on Socket/EVENT_CONNECT\n       (proxy [Emitter$Listener] []\n         (call [xs] (apply socket-connect (seq xs)))))\n\n  ;; Get clientid and secret by auth2 client here: https://online-go.com/developer\n  ;; Get app password from user profile\n\n  (def auth\n    (ogs-auth\n      {:client_id     \"\"\n       :client_secret \"\"\n       :username      \"\"\n       :password      \"\"}))\n\n  (def auth (ogs-auth (read-string (slurp \".creds\"))))\n  (def authconfig (:body (config auth)))\n  (def socket (setup-socket))\n  (socket-emit socket \"authenticate\"\n    {:auth (:chat_auth authconfig)\n     :player_id (:id (:user authconfig))\n     :username (:username (:user authconfig))})\n\n  (def player (:body (me auth)))\n  #_(def game (:body (client/get (str url \"/api/v1/games/3374557\") (ogs-headers auth))))\n  #_(def ctx (atom {}))\n  (connect-record igoki.main/ctx socket \"9567247\" auth)\n  (socket-emit socket \"game/connect\" {:game_id (:id game) :player_id (:id player) :chat false})\n  (socket-emit socket \"game/move\" {:game_id (:id game) :move \"rg\" :player_id (:id player) :auth (:auth game)}))\n"
  },
  {
    "path": "src/igoki/integration/robot.clj",
    "content": "(ns igoki.integration.robot\n  (:require\n    [seesaw.core :as s]\n    [igoki.camera :as camera]\n    [igoki.integration.ogs :as ogs]\n    [igoki.sgf :as sgf]\n    [clojure.string :as str]\n    [igoki.inferrence :as inferrence]\n    [igoki.sound.sound :as snd]\n    [igoki.sound.announce :as announce]\n    [clojure.tools.logging :as log])\n  (:import\n    (java.awt Robot Rectangle GraphicsDevice Point MouseInfo)\n    (java.awt.image BufferedImage)\n    (org.nd4j.linalg.exception ND4JIllegalStateException)\n    (java.util Date)\n    (java.text SimpleDateFormat)\n    (java.awt.event InputEvent)\n    (javax.swing JWindow)))\n\n\n; BufferedImage before = getBufferedImage(encoded);\n; int w = before.getWidth();\n; int h = before.getHeight();\n; BufferedImage after = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);\n; AffineTransform at = new AffineTransform();\n; at.scale(2.0, 2.0);\n; AffineTransformOp scaleOp =\n;    new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);\n; after = scaleOp.filter(before, after);\n\n(defn rescale-image [bufimg [width height]]\n  (let [result (BufferedImage. width height BufferedImage/TYPE_INT_RGB)\n        g2d (.getGraphics result)]\n\n    (.drawImage g2d bufimg 0 0 width height nil)\n    (.dispose g2d)\n    result))\n\n(defn read-frame [ctx]\n  (let [{:keys [goban] :as c} @ctx\n        {:keys [frame bounds ^Robot robot captured-list]} (:robot c)\n        size (or (:size goban) 19)\n        [x y w h] bounds\n        #_#__ (.setVisible frame false)\n        #_#__ (Thread/sleep 50)\n        bufimg (.createScreenCapture robot (Rectangle. x y w h))\n        scaled (rescale-image bufimg (camera/ref-size-vec (dec size)))\n        board\n        (doall\n          (for [y (range size)]\n            (doall\n              (for [x (range size)]\n                (try\n                  (let [pt\n                        (.getSubimage scaled (* x camera/block-size) (* y camera/block-size)\n                          camera/block-size camera/block-size)\n                        [b e w]\n                        (try\n                          (camera/eval-spot pt)\n                          (catch ND4JIllegalStateException e\n                            (.printStackTrace e)))]\n                    (cond\n                      (> b 0.5) :b\n                      (> w 0.5) :w))\n                  (catch Exception e))))))]\n\n    (swap! ctx update :robot assoc\n      :scaled scaled\n      :update-list (conj (or captured-list []) board)\n      :board board)\n\n    #_(.setVisible frame true)\n    (.repaint frame)))\n\n(defn read-robot-loop [ctx]\n  (let [{:keys [goban robot]} @ctx]\n    ;; If frame is paused, skip reading.\n    (when (true? (:started robot))\n      (read-frame ctx))\n    (Thread/sleep 250)\n\n    (when (:started robot)\n      (recur ctx))))\n\n(defn initialize [ctx game]\n  (let [initial-node\n        (cond->\n          {:branches []\n           :player-start [(case (:initial_player game) \"white\" \"W\" \"B\")]\n           :application [(str \"igoki v\" (System/getProperty \"igoki.version\"))]\n           :file-format [\"4\"]\n           :gametype [\"1\"]\n           :size [(:width game) (:height game)]\n           :date [(.format (SimpleDateFormat. \"YYYY-MM-dd\") (Date.))]\n           :game-name [(:game_name game)]\n           :black-rank [(-> game :players :black :rank)]\n           :black-name [(-> game :players :black :name)]\n           :white-rank [(-> game :players :white :rank)]\n           :white-name [(-> game :players :white :name)]}\n          (not (str/blank? (-> game :initial_state :white)))\n          (assoc :add-white (map (partial apply str) (partition 2 (-> game :initial_state :white))))\n\n          (not (str/blank? (-> game :initial_state :black)))\n          (assoc :add-black (map (partial apply str) (partition 2 (-> game :initial_state :black)))))\n\n        game-setup\n        (inferrence/reconstruct\n          {:moves initial-node\n           :current-branch-path [[]]\n           :movenumber 0})]\n\n    (swap! ctx\n      (fn [c]\n        (->\n          c\n          (update :kifu merge game-setup)\n          (update :robot assoc\n            :gameinfo game\n            :current-branch-path (:current-branch-path game-setup)\n            :movenumber (:movenumber game-setup)))))))\n\n(defn initialize-game [ctx]\n  (let [{:keys [goban robot]} @ctx\n        {:keys [game-detail board]} robot\n\n        converted\n        (->>\n          (for [[y rows] (map-indexed vector board)\n                [x cell] (map-indexed vector rows)]\n            (when cell\n              [cell (sgf/convert-coord x y)]))\n          (remove nil?))\n        white\n        (->>\n          converted\n          (filter (fn [[m _]] (= m :w)))\n          (map second)\n          (apply str))\n\n        black\n        (->>\n          converted\n          (filter (fn [[m _]] (= m :b)))\n          (map second)\n          (apply str))\n        ]\n    (initialize ctx\n      {:initial_player (str/lower-case (:initial-player game-detail))\n       :width (:size goban)\n       :height (:size goban)\n       :start_time (int (/ (System/currentTimeMillis) 1000))\n       :game_name (:game-name game-detail)\n       :players\n       {:black {:rank (:black-rank game-detail)\n                :name (:black-name game-detail)}\n        :white {:rank (:white-rank game-detail)\n                :name (:white-name game-detail)}}\n       :moves []\n       :initial_state\n       {:white white\n        :black black}})))\n\n(defn infer-board-play [ctx {:keys [robot kifu]}]\n  (let [{:keys [update-list]} robot\n        new (inferrence/infer-moves kifu update-list (last update-list))]\n    (when (and new (not= (:kifu-board new) (:kifu-board kifu)))\n      (swap! ctx\n        (fn [c]\n          (-> c\n              (assoc :kifu (dissoc new :submit))\n              (update :robot assoc :update-list []))))\n      (snd/play-sound :click)\n\n      (announce/comment-move ctx\n        (last\n          (sgf/current-branch-node-list\n            (take (:movenumber new) (:current-branch-path new)) (:moves new)))\n        (:constructed new)))))\n\n\n(defn check-submit-move [ctx old new]\n  (let [robot (get-in new [:robot :robot])\n        {:keys [robot-player]} (get-in new [:robot :game-detail])\n        oldpath (-> old :kifu :current-branch-path)\n        newpath (-> new :kifu :current-branch-path)\n        ]\n    ;; When either the kifu path or the ogspath changes - check for submission\n    (when (not= oldpath newpath)\n      (let [{:keys [black white]}\n            (->>\n              (sgf/current-branch-node-list newpath (-> new :kifu :moves))\n              (last))\n            move (first (or black white))]\n        (when\n          (or\n            (and move (= robot-player \"Both\"))\n            (and black (= robot-player \"Black\"))\n            (and white (= robot-player \"White\")))\n          (do\n            (log/info \"Submitting mouse click at: \" move)\n            (let [frame ^JWindow (get-in new [:robot :frame])\n\n                  size (get-in new [:goban :size])\n                  _ (println \"size: \" size)\n                  cellwidth (int (/ (.getWidth frame) size))\n                  cellheight (int (/ (.getHeight frame) size))\n                  _ (println \"cell: \" [cellwidth cellheight])\n                  [x y] (sgf/convert-sgf-coord move)\n                  _ (println \"coords: \" [x y])\n                  ;; Turns out this may not be needed? let's hope.\n                  #_#_reference-point\n                  (.getLocation\n                    (.getBounds\n                      (.getGraphicsConfiguration frame)))\n                  frame-location (.getLocationOnScreen frame)\n                  mx (int (+ (.getX frame-location) (* cellwidth x) (/ cellwidth 2)))\n                  my (int (+ (.getY frame-location) (* cellheight y) (/ cellheight 2)))\n                  mouse (.getLocation (MouseInfo/getPointerInfo))]\n              (println \"ref: \" )\n              (println \"m\" [mx my])\n              (.getGraphicsConfiguration frame)\n              ;; This doseq is due to a bug in older jre 8 - where it takes a few tries to get\n              ;; the mouse in the right place... what?!\n              (doseq [_ (range 5)]\n                (.mouseMove robot mx my))\n              (Thread/sleep 10)\n              (.mousePress ^Robot robot InputEvent/BUTTON1_DOWN_MASK)\n              (Thread/sleep 10)\n              (.mouseRelease ^Robot robot InputEvent/BUTTON1_DOWN_MASK)\n              (Thread/sleep 10)\n              (doseq [_ (range 5)]\n                (.mouseMove robot (.getX mouse) (.getY mouse))))))))))\n\n(defn start-capture [ctx ^GraphicsDevice screen bounds game-detail]\n  (try\n    (swap! ctx update :robot assoc\n      :started true :robot (Robot. screen) :bounds bounds\n      :game-detail game-detail)\n\n    (read-frame ctx)\n\n    (initialize-game ctx)\n\n    (add-watch ctx ::robot-capture\n      (fn [k r o n]\n        (try\n          ;; See if there's a new board state in the capture\n          (let [oldboard (get-in o [:robot :board])\n                newboard (get-in n [:robot :board])]\n\n            ;; See if there's a new board state that we need to infer\n            (when\n              (and\n                (true? (get-in o [:robot :started]))\n                (not= oldboard newboard))\n              (infer-board-play ctx n)))\n          (catch Exception e\n            (.printStackTrace e)))))\n\n    (add-watch ctx ::robot-submit\n      (fn [k r o n]\n        ;; See if we need to submit a click\n        (try\n          (check-submit-move ctx o n)\n\n          ;; Any exception shouldn't bubble up and kill stuff.\n          (catch Exception e\n            (.printStackTrace e)))))\n\n    (doto\n      (Thread. (partial #'read-robot-loop ctx))\n      (.setDaemon true)\n      (.start))\n    (catch Exception e\n      (s/alert (str \"Could not start capturing: \" (.getName (.getClass e)) \" - \" (.getMessage e)) :type :error)\n      (.printStackTrace e))))\n\n(defn pause-capture [ctx]\n  (when (-> @ctx :robot :started)\n    (swap! ctx update :robot assoc :started :paused)))\n\n(defn unpause-capture [ctx]\n  (when (-> @ctx :robot :started)\n    (swap! ctx update :robot assoc :started true)))\n\n(defn stop-capture [ctx]\n  (when (-> @ctx :robot :started)\n    (remove-watch ctx ::robot-capture)\n    (swap! ctx update :robot assoc :started false)))"
  },
  {
    "path": "src/igoki/litequil.clj",
    "content": "(ns igoki.litequil\n  (:require [seesaw.core :as s])\n  (:import\n    (javax.swing JPanel SwingUtilities JFrame)\n    (java.awt Graphics2D Dimension Color Image BasicStroke RenderingHints Font Polygon)\n    (java.awt.event MouseListener MouseEvent MouseMotionListener KeyListener KeyEvent WindowStateListener)\n    (java.awt.geom Ellipse2D$Double Rectangle2D)\n    (javax.swing.event AncestorListener AncestorEvent)))\n\n;; We desperately need to move off Processing. It doesn't compose well, so this implements the same\n;; abstractions we use in quil, but directly in a jpanel, which will let us do more UI things\n;; in basic Swing later.\n\n(def ^:dynamic *sketch* (atom nil))\n(def ^:dynamic ^JPanel panel nil)\n(def ^:dynamic ^Graphics2D g2d nil)\n\n(defn input-action [s local-panel options e k]\n  (let [afn (get options k)]\n    (if afn\n      (with-bindings\n        {#'*sketch* s\n         #'panel local-panel\n         #'g2d (.getGraphics local-panel)}\n        (afn e)))))\n\n(defn frame-sleep [sketch-atom]\n  (let [{:keys [last-frametime frame-rate]} @sketch-atom\n        ;; frame rate is frames per sec\n        ;; 10 = 10 frames for 1000 millis 1000/10 = 100ms ideal time per frame\n        target-time (/ 1000 frame-rate)\n        now (System/currentTimeMillis)\n        time-sleep (- target-time (- now (or last-frametime now)))]\n    #_(println \"Sleep time: \" time-sleep)\n    (when (pos? time-sleep)\n      (Thread/sleep time-sleep))\n\n    (swap! sketch-atom assoc :last-frametime now)))\n\n(defn sketch-panel [options]\n  (let [{:keys [draw setup close]} options\n        sketch-atom (atom {:options options :stopped false :frame-rate 10})\n        local-panel\n        (proxy [JPanel] []\n          (paintComponent [^Graphics2D local-g2d]\n            (when draw\n              (with-bindings\n                {#'*sketch* sketch-atom\n                 #'panel this\n                 #'g2d local-g2d}\n                (draw)))))]\n    (.setFocusable local-panel true)\n\n    (swap! sketch-atom assoc :panel local-panel)\n\n\n\n    (.addMouseListener local-panel\n      (proxy [MouseListener] []\n        (mouseClicked [^MouseEvent e]\n          (input-action sketch-atom local-panel options e :mouse-clicked))\n        (mousePressed [^MouseEvent e]\n          (input-action sketch-atom local-panel options e :mouse-pressed))\n        (mouseReleased [^MouseEvent e]\n          (input-action sketch-atom local-panel options e :mouse-released))\n        (mouseEntered [^MouseEvent e]\n          (input-action sketch-atom local-panel options e :mouse-entered))\n        (mouseExited [^MouseEvent e]\n          (input-action sketch-atom local-panel options e :mouse-exited))))\n\n    (.addMouseMotionListener local-panel\n      (proxy [MouseMotionListener] []\n        (mouseDragged [^MouseEvent e]\n          (input-action sketch-atom local-panel options e :mouse-dragged))\n        (mouseMoved [^MouseEvent e]\n          (input-action sketch-atom local-panel options e :mouse-moved))))\n\n    (.addKeyListener local-panel\n      (proxy [KeyListener] []\n        (keyPressed [^KeyEvent e]\n          (input-action sketch-atom local-panel options e :key-pressed))\n        (keyReleased [^KeyEvent e]\n          (input-action sketch-atom local-panel options e :key-released))\n        (keyTyped [^KeyEvent e]\n          (input-action sketch-atom local-panel options e :key-typed))))\n\n\n    (.addAncestorListener local-panel\n      (proxy [AncestorListener] []\n        (ancestorAdded [^AncestorEvent event]\n          (when\n            (and (.isVisible (.getAncestor event))\n              (not (:started @sketch-atom)))\n\n            (swap! sketch-atom assoc :started true :stopped false)\n\n            (when setup\n              (with-bindings\n                {#'*sketch* sketch-atom\n                 #'panel local-panel\n                 #'g2d (.getGraphics local-panel)}\n                (setup)))\n\n            (doto\n              (Thread.\n                (fn []\n                  (while (not (:stopped @sketch-atom))\n                    (frame-sleep sketch-atom)\n                    (when (.isVisible local-panel)\n                      (.repaint local-panel)))\n                  (println \"Paint thread stopped.\")))\n\n              (.setDaemon true)\n              (.start))))\n\n        (ancestorRemoved [event]\n          (println \"CLOSED!!!\")\n          (swap! sketch-atom assoc :stopped true :started false)\n          (when close\n            (close)))\n\n        (ancestorMoved [event])))\n\n\n    {:sketch-atom sketch-atom\n     :panel local-panel}))\n\n(defn sketch [options]\n  (let [{:keys [title size draw setup close]} options\n        ^JFrame local-frame\n        (s/frame\n          :title title\n          :icon \"igoki48.png\"\n          :resizable? true\n          :on-close :dispose)\n\n        sk (sketch-panel options)\n        local-panel (:panel sk)\n        sketch-atom (:sketch-atom sk)]\n\n    (when size\n      (let [[w h] size]\n        (.setSize local-panel (Dimension. w h))\n        (.setPreferredSize local-panel (Dimension. w h))))\n\n    (.add (.getContentPane local-frame) ^JPanel local-panel)\n\n    ;; Might not be needed now?\n    #_(.addWindowListener local-frame\n      (proxy [WindowAdapter] []\n        (windowClosed [e]\n          (swap! sketch-atom assoc :stopped true)\n          (when close\n            (close)))))\n    (doto local-frame\n      (.pack)\n      (.setExtendedState JFrame/MAXIMIZED_BOTH)\n      (.setVisible true)\n      )\n\n    (.grabFocus local-panel)\n    (swap! sketch-atom assoc :frame local-frame)\n    sketch-atom))\n\n(defn smooth []\n  (doto g2d\n    (.setRenderingHint RenderingHints/KEY_ANTIALIASING\n      RenderingHints/VALUE_ANTIALIAS_ON)\n    (.setRenderingHint RenderingHints/KEY_FRACTIONALMETRICS\n      RenderingHints/VALUE_FRACTIONALMETRICS_ON)\n    (.setRenderingHint RenderingHints/KEY_INTERPOLATION\n      RenderingHints/VALUE_INTERPOLATION_BICUBIC)))\n\n(defn frame-rate [rate-per-sec]\n  (when-not (or (nil? rate-per-sec) (zero? rate-per-sec))\n    (swap! *sketch* assoc :frame-rate rate-per-sec)))\n\n(defn width\n  ([]\n   (.getWidth panel))\n  ([sketch-atom]\n   (.getWidth (:panel @sketch-atom))))\n\n\n(defn height\n  ([]\n   (.getHeight panel))\n  ([sketch-atom]\n   (.getHeight (:panel @sketch-atom))))\n\n(defn color\n  ([g]\n   (.setColor g2d (Color. (int g) (int g) (int g))))\n  ([g a]\n   (.setColor g2d (Color. (int g) (int g) (int g) (int a))))\n  ([r g b]\n   (.setColor g2d (Color. (int r) (int g) (int b))))\n  ([r g b a]\n   (.setColor g2d (Color. (int r) (int g) (int b) (int a)))))\n\n(defn background\n  ([g]\n   (.setBackground g2d (Color. (int g) (int g) (int g))))\n  ([g a]\n   (.setBackground g2d (Color. (int g) (int g) (int g) (int a))))\n  ([r g b]\n   (.setBackground g2d (Color. (int r) (int g) (int b))))\n  ([r g b a]\n   (.setBackground g2d (Color. (int r) (int g) (int b) (int a)))))\n\n(defn stroke-weight [width]\n  (.setStroke g2d (BasicStroke. width)))\n\n\n\n(defn rect [x y w h]\n  (.clearRect g2d x y w h)\n  (.drawRect g2d x y w h))\n\n(defn fillrect [x y w h]\n  (.fillRect g2d x y w h))\n\n(defn ellipse\n  ([g2d x y w h]\n   (let [e (Ellipse2D$Double. (- x (/ w 2)) (- y (/ h 2)) w h)\n         bg (.getBackground g2d)\n         c (.getColor g2d)]\n     (.setColor g2d bg)\n     (.fill g2d e)\n     (.setColor g2d c)\n     (.draw g2d e)))\n  ([x y w h]\n   (ellipse g2d x y w h)))\n\n(defn triangle [x1 y1 x2 y2 x3 y3]\n  (let [t (doto (Polygon.)\n            (.addPoint x1 y1)\n            (.addPoint x2 y2)\n            (.addPoint x3 y3)\n            (.addPoint x1 y1))\n        bg (.getBackground g2d)\n        c (.getColor g2d)]\n    (.setColor g2d bg)\n    (.fill g2d t)\n    (.setColor g2d c)\n    (.draw g2d t)))\n\n(defn line\n  ([[x1 y1] [x2 y2]]\n   (line x1 y1 x2 y2))\n  ([x1 y1 x2 y2]\n   (.drawLine g2d x1 y1 x2 y2)))\n\n(defn text-size [size]\n  (.setFont g2d\n    (.deriveFont (.getFont g2d) (float size))))\n\n(defn calculate-horiz-offset [alignh ^Rectangle2D bounds]\n  (case alignh\n    :right (- (.getWidth bounds))\n    :center (- (/ (.getWidth bounds) 2))\n    0))\n\n(defn calculate-vert-offset [alignv ^Rectangle2D bounds]\n  (case alignv\n    :bottom (- (.getHeight bounds))\n    :center (- (/ (.getHeight bounds) 2))\n    0))\n\n(defn text [txt x y & [{:keys [align]}]]\n  (let [txt (str txt)\n        [alignh alignv] align\n        render-ctx (.getFontRenderContext g2d)\n        metrics (.getFontMetrics g2d)\n        font (.getFont g2d)\n        bounds (.getStringBounds font txt render-ctx)\n        offset-x (calculate-horiz-offset alignh bounds)\n        offset-y (calculate-vert-offset alignv bounds)]\n    #_(.drawRect g2d (+ offset-x x)\n      (+\n        offset-y\n        #_(.getDescent metrics)\n        (+ y #_(.getAscent metrics))) (.getWidth bounds) (.getHeight bounds))\n\n    (.drawString g2d ^String txt\n      (int (+ x offset-x))\n      (int (+ y offset-y (.getAscent metrics))))))\n\n(defn image [^Image img x y w h]\n  (let [scaled (.getScaledInstance img w h Image/SCALE_SMOOTH)]\n    (.drawImage g2d scaled (int x) (int y) nil)))\n\n(defn focused []\n  (.isFocused\n    (SwingUtilities/getWindowAncestor panel)))\n\n(defn mouse-position []\n  (.getMousePosition panel))\n\n(defn mouse-x\n  ([]\n   (let [position (.getMousePosition panel)]\n     (if position\n       (.getX position)\n       0)))\n  ([^MouseEvent e]\n   (.getX e)))\n\n(defn mouse-y\n  ([]\n   (let [position (.getMousePosition panel)]\n     (if position\n       (.getY position)\n       0)))\n  ([^MouseEvent e]\n   (.getY e)))\n\n(defn key-code [^KeyEvent e]\n  (.getKeyCode e))\n\n(defn shadow-text\n  ([^String s x y]\n   (shadow-text s x y :left :bottom))\n  ([^String s x y align-horiz]\n   (shadow-text s x y align-horiz :bottom))\n  ([^String s x y align-horiz align-vert]\n   (color 0 196)\n   (text-size 20)\n   (text s (inc x) (inc y)\n     {:align [(or align-horiz :left) (or align-vert :bottom)]})\n\n   (color 255)\n   (text-size 20)\n   (text s x y\n     {:align [(or align-horiz :left) (or align-vert :bottom)]})))\n\n(def fonts\n  {\"helvetica-20pt\" (Font. \"Helvetica\" Font/PLAIN 20)})\n\n(defn text-font [font-name]\n  (let [font (get fonts font-name (Font/decode font-name))]\n    (.setFont g2d font)))\n\n"
  },
  {
    "path": "src/igoki/projector.clj",
    "content": "(ns igoki.projector\n  (:require\n    [igoki.util :as util]\n    [igoki.game :as game]\n    [igoki.sgf :as sgf]\n    [igoki.litequil :as lq]\n    [igoki.camera :as camera]\n    [seesaw.core :as s])\n  (:import\n    (org.opencv.calib3d Calib3d)\n    (org.opencv.imgproc Imgproc)\n    (org.opencv.core Mat Size MatOfPoint2f TermCriteria Core Point Scalar CvType)))\n\n(defonce proj-ctx\n  (atom {}))\n\n(defn update-projmat [ctx]\n  (let [{:keys [homography board-homography sketch] :as projcontext} @proj-ctx\n        {:keys [camera proj-img kifu board goban] :as context} @ctx\n        {:keys [kifu-board]} kifu\n        existing-corners (:corners projcontext)\n        size (Size. 9 7)\n        lastmove (game/find-last-move ctx)]\n\n    (when board-homography\n      (util/with-release\n        [target (MatOfPoint2f.)\n         projmat (Mat. (Size. (* (inc (:size goban)) camera/block-size)\n                         (* (inc (:size goban)) camera/block-size)) CvType/CV_8UC3)\n         newflat (Mat.)]\n        (let [[[x1 y1] [x2 y2] [x3 y3] [x4 y4] :as corner-points]\n              (camera/target-points (:size goban))\n\n              sample-points (camera/sample-points corner-points (:size goban))\n              target (util/vec->mat target (apply concat sample-points))]\n          (Core/perspectiveTransform target target (.inv board-homography))\n          (Imgproc/rectangle projmat\n            (Point. 0 0)\n            (Point. (+ x3 camera/block-size) (+ y3 camera/block-size))\n            (Scalar. 0 0 0) -1)\n\n          ;; Highlight differences between constructed and camera board (visual syncing)\n          (when (and board kifu-board)\n            (doseq [[x y o c]\n                    (game/board-diff kifu-board board)]\n              (let [[px py] (nth (nth sample-points y) x)]\n                (Imgproc/circle projmat (Point. px py) 5 (Scalar. 0 0 255)\n                  (if (= o :b) 1 -1)))))\n\n          ;; Highlight last move\n          (let [{:keys [black white]} lastmove\n                m (or black white)]\n            (doseq [coord m]\n              (let [[x y] (sgf/convert-sgf-coord coord)\n                    [px py] (when (and x y) (nth (nth sample-points y) x))]\n                (when (and px py)\n                  (Imgproc/circle projmat (Point. px py) 5 (Scalar. 0 255 0)\n                    (if black 1 -1))))))\n\n          (when (:show-branches kifu)\n            (doseq\n              [[idx {:keys [black white]}] (map-indexed vector (:branches lastmove))\n               m (or black white)]\n              (let [[x y :as p] (sgf/convert-sgf-coord m)\n                    [px py] (nth (nth sample-points y) x)]\n                #_(Core/circle projmat (Point. px py) 5 (Scalar. 255 255 255) -1)\n                (Imgproc/putText projmat (str (char (+ 65 idx))) (Point. (- px 5) (+ py 5))\n                  Imgproc/FONT_HERSHEY_PLAIN 0.7 (Scalar. 255 255 255) 1.5))))\n\n          ;; Draw entire current board state\n          #_(doseq [[y row] (map-indexed vector (:board @ui/ctx))\n                    [x cell] (map-indexed vector row)]\n             (let [[x y] (nth (nth sample-points y) x)]\n               (case cell\n                 :b (Core/circle projmat (Point. x y) 5 (Scalar. 255 255 255) 1)\n                 :w (Core/circle projmat (Point. x y) 5 (Scalar. 255 255 255) -1)\n                 nil)))\n\n\n          (Imgproc/warpPerspective projmat newflat (.inv board-homography) (Size. (lq/width sketch) (lq/height sketch)))\n          (swap! proj-ctx assoc :proj-img (util/mat-to-pimage newflat (:bufimg proj-img)))\n\n          #_(doseq [[x y] (util/mat->seq target)]\n              (lq/color 0 0 255)\n              (lq/stroke-weight 20)\n              (lq/point x y)\n              (lq/stroke-weight 0))))\n      #_(Core/circle (:raw camera) (Point. 540 265) 20 (Scalar. 255 0 0)))))\n\n\n(defn draw-checkerboard [{:keys [board]}]\n  (doseq [[x y w h] board]\n    (lq/rect x y w h)))\n\n(defn checkerboard [x y width height xblocks yblocks]\n  (let [bw (/ width xblocks)\n        bh (/ height yblocks)]\n    {:setup {:x x :y y :width width :height height :xblocks xblocks :yblocks yblocks}\n     :size (Size. (dec xblocks) (dec yblocks))\n     :board\n     (for [cellx (range xblocks) celly (range yblocks)\n           :when (= (mod (+ cellx (* celly (inc xblocks))) 2) 1)]\n       (if (= (mod celly 2) 0)\n         [(+ x (* cellx bw)) (+ y (* celly bh)) (dec bw) (dec (+ bh (/ bh 10)))]\n         [(+ x (* cellx bw)) (+ y (* celly bh) (/ bh 10)) (dec bw) (dec (- bh (/ bh 10)))]))\n     :points\n     (for [celly (range 1 yblocks) cellx (range 1 xblocks)]\n       (if (= (mod celly 2) 0)\n         [(+ x (* cellx bw)) (+ y (* celly bh))]\n         [(+ x (* cellx bw)) (+ y (* celly bh) (/ bh 10))]))}))\n\n(defn fix-checker-orientation [corners-mat]\n  (let [corners (util/mat->seq corners-mat)\n        p1 (nth corners 0)\n        p2 (nth corners 9)\n        p3 (nth corners 18)]\n    (if (> (util/line-length [p1 p2]) (util/line-length [p2 p3]))\n      (util/vec->mat corners-mat (reverse corners))\n      corners-mat)))\n\n(defn look-for-checkerboard [proj-ctx camera checker]\n  (util/with-release [gray (Mat.)]\n    (let [corners (MatOfPoint2f.)\n          crit (TermCriteria. (bit-or TermCriteria/EPS TermCriteria/MAX_ITER) 30 0.1)]\n      (Imgproc/cvtColor (:raw camera) gray Imgproc/COLOR_BGR2GRAY)\n      (let [found\n            (Calib3d/findChessboardCorners gray (:size checker) corners\n              (+ Calib3d/CALIB_CB_ADAPTIVE_THRESH\n                Calib3d/CALIB_CB_NORMALIZE_IMAGE\n                Calib3d/CALIB_CB_ASYMMETRIC_GRID\n                Calib3d/CALIB_CB_EXHAUSTIVE))]\n        (.println System/out (str \"Checking: \" found))\n        (when found\n          (println \"Found corners.\")\n          #_(Imgproc/cornerSubPix gray corners (Size. 3 3) (Size. -1 -1) crit)\n          (fix-checker-orientation corners)\n          (swap! proj-ctx assoc :corners corners))))))\n\n(defn update-homography [proj-ctx existing-corners checker]\n  (util/with-release\n    [target (MatOfPoint2f.)]\n    (let [target (util/vec->mat target (:points checker))\n          homography\n          (Calib3d/findHomography ^MatOfPoint2f existing-corners ^MatOfPoint2f target\n            Calib3d/FM_RANSAC 3.0)]\n      (when homography\n        (println \"Homography updated\")\n        (swap! proj-ctx assoc :homography homography)))))\n\n(defn update-board-homography [ctx proj-ctx homography]\n  (util/with-release\n    [projector-space (MatOfPoint2f.)\n     board-space (MatOfPoint2f.)]\n    (let [goban (:goban @ctx)\n          projector-space (util/vec->mat projector-space (:points goban))\n          _ (Core/perspectiveTransform projector-space projector-space homography)\n          board-space (util/vec->mat board-space (camera/target-points (:size goban)))\n          board-homography\n          (Calib3d/findHomography ^MatOfPoint2f projector-space ^MatOfPoint2f board-space\n            Calib3d/FM_RANSAC 3.0)]\n\n      (doseq [[x y] (util/mat->seq projector-space)]\n        (lq/color 255 0 0)\n        (lq/ellipse x y 10 10))\n\n      (when board-homography\n        (println \"Board Homography updated\")\n        (swap! proj-ctx assoc :board-homography board-homography)))))\n\n(defn draw [proj-ctx ctx]\n  (lq/frame-rate 10)\n  (lq/background 0 0 0)\n  (lq/rect 0 0 (lq/width) (lq/height))\n\n  (let [[w h] [(lq/width) (lq/height)]\n        [gw gh] [(/ w 2) (/ h 2)]\n        checker (checkerboard (- gw (/ gw 2)) (- gh (/ gh 2)) gw gh 10 8)\n\n        {:keys [camera goban] :as context} @ctx\n        {:keys [homography board-homography calibrate? proj-img] :as projcontext} @proj-ctx\n        existing-corners (:corners projcontext)\n        img (:bufimg proj-img)]\n\n    (lq/background 255 255 255)\n    (lq/rect 0 0 (lq/width) (lq/height))\n    (lq/background 0 0 0)\n\n    (when calibrate?\n      (draw-checkerboard checker)\n\n      ;; Draw screen intersection points\n      #_(do\n        (lq/color 255 0 0)\n        (doseq [[x y] (:points checker)]\n          (lq/ellipse x y 5 5)))\n\n      #_(lq/image (-> @proj-ctx :pattern) (- (/ (q/width) 2) 180) (- (/ (q/height) 2) 200) 300 400)\n\n\n\n\n      ;; There's no way this should be happening in the draw call.\n\n      (cond\n        (:raw camera)\n        (look-for-checkerboard proj-ctx camera checker))\n\n\n\n      ;; Draw the found checkerboard for acceptance..\n      (when (and existing-corners)\n        (util/with-release [clone (.clone (:raw camera))]\n          (Calib3d/drawChessboardCorners clone (:size checker) existing-corners true)\n          (swap! ctx update :camera\n            assoc :pimg\n            (util/mat-to-pimage clone\n              (-> context :camera :pimg :bufimg))))))\n\n    (when-not calibrate?\n      (cond\n        (and existing-corners (not homography))\n        (update-homography proj-ctx existing-corners checker)\n\n        (and homography (= 4 (count (:points goban))) (not board-homography))\n        (update-board-homography ctx proj-ctx homography)\n\n        proj-img\n        (lq/image img 0 0 (.getWidth img) (.getHeight img))))))\n\n(defn reset-ctx []\n  (reset! proj-ctx {:sketch (:sketch @proj-ctx)}))\n\n(defn show-calibration []\n  (swap! proj-ctx assoc :calibrate? true))\n\n(defn accept-calibration [ctx]\n  (swap! proj-ctx assoc :calibrate? false)\n  (swap! ctx assoc-in [:projector :setting-up] false))\n\n(defn stop-cframe [ctx close-fn]\n  (let [{:keys [sketch]} @proj-ctx]\n    (when (and sketch (:frame @sketch))\n      (.dispose (:frame @sketch))))\n  (reset! proj-ctx {})\n  (swap! ctx dissoc :projector)\n  (when close-fn\n    (close-fn)))\n\n(defn start-cframe [ctx close-fn]\n  (stop-cframe ctx close-fn)\n\n  (let [sketch\n        (lq/sketch\n          {:title \"Move on board in camera view (place paper on board for contrast)\"\n           :draw (partial #'draw proj-ctx ctx)\n           :size (or (-> @proj-ctx :sketchconfig :size) [1280 720])})]\n    (swap! proj-ctx assoc :sketch sketch :calibrate? false))\n  (swap! ctx assoc :projector {:setting-up true})\n  (doto\n    (Thread.\n      ^Runnable\n      (fn []\n        (while (not (:stopped (:sketch @proj-ctx)))\n          (try\n            (update-projmat ctx)\n            (catch Exception e\n              (.printStackTrace e)))\n          (Thread/sleep 500))\n        (when close-fn\n          (close-fn))))\n    (.setDaemon true)\n    (.start))\n  #_(when (:sketch @proj-ctx)\n     (doto (:sketch @proj-ctx)\n       #_(.setExtendedState JFrame/MAXIMIZED_BOTH)\n       #_(.setUndecorated true))))\n\n\n\n"
  },
  {
    "path": "src/igoki/scratch/scratch.clj",
    "content": "(ns igoki.scratch.scratch\n  (:require\n    [igoki.util :as util :refer [-->]])\n  (:import\n    (org.opencv.objdetect CascadeClassifier)\n    (org.opencv.core MatOfRect Core Rect Point Scalar Mat Size MatOfPoint MatOfKeyPoint MatOfPoint2f Point3 TermCriteria MatOfPoint3 CvType MatOfPoint3f)\n    (java.awt.image BufferedImage WritableRaster DataBufferByte)\n    (java.awt Color Graphics KeyboardFocusManager KeyEventDispatcher Font RenderingHints)\n    (java.io File)\n    (javax.imageio ImageIO)\n    (javax.swing JFrame JPanel)\n    (org.opencv.imgproc Imgproc)\n    (java.awt.event KeyEvent MouseListener MouseEvent)\n    (org.opencv.imgcodecs Imgcodecs)\n    (org.opencv.videoio VideoCapture Videoio)))\n\n;; This namespace represents some of the early igoki work, mostly to detect the actual Go Board.\n;; it has been deprecated in favour of simply doing manual calibration due to the complexity\n;; of dealing with the fickleness of variances in Go boards, lighting conditions, camera quality,\n;; etc.\n;;\n;; It would be neat to have automatic handling, but it's not the core objective of this project to\n;; detect Go boards - instead, it's focussed on bridging the gap between the digital and physical\n;; game.\n\n;; Step 1 - Define Corners of board\n;; Step 2 - Verify coordinates\n;; Step 3 - Choose mode: Local Kifu, OGS Kifu\n\n(nu.pattern.OpenCV/loadShared)\n\n(defonce camera (atom nil))\n\n\n\n(defonce appstate\n         (atom\n           {:images      [{} {} {} {} {}]\n            :goban-corners [[670 145] [695 900] [1320 855] [1250 220]]\n            :input :camera\n            :selected    -1\n            :frozen      false}))\n\n\n\n(defn update-image-mat! [slot image title]\n  (swap! appstate #(assoc-in % [:images slot] {:mat image :title title})))\n\n(defn reset-to-index! []\n  (swap! appstate assoc :selected -1))\n\n(defn rotate-slot-left! []\n  (swap! appstate (fn [i] (update i :selected #(mod (dec %) (count (:images i)))))))\n\n(defn rotate-slot-right! []\n  (swap! appstate (fn [i] (update i :selected #(mod (dec %) (count (:images i)))))))\n\n(defn select-frame! [n]\n  (swap! appstate (fn [i] (assoc i :selected (mod n (count (:images i)))))))\n\n(defn toggle-freeze! []\n  (println \"Freeze toggle\")\n  (swap! appstate update-in [:frozen] not))\n\n(defn save-image [^BufferedImage img]\n  (ImageIO/write img \"png\" (File. \"resources/new.png\")))\n\n(defn load-image [^String file]\n  (ImageIO/read (File. file)))\n\n(defn handle-keypress [^KeyEvent e]\n  (println \"Key pressed: \" (.getKeyCode e) \" - Shift: \" (.isShiftDown e) )\n  (when (= (.getID e) KeyEvent/KEY_PRESSED)\n    (case (.getKeyCode e)\n      67 (swap! appstate assoc :input :camera)\n      82 (swap! appstate assoc :input (Imgcodecs/imread \"resources/goboard.png\"))\n      32 (toggle-freeze!)\n      27 (reset-to-index!)\n      49 (select-frame! 1)\n      50 (select-frame! 2)\n      51 (select-frame! 3)\n      52 (select-frame! 4)\n      53 (select-frame! 5)\n      54 (select-frame! 6)\n      55 (select-frame! 7)\n      56 (select-frame! 8)\n      57 (select-frame! 9)\n      48 (select-frame! 0)\n\n      10 (swap! appstate assoc :accepted true)\n      false)))\n\n(defn draw-title [g title x y]\n  (.setColor g (Color/BLACK))\n  (.drawString g title (dec x) (dec y))\n  (.setColor g (Color/WHITE))\n  (.drawString g title x y))\n\n(defn draw-index [^JFrame frame ^Graphics g {:keys [images]}]\n  (let [gridsize (Math/ceil (Math/sqrt (count images)))\n        gw (/ (.getWidth frame) gridsize)]\n    (doseq [[c {:keys [mat title]}] (map-indexed vector images)]\n      (if-let [image (if (pos? (.width mat)) (util/mat-to-buffered-image mat nil))]\n        (let [ratio (if image (/ (.getHeight image) (.getWidth image)))\n              x (* (mod c gridsize) gw)\n              y (* (Math/floor (/ c gridsize)) ratio gw)]\n          (.drawImage g image (int x) (int y) (int gw) (int (* ratio gw)) nil)\n          (draw-title g (str title \", Slot: \" c) (int (+ x 5)) (int (+ y 15)))\n          )))))\n\n(defn render [^JFrame frame ^Graphics g]\n  (let [{:keys [images selected] :as state} @appstate\n        {:keys [mat title] :as im} (get images selected)\n        image (if (and im (pos? (.getWidth frame))) (util/mat-to-buffered-image mat nil))\n        ratio (if image (/ (.getHeight image) (.getWidth image)))]\n    (.setRenderingHints g (RenderingHints. RenderingHints/KEY_INTERPOLATION RenderingHints/VALUE_INTERPOLATION_BICUBIC))\n    (if (or (= selected -1) (nil? image))\n      (draw-index frame g state)\n      (do\n        (.drawImage g image 0 0 (.getWidth frame) (* ratio (.getWidth frame)) nil)\n        (draw-title g (str title \", Slot: \" selected) 5 15)))))\n\n(defn click-mouse [^MouseEvent e]\n  )\n\n(defn window [text x y]\n  (let [frame (JFrame.)]\n    (.add (.getContentPane frame)\n          (proxy [JPanel] []\n            (paint [^Graphics g]\n              (render frame g))\n            ))\n\n    (.addMouseListener\n      frame\n      (proxy [MouseListener] []\n        (mousePressed [^MouseEvent e]\n          (click-mouse e))))\n\n    (.addKeyEventDispatcher (KeyboardFocusManager/getCurrentKeyboardFocusManager)\n                            (proxy [KeyEventDispatcher] []\n                              (dispatchKeyEvent [e]\n                                (handle-keypress e)\n                                false)))\n    (doto frame\n      (.setDefaultCloseOperation JFrame/DISPOSE_ON_CLOSE)\n      (.setTitle text)\n      (.setResizable true)\n      (.setSize 800 600)\n      (.setLocation x y)\n      (.setVisible true))))\n\n#_(defn highlight-faces [image]\n  (let [face-detector (CascadeClassifier. (.getAbsolutePath (clojure.java.io/file \"resources/lbpcascade_frontalface.xml\")))\n        face-detections (MatOfRect.)]\n    (.detectMultiScale face-detector image face-detections)\n    (doseq [^Rect r (seq (.toArray face-detections))]\n      (Core/rectangle image (Point. (.-x r) (.-y r)) (Point. (+ (.-x r) (.-width r)) (+ (.-y r) (.-height r))) (Scalar. 0 255 0)))\n    ))\n\n(defn matofpoint-vec\n  \"Convert MatOfPoint to vector list [[x y] [x y] ...]\"\n  [mat]\n  (for [p (.toList mat)]\n    [(.-x p) (.-y p)]))\n\n\n(def target-homography\n  {:9x9\n   (doto (MatOfPoint2f.)\n     (.fromList (for [x (range 1 3) y (range 1 2)] (Point. (* 70.0 x) (* 70.0 y)))))\n   :13x13\n   (util/vec->mat (MatOfPoint2f.) [[70 70] [70 980] [980 980] [980 70]])\n   :19x19\n   (doto (MatOfPoint2f.)\n     (.fromList (for [x (range 1 2) y (range 1 2)] (Point. (* 70.0 x) (* 70.0 y)))))})\n\n(def current-transform (atom nil))\n\n(defn count-perimeter [mat]\n  (let [m (for [x (range (.rows mat))] (seq (.get mat x 0)))]\n    (first\n      (reduce\n        (fn [[r [ax ay :as a]] [x y :as p]]\n          (cond\n            (nil? a) [r p]\n            :else\n            [(+ r (Math/sqrt (+ (* (- ax x) (- ax x)) (* (- ay y) (- ay y))))) p]))\n        [0 nil] (concat [(last m)] m)))))\n\n(defn find-goban [gray-img colour]\n  #_(let [sorted (:goban-corners @appstate)\n        s (map-indexed vector sorted)\n        c (--> gray-img (Imgproc/Canny 100 50 3 false))]\n    (update-image-mat! 5 c \"Contours\")\n    (reduce\n      (fn [[i [ax ay :as a]] [_ [x y] :as p]]\n        (cond\n          (nil? a) p\n          :else\n          (do\n            (Core/line colour (Point. ax ay) (Point. x y) (Scalar. 0 0 (- 255 (* i 32))) 5)\n            p)))\n      nil (concat s [(first s)]))\n    sorted\n    )\n\n  #_(let [contours (ArrayList.) hier (Mat.)\n          c (--> gray-img (Imgproc/Canny 100 50 3 false))]\n      ;; Find contours\n      (Imgproc/findContours c\n                            contours hier Imgproc/RETR_TREE Imgproc/CHAIN_APPROX_NONE (Point. 0 0))\n      (update-image-mat! 5 c \"Contours\")\n\n      ;; Find largest area 4-cornered polygon\n      (let\n        [[sq x _]\n         (loop [[x & xs] (range 0 (.cols hier))\n                [_ _ rarea :as result] nil]\n           (if (nil? x)\n             result\n             (let [sq (MatOfPoint2f.)\n                   c (nth contours x)\n                   area (Imgproc/contourArea c)\n                   perim (count-perimeter c)]\n               (Imgproc/approxPolyDP (doto (MatOfPoint2f.) (.fromArray (.toArray c))) sq (* perim 0.02) true)\n               (cond\n                 (and (= 4 (.rows sq)) (> area (or rarea 0)))\n                 (recur xs [sq x area])\n\n                 :else (recur xs result)))))\n\n         ;; Sort the corners\n         sorted\n         (if-not (nil? sq)\n           (let [pointlist (matofpoint-vec sq)\n                 closest-to-origin (first (sort-by #(apply + %) (matofpoint-vec sq)))\n                 ;; Rotate corners until closest-to-origin is first\n                 [p1 [_ pfy :as pf] pc [_ ply :as pl] :as rotated] (take 4 (drop-while #(not= closest-to-origin %) (concat pointlist pointlist)))]\n             ;; Flip so that 'top right' point is next\n             (if (< pfy ply) rotated [p1 pl pc pf])))]\n\n        ;; Draw the actual matching contour\n        (Imgproc/drawContours colour contours x\n                              (Scalar. 0 0 255) 1 0 hier 1 (Point. 0 0))\n\n        ;; Draw the \"board\" polygon.\n        (let [s (map-indexed vector sorted)]\n          (reduce\n            (fn [[i [ax ay :as a]] [_ [x y] :as p]]\n              (cond\n                (nil? a) p\n                :else\n                (do\n                  (Core/line colour (Point. ax ay) (Point. x y) (Scalar. 0 0 (- 255 (* i 32))) 5)\n                  p)))\n            nil (concat s [(first s)])))\n\n        sorted)))\n\n\n#_(defn old-process [w calibration frame]\n  (do\n    #_(highlight-faces frame)\n    (let [fil (Mat.) m (Mat.) m2 (Mat.) edges (Mat.) hough (Mat.) hough-img (Mat.)\n          corners (MatOfPoint.) corners2f (MatOfPoint2f.)\n          timg (Mat.) detectimg (Mat.)\n\n          dest\n          (-->\n            frame\n            (Imgproc/cvtColor Imgproc/COLOR_BGR2GRAY)\n            (Imgproc/bilateralFilter 5 (double 155) (double 105))\n            )\n\n          colour\n          (-->\n            dest\n            (Imgproc/cvtColor Imgproc/COLOR_GRAY2BGR))\n          [p1 pf _ pl :as goban-corners] (find-goban dest colour)\n          goban-contour (util/vec->mat (MatOfPoint2f.) goban-corners)]\n      (Imgproc/goodFeaturesToTrack dest corners 1000 0.03 15 (Mat.) 10 false 0.1)\n      (.fromArray corners2f (.toArray corners))\n      (Imgproc/cornerSubPix dest corners2f (Size. 11 11) (Size. -1 -1)\n                            (TermCriteria. (bit-or TermCriteria/EPS TermCriteria/COUNT) 30 0.1))\n\n      #_(println goban-corners)\n      #_(println (filter\n                   #(pos? (Imgproc/pointPolygonTest goban-corners % true))\n                   (seq (.toArray corners2f))))\n\n\n      (let\n        [goban-points\n         (->>\n           (seq (.toArray corners2f))\n           (filter\n             #(> (Imgproc/pointPolygonTest goban-contour % true) -10))\n           (map (fn [p] [(.-x p) (.-y p)])))\n         _ (println (count goban-points))\n         {:keys [size target]}\n         (condp < (count goban-points)\n           400 {:size 19 :target (:19x19 target-homography)}\n           100 {:size 13 :target (:13x13 target-homography)}\n           {:size 9 :target (:9x9 target-homography)}\n           )\n         sorted\n         (sort-by\n           (juxt\n             (comp #(int (/ % 25)) (partial util/line-to-point-dist [p1 pf]))\n             (comp #(int (/ % 25)) (partial util/line-to-point-dist [p1 pl])))\n           (take (.rows target) goban-points))\n         origpoints\n         (doto (MatOfPoint2f.)\n           (.fromList (map (fn [[x y]] (Point. x y)) goban-corners)))\n         h (if (= (.rows target) (count goban-corners))\n             (Calib3d/findHomography ^MatOfPoint2f origpoints ^MatOfPoint2f target Calib3d/FM_RANSAC 3.0))]\n\n\n        (if h\n          (reset! current-transform h))\n\n        (when-let [h @current-transform]\n          (let [transformed (Mat.) ih (Mat.) invert-transformed (Mat.)]\n            (Core/perspectiveTransform corners2f transformed h)\n            #_(Core/perspectiveTransform target invert-transformed ih)\n\n            (Imgproc/warpPerspective frame timg h (.size frame))\n            (Imgproc/warpPerspective frame detectimg h (.size frame))\n\n            #_(Imgproc/erode detectimg detectimg (Imgproc/getStructuringElement Imgproc/MORPH_RECT (Size. 10 10)))\n            (doseq [c (range 0 (.rows transformed))]\n              (let [[x1 y1 :as p] (seq (.get transformed c 0))]\n                #_(Core/putText hough-img (str c) (Point. x1 y1) Core/FONT_HERSHEY_COMPLEX 1 (Scalar. 0 255 0) 2)\n                (Core/circle colour (Point. x1 y1) 2 (Scalar. 128 255 0) 2)\n                ))\n            (doseq [c (range 0 (.rows invert-transformed))]\n              (let [[x1 y1 :as p] (seq (.get invert-transformed c 0))]\n                #_(Core/putText hough-img (str c) (Point. x1 y1) Core/FONT_HERSHEY_COMPLEX 1 (Scalar. 0 255 0) 2)\n                (Core/circle colour (Point. x1 y1) 2 (Scalar. 0 255 0 128) 10)\n                ))\n            )\n          (Core/rectangle detectimg (Point. 70 70) (Point. 980 980) (Scalar. 0 255 0) 1)\n\n          (doseq [x (range 1 (inc size))]\n            (doseq [y (range 1 (inc size))]\n              (let [p (Point. (- (* 75.0 x) 15) (- (* 75.0 y) 15))\n                    roi (Rect. p (Size. 30 30))\n                    m (Mat. detectimg roi)\n                    a (Core/mean m)\n                    c (int (first (seq (.-val a))))\n                    text (cond (< c 30) \"B\" (> c 200) \"W\")]\n                (when text\n                  (Core/putText timg text p Core/FONT_HERSHEY_COMPLEX 1 (Scalar. 0 0 255) 1.5))\n                (Core/rectangle detectimg p (Point. (+ (.-x p) 30) (+ (.-y p) 30)) (Scalar. 0 255 0) 1)\n                (Core/circle timg (Point. (+ (.-x p) 15) (+ (.-y p) 15)) 5 a 5))))))\n\n      #_(doseq [{[x y] :value :as p}\n                (filter #(> (:strength (meta %)) 10) (kdtree-seq @stable-points))]\n          (Core/circle colour (Point. x y)\n                       (/ (Math/min (or (:strength (meta p)) 1) 100) 5) (Scalar. 255 25 25) 2))\n\n      (doseq [p (seq (.toArray corners2f))]\n        (Core/circle colour p 2 (Scalar. 255 0 255) 3))\n\n\n      (update-image-mat! 0 frame \"Source\")\n      (update-image-mat! 1 dest \"Find points\")\n      (update-image-mat! 2 colour \"Find points\")\n      (update-image-mat! 3 timg \"Detected goban\")\n      (update-image-mat! 4 detectimg \"Check for pieces\")\n      #_(when-not (.empty timg)\n          (update-image-mat! 2 timg \"Perspective Shifted\")\n          (update-image-mat! 3 detectimg \"Detect Stones\")\n          )\n      (.repaint w)\n      true)))\n\n(defn refresh-camera [w camera frame]\n  (Thread/sleep 100)\n  #_(let [{:keys [frozen calib-corners calibration input] :as state} @appstate\n        read (if frozen true (if (= input :camera) (.read camera frame) false))\n        frame (if (= input :camera) frame input)]\n    (cond\n      (not read) false\n      :else\n      (old-process w calibration frame)))\n  (.repaint w))\n\n\n\n(defn capture [camidx]\n  (let [camera (VideoCapture. ^int camidx Videoio/CAP_ANY)\n        frame (Mat.)]\n    (.read camera frame)\n\n    (cond\n      (not (.isOpened camera)) (println \"Error: camera not opened\")\n      :else\n      (do\n        (update-image-mat! 0 frame \"Source\")\n        (let [w (window \"Original\" 0 0)]\n          (swap! appstate assoc :diag-window w)\n          (doto\n            (Thread.\n              #(loop []\n                (try\n                  (refresh-camera w camera frame)\n                  (catch Exception e\n                    (.printStackTrace e)\n                    (Thread/sleep 5000)))\n                (recur)))\n            (.setDaemon true)\n            (.start)))))\n    #_(.release camera)))\n\n\n\n;; Some work on hough circles, discarded because it gets very inaccurate on busy boards.\n(comment\n  (defn hough-circles [m]\n    (util/with-release\n      [mat (Mat.)\n       bilat (Mat.)\n       blurred (Mat.)\n       white-mask (Mat.)\n       black-mask (Mat.)\n       laplacian (Mat.)\n       masked (Mat.)\n       circles (Mat.)\n       canny (Mat.)]\n      #_(doseq [r (range (count (:signature cluster)))]\n          (let [[x y] [(mod r szx) (int (/ r szx))]]\n            (.put ^Mat mat x y (double-array (repeat 3 (* 255.0 (double (get (:signature cluster) r))))))))\n\n      (Imgproc/cvtColor m mat Imgproc/COLOR_HSV2BGR)\n      (Imgproc/cvtColor mat mat Imgproc/COLOR_BGR2GRAY)\n      (Imgproc/dilate mat blurred (Imgproc/getStructuringElement Imgproc/MORPH_ELLIPSE (Size. 7 7)))\n      (Imgproc/erode blurred blurred (Imgproc/getStructuringElement Imgproc/MORPH_ELLIPSE (Size. 9 9)))\n      (Imgproc/blur blurred bilat (Size. 5 5))\n      #_(Imgproc/Laplacian bilat laplacian 0 1 0.8 0.1)\n      #_(Core/addWeighted laplacian 10.0 bilat 0.8 10.0 bilat)\n\n      (Core/compare bilat (Scalar. 200.0) white-mask Core/CMP_GT)\n      (Imgproc/dilate white-mask white-mask (Imgproc/getStructuringElement Imgproc/MORPH_ELLIPSE (Size. 12 12)))\n      (Core/compare bilat (Scalar. 80.0) black-mask Core/CMP_LT)\n      #_(Imgproc/GaussianBlur blurred bilat (Size. 9 9) 20)\n      #_(Imgproc/cvtColor (ui/illuminate-correct blurred) bilat Imgproc/COLOR_BGR2GRAY)\n      #_(Imgproc/bilateralFilter blurred bilat 2 (double 10) (double 10))\n\n      (Imgproc/Canny bilat canny 50 25)\n\n      #_(q/image (util/mat-to-pimage bilat) 0 0)\n      (let [min-radius 17\n            max-radius 25]\n        (Imgproc/HoughCircles bilat circles Imgproc/CV_HOUGH_GRADIENT 1 25 50 8 min-radius max-radius)\n\n        (let [found (doall (map #(vec (.get circles 0 %)) (range (.cols circles))))]\n          #_(println \"---------------\")\n          {:bilat (util/mat-to-pimage bilat)\n           :canny (util/mat-to-pimage canny)\n           :white (doall (filter (fn [[x y]]\n                                   #_(println (first (.get ^Mat white-mask y x)))\n                                   (= 255 (int (first (.get ^Mat white-mask y x))))) found))\n           :black (doall (filter (fn [[x y]]\n                                   #_(println (first (.get ^Mat white-mask y x)))\n                                   (= 255 (int (first (.get ^Mat black-mask y x))))) found))}\n          #_{:white (filter (fn [[x y]] (= 1 (int (first (.get ^Mat white-mask y x))))) found)\n             :black (filter (fn [[x y]] (= 1 (int (first (.get ^Mat black-mask y x))))) found)}))))\n  (defn read-circle-board [samplepoints {:keys [black white] :as circles}]\n    #_(for [[y row] (map-indexed vector samplepoints)\n            [x [px py]] (map-indexed vector row)]\n        )\n    ))\n\n\n;; Some prelim work on determining the corners of a board to ease or skip initial calibration.\n\n(defn mat->lines [^Mat mat]\n  (for [x (range (.cols mat))]\n    (.get mat 0 x)))\n\n(defn theta [[x1 y1 x2 y2]]\n  (mod (Math/atan2 (- y1 y2) (- x1 x2)) Math/PI))\n\n(defn avg-theta [vs]\n  (/ (reduce #(+ %1 (nth %2 4)) 0 vs) (count vs)))\n\n(defn group-lines [avg lines]\n  (let [opp (mod (- avg (/ Math/PI 2)) Math/PI)\n        [mn mx] (sort [(mod (- avg (/ Math/PI 2)) Math/PI) avg])]\n    (group-by #(if (< mn (nth % 4) mx) avg opp) lines)))\n\n(defn line-group [[cx cy] [x1 y1 x2 y2 t :as l]]\n  (if (< (/ Math/PI 4) t (* 3 (/ Math/PI 4)))\n    [(Math/round (* t 5)) (Math/round (- x2 (/ (- y2 cy) (Math/tan t)))) cy]\n    [(Math/round (* t 5)) cx (Math/round (- y2 (* (- x2 cx) (Math/tan t))))]))\n\n\n(defn remove-outliers [[k ls]]\n  (let [avg (last (last (take (/ (count ls) 2) (sort-by #(nth % 4) ls))))]\n    [avg (filter (fn [[_ _ _ _ t]] (< (Math/abs (double (- t avg))) (/ Math/PI 9))) ls)]))\n\n\n(defn find-board [ctx]\n  (let [{{:keys [homography shift reference]} :view\n         {:keys [raw]} :camera\n         {:keys [size]} :goban} @ctx\n\n        cleaned (Mat.)\n        bilat (Mat.)\n        mask (Mat.)\n        pts2f (MatOfPoint2f.)]\n    (.copyTo raw cleaned)\n    #_(Imgproc/filter2D cleaned cleaned 1  (Mat. [1 1 1 1 -8 1 1 1 1]))\n    (Imgproc/cvtColor cleaned cleaned Imgproc/COLOR_BGR2GRAY)\n    #_(Imgproc/equalizeHist cleaned cleaned)\n    #_(Imgproc/bilateralFilter cleaned bilat 5 (double 15) (double 15))\n    (Imgproc/GaussianBlur cleaned bilat (Size. 5 5) 2)\n    (Imgproc/Laplacian bilat bilat -8 3 8 2)\n    (Imgproc/cvtColor bilat bilat Imgproc/COLOR_GRAY2BGR)\n    (Imgproc/cvtColor bilat bilat Imgproc/COLOR_BGR2HSV)\n    (Core/inRange bilat (Scalar. 0 0 100) (Scalar. 180 255 255) mask)\n\n    #_(Imgproc/Canny bilat bilat 200 50 3 true)\n    (Imgproc/HoughLinesP mask pts2f 1 (/ Math/PI 360) 100 50 10)\n    #_(println (util/write-mat pts2f))\n    (Imgproc/cvtColor bilat bilat Imgproc/COLOR_HSV2BGR)\n    #_(println (avg-theta (mat->lines pts2f)))\n    #_(println (map theta (mat->lines pts2f)))\n    #_(let [groups\n            (->>\n              (mat->lines pts2f)\n              group-lines\n              (map remove-outliers))])\n    (let [lines (map (fn [[x1 y1 x2 y2 :as k]] [x1 y1 x2 y2 (theta k)]) (mat->lines pts2f))\n          avg (avg-theta lines)]\n      #_(println \"=================================================\")\n      (swap! ctx update-in [:linedump] conj (map remove-outliers (group-lines avg lines)))\n      #_(doseq [[k ls] (map remove-outliers (group-lines avg lines))\n              [[_ gx gy] gls] (group-by (partial line-group [(/ (.cols bilat) 2) (/ (.rows bilat) 2)]) ls)]\n\n        #_(println [x1 y1 x2 y2 t])\n        #_(println g \" -- \" (count gls))\n        (let [[x1 y1 x2 y2 t] (first gls)\n              k (* k (/ 180 Math/PI))\n              l (min (* (count gls) 30) 255)]\n          (Core/line bilat (Point. x1 y1) (Point. x2 y2) (Scalar. 255 l 0) 5)\n          (Core/line bilat (Point. gx gy ) (Point. x2 y2) (Scalar. 0 0 255) 2)\n          #_(let [x (/ (/ (.rows bilat) 2) (Math/tan t))]\n              #_(println x \" = \" y2 \" / \" (Math/tan t))\n              (Core/line bilat (Point. (+ x (- x1 (/ y1 (Math/tan t)))) (/ (.rows bilat) 2)) (Point. x2 y2) (Scalar. 255 0 0) 13)\n              #_(Core/line bilat (Point. xa 0) (Point. x1 y1) (Scalar. 255 0 0) 13)))))\n\n\n    #_(doseq [[x1 y1 x2 y2] (partition 4 (:data (util/write-mat pts2f)))]\n        (Core/line bilat (Point. x1 y1) (Point. x2 y2) (Scalar. 0 255 0) 1))\n    #_(println (util/write-mat pts2f))\n    #_(doseq [p (seq (.toArray pts2f))]\n        (Core/line cropped p 2 (Scalar. 255 0 255) 3))\n    (comment\n      ;; Finding interesting points.. This doesn't lend itself too well :/\n      (Imgproc/cvtColor cropped cleaned Imgproc/COLOR_BGR2GRAY)\n      (Imgproc/bilateralFilter cleaned bilat 5 (double 15) (double 15))\n      (Imgproc/goodFeaturesToTrack bilat pts 500 0.01 (- camera/block-size 5))\n      (.fromArray pts2f (.toArray pts))\n      (Imgproc/cornerSubPix bilat pts2f (Size. 11 11) (Size. -1 -1)\n        (TermCriteria. (bit-or TermCriteria/EPS TermCriteria/COUNT) 30 0.1))\n      (doseq [p (seq (.toArray pts2f))]\n        (Core/circle cropped p 2 (Scalar. 255 0 255) 3)))\n    (swap! ctx assoc-in [:goban :flat]\n      (util/mat-to-pimage bilat nil))\n    #_(util/write-mat pts)))"
  },
  {
    "path": "src/igoki/scratch/training.clj",
    "content": "(ns igoki.scratch.training\n  (:require\n    [clojure.java.io :as io]\n    [igoki.util :as util]\n    [clojure.edn :as edn])\n  (:import\n    (java.io File FileInputStream)\n    (org.opencv.core MatOfByte MatOfPoint2f Mat Size Rect Core)\n    (org.opencv.calib3d Calib3d)\n    (org.opencv.imgproc Imgproc)\n    (org.opencv.imgcodecs Imgcodecs)))\n\n(defn file-id [^File img]\n  (let [nm (.getName img)]\n    (.substring nm 0 (- (.length nm) 4))))\n\n(defn config-file [^File img]\n  (let [pn (str (file-id img) \".edn\")]\n    (io/file (.getParentFile img) pn)))\n\n(defn config-exists? [^File img]\n  (.exists (config-file img)))\n\n(defn load-image [^File f]\n  (let [result (byte-array (.length f))]\n    (with-open [is (FileInputStream. f)]\n      (.read is result))\n    result))\n\n(defn load-raw [^File img]\n  (Imgcodecs/imdecode (MatOfByte. (load-image img)) Imgcodecs/IMREAD_UNCHANGED))\n\n(defn load-next-sample [ctx folder]\n  (let [imgfile\n        (->>\n          (.listFiles (io/file folder))\n          (filter #(.endsWith (.toLowerCase (.getName %)) \".jpg\"))\n          (remove config-exists?)\n          first)\n        _ (println imgfile)\n        raw (load-raw imgfile)\n        oldpimg (-> @ctx :camera :pimg)\n        pimg (util/mat-to-pimage raw (:bufimg oldpimg))]\n    (swap! ctx\n      #(-> %\n        (assoc :goban {:points [] :size 19}\n               :state :goban)\n        (update :camera assoc :raw raw :pimg pimg)\n        (update :training assoc :file imgfile)))\n    (println imgfile \"loaded\")))\n\n(defn save-current-sample [ctx]\n  (let [{:keys [board training view goban]} @ctx\n        config\n        {:file (.getName (:file training))\n         :board board\n         :goban goban\n         :view (select-keys view [:samplesize :samplecorners :samplepoints])}]\n    (spit (config-file (:file training)) (pr-str config))))\n\n(defn target-points [block-size size]\n  (let [extent (* block-size size)]\n    [[block-size block-size] [extent block-size] [extent extent] [block-size extent]]))\n\n(defn ref-size [block-size size]\n  (Size. (* block-size (inc size)) (* block-size (inc size))))\n\n(defn sample-points [corners size]\n  (let [[ctl ctr cbr cbl] corners\n        divide (fn [[x1 y1] [x2 y2]]\n                 (let [xf (/ (- x2 x1) (dec size))\n                       yf (/ (- y2 y1) (dec size))]\n                   (map (fn [s] [(+ x1 (* s xf)) (+ y1 (* s yf))]) (range size))))\n        leftedge (divide ctl cbl)\n        rightedge (divide ctr cbr)]\n    (map\n      (fn [left right] (divide left right))\n      leftedge rightedge)))\n\n(defn dump-points [^File imgfile sample-size flat samplepoints board id]\n  (let [rots [nil 0 1 -1]]\n    (util/with-release\n      [sample (Mat.)]\n      (doseq [[py rows] (map-indexed vector samplepoints)]\n        (doseq [[px [x y]] (map-indexed vector rows)]\n          (let [r (Rect. (- x sample-size) (- y sample-size) (* sample-size 2) (* sample-size 2))\n                p (get-in board [py px])]\n            (doseq [rotname rots]\n              (let [nm (str (.getAbsolutePath (.getParentFile imgfile)) \"/samples/\"\n                         (if p (name p) \"e\") \"/\" id \"/\" px \"-\" py \"-\" (or rotname \"O\")\".png\")]\n                (if rotname\n                  (do\n                    (Core/transpose (.submat ^Mat flat r) sample)\n                    (Core/flip sample sample rotname)\n                    (Imgcodecs/imwrite nm sample))\n                  (Imgcodecs/imwrite nm\n                    (.submat ^Mat flat r))))))))))\n  samplepoints)\n\n(defn generate-sample-points [settings ^File img]\n  (util/with-release\n    [target (MatOfPoint2f.)\n     origpoints (MatOfPoint2f.)\n     ref (Mat.)]\n    (let [{{:keys [points size]} :goban :as config} (edn/read-string (slurp (config-file img)))\n          raw (load-raw img)\n          target (util/vec->mat target (target-points (:block-size settings) size))\n          origpoints (util/vec->mat origpoints points)\n          samplecorners (target-points (:block-size settings) size)\n          samplepoints (sample-points samplecorners size)\n          homography\n          (Calib3d/findHomography ^MatOfPoint2f origpoints ^MatOfPoint2f target\n            Calib3d/FM_RANSAC 3.0)]\n      (Imgproc/warpPerspective raw ref homography (ref-size (:block-size settings) size))\n      (dump-points img (:sample-size settings) ref samplepoints (:board config) (.getName img)))))\n\n(defn generate-all-samples [settings folder]\n  (let [f (io/file folder)\n        imgs\n        (->>\n          (.listFiles f)\n          (filter #(.endsWith (.toLowerCase (.getName %)) \".jpg\"))\n          (filter config-exists?))]\n    (.mkdirs (File. f \"samples/b\"))\n    (.mkdirs (File. f \"samples/e\"))\n    (.mkdirs (File. f \"samples/w\"))\n\n    (doseq [i imgs]\n      (.mkdirs (File. f (str \"samples/b/\" (.getName i))))\n      (.mkdirs (File. f (str \"samples/e/\" (.getName i))))\n      (.mkdirs (File. f (str \"samples/w/\" (.getName i))))\n      (println \"Processing : \" (.getAbsolutePath i))\n      (generate-sample-points settings i))))\n\n(comment\n  (ui/stop-read-loop igoki.core/ctx)\n\n  (load-next-sample igoki.core/ctx \"resources/samples/testing\")\n  (let [c (swap! igoki.core/ctx camera/read-board)] :done)\n  (save-current-sample igoki.core/ctx)\n  (generate-all-samples {:block-size 10 :sample-size 5} \"resources/samples/training\"))"
  },
  {
    "path": "src/igoki/sgf.clj",
    "content": "(ns igoki.sgf\n  (:require\n    [igoki.util :as util]\n    [clojure.set :as set]))\n\n;; According to http://www.red-bean.com/sgf/properties.html\n(def property-lookup\n  {\n   ;; Moves\n   \"B\"  :black\n   \"KO\" :ko\n   \"MN\" :move-number\n   \"W\"  :white\n\n   ;; Setup\n   \"AB\" :add-black\n   \"AE\" :add-erase\n   \"AW\" :add-white\n   \"PL\" :player-start\n\n   ;; Annotation\n   \"C\"  :comment\n   \"DM\" :position-even\n   \"GB\" :position-bias-black\n   \"GW\" :position-bias-white\n   \"HO\" :position-hotspot\n   \"N\"  :name\n   \"UC\" :position-unclear\n   \"V\"  :value-to-white                                     ; Negative is good for black\n\n   ;; Move annotation\n   \"BM\" :move-bad\n   \"DO\" :move-doubtful\n   \"IT\" :move-interesting\n   \"TE\" :move-tesuji\n\n   ;; Markup\n   \"AR\" :arrow                                              ; 'from:to' in value\n   \"CR\" :circle\n   \"DD\" :grayout\n   \"LB\" :label                                              ; 'point:label'\n   \"LN\" :line                                               ; 'from:to'\n   \"MA\" :mark                                               ; with X\n   \"SL\" :selected\n   \"SQ\" :square\n   \"TR\" :triangle\n   \"TB\" :territory-black\n   \"TW\" :territory-white\n\n   ;; Root info\n   \"AP\" :application\n   \"CA\" :charset\n   \"FF\" :file-format\n   \"GM\" :gametype                                           ; 1=Go\n   \"ST\" :variation-show-type\n   \"SZ\" :size\n\n   ;; Game info\n   \"AN\" :annotator\n   \"BR\" :black-rank\n   \"BT\" :black-team\n   \"CP\" :copyright\n   \"DT\" :date\n   \"EV\" :event\n   \"GN\" :game-name\n   \"GC\" :game-comment\n   \"ON\" :opening\n   \"OT\" :overtime-method\n   \"PB\" :black-name\n   \"PC\" :place\n   \"PW\" :white-name\n   \"RE\" :result                                             ; [WB][+]([RTF](esign|ime|orfeit)?)?, 0, Draw, Void, '?'\n   \"RO\" :round\n   \"RU\" :rules\n   \"SO\" :source\n   \"US\" :user\n   \"WR\" :white-rank\n   \"WT\" :white-team\n   \"HA\" :handicap\n   \"KM\" :komi\n\n   ;; Timing\n   \"BL\" :black-time-left\n   \"OB\" :black-moves-left\n   \"OW\" :white-moves-left\n   \"WL\" :white-time-left\n\n   ;; Miscellaneous\n   \"FG\" :print-figure\n   \"PM\" :print-move-numbers\n   \"VW\" :view}                                              ; show only listed points or reset if empty\n  )\n\n(def handicap-placement\n  {2 [\"pd\" \"dp\"]\n   3 [\"pd\" \"dp\" \"pp\"]\n   4 [\"pd\" \"dp\" \"pp\" \"dd\"]\n   5 [\"pd\" \"dp\" \"pp\" \"dd\" \"jj\"]\n   6 [\"pd\" \"dp\" \"pp\" \"dd\" \"dj\" \"pj\"]\n   7 [\"pd\" \"dp\" \"pp\" \"dd\" \"jj\" \"dj\" \"pj\"]\n   8 [\"pd\" \"dp\" \"pp\" \"dd\" \"dj\" \"pj\" \"jd\" \"jp\"]\n   9 [\"pd\" \"dp\" \"pp\" \"dd\" \"dj\" \"pj\" \"jd\" \"jp\" \"jj\"]})\n\n(def reverse-property-lookup\n  (into {} (map (fn [[k v]] [v k]) property-lookup)))\n\n(defn convert-coord [x y]\n  (if (or (neg? x) (neg? y))\n    \"\"\n    (str (char (+ 97 x)) (char (+ 97 y)))))\n\n(defn convert-sgf-coord [[x y :as s]]\n  (when (and x y)\n    [(- (int x) 97) (- (int y) 97)]))\n\n(defn inpath [branch-path]\n  (concat [:branches] (mapcat (fn [i] [i :branches]) (mapcat identity branch-path))))\n\n(defn current-branch-node-list [path rootnode]\n  (reductions\n    (fn [node p]\n      (get (:branches node) p))\n    rootnode (mapcat identity path)))\n\n(defn accumulate-action [{:keys [action text node] :as state}]\n  (let [a (get property-lookup action action)]\n    (-> state\n        (update-in [:node a] #(conj (or % []) text))\n        (assoc :mode :collected))))\n\n(defn collect-text [state c]\n  (update state :text str c))\n\n(defn collect-action [state c]\n  (if (= (:mode state) :collected)\n    (assoc (dissoc state :mode) :action (str c))\n    (update state :action str c)))\n\n(defn find-existing-branch [branches {:keys [black white]}]\n  (->>\n    (map-indexed vector branches)\n    (filter (fn [[_ n]] (or (and black (= (:black n) black))\n                            (and white (= (:white n) white)))))\n    first))\n\n(defn collect-node [game node branch-path & [branch?]]\n  (let [branches (get-in game (inpath branch-path))\n        [idx branch] (find-existing-branch branches node)]\n    (cond\n      (empty? node)\n      [game (if branch? (conj branch-path []) branch-path)]\n\n      idx\n      [(util/iupdate-in game (conj (inpath branch-path) idx) merge node)\n       (cond->\n         branch-path true (update (dec (count branch-path)) conj idx)\n         branch? (conj []))]\n\n      :else\n      [(util/iupdate-in game (inpath branch-path) (fnil conj []) node)\n       (let [newpoint (count (get-in game (inpath branch-path)))]\n         (cond->\n           branch-path\n           true (update (dec (count branch-path)) conj newpoint)\n           branch? (conj [])))])))\n\n(defn read-sgf [sgf-string]\n  (let [new-node {:branches []}\n        initial-state {:mode nil :action \"\" :node nil}]\n    (loop [[game branch-path :as g] [new-node []]\n           state initial-state\n           [c & o] sgf-string]\n      (cond\n        (nil? c)\n        ;; TODO: This picks the first branch in an sgf as the root, might need to show a list\n        ;; of games instead if the SGF actually has multiple root branches.\n        (first (:branches game))\n\n        (and (= c \\\\) (= (:mode state) :collect-text))\n        (recur g state o) ;; Escape character in text.\n\n        (= c \\])\n        (recur g (accumulate-action state) o)\n\n        (= (:mode state) :collect-text)\n        (recur g (collect-text state c) o)\n\n        (= c \\[)\n        (recur g (assoc state :mode :collect-text :text \"\") o)\n\n        (Character/isUpperCase ^char c)\n        (recur g (collect-action state c) o)\n\n        (= c \\;)\n        (do\n          (recur (collect-node game (:node state) branch-path) initial-state o))\n\n        (= c \\()\n        (recur (collect-node game (:node state) branch-path true) initial-state o)\n\n        (= c \\))\n        (let [[g np] (collect-node game (:node state) branch-path)]\n          (recur [g (vec (butlast np))] initial-state o))\n        :else\n        (recur g state o)\n        ))))\n\n(defn node-to-sgf [node]\n  (let [nodestr\n        (apply str\n               (mapcat\n                 (fn [[k v]]\n                   (str\n                     (get reverse-property-lookup k k)\n                     \"[\" (apply str (interpose \"][\" v)) \"]\"))\n                 (dissoc node :branches)))]\n    (str\n      \";\" nodestr\n      (cond\n        (nil? (:branches node)) \"\"\n        (> (count (:branches node)) 1) (str \"(\" (apply str (interpose \")(\" (map node-to-sgf (:branches node)))) \")\")\n        :else (node-to-sgf (first (:branches node)))))))\n\n(defn sgf [root]\n  (str \"(\" (node-to-sgf root) \")\"))\n\n\n;; == Board construction ==\n(defmulti step-action\n  (fn [b k v]\n    (cond\n      (#{:annotator :black-rank :black-team :copyright :date :event :game-name :game-comment\n         :opening :overtime-method :black-name :place :white-name :result :round :rules :source\n         :user :white-rank :white-team :handicap :komi :application :charset :file-format :gametype\n         :variation-show-type :comment :name :value-to-white} k)\n      :annotate-game\n      (#{:circle :mark :selected :square :triangle :territory-black :territory-white} k)\n      :markup\n      (#{:arrow :line} k)\n      :line\n      (#{:move-bad :move-doubtful :move-interesting :move-tesuji} k)\n      :movequality\n      (#{:position-even :position-bias-black :position-bias-white :position-hotspot :position-unclear} k)\n      :positionquality\n      :else k)))\n\n(defmethod step-action :default [b k v]\n  #_(println \"Unhandled property: \" k)\n  b)\n\n;; Annotations and metadata guck\n(defn rectangle-point-list [v]\n  (let [[[fx fy :as fp] tp] (.split v \":\")\n        [tx ty] (or tp fp)]\n    (for [x (range (int fx) (inc (int tx)))\n          y (range (int fy) (inc (int ty)))]\n      (str (char x) (char y)))))\n\n(defmethod step-action :annotate-game [b k v]\n  (assoc b k v))\n\n(defmethod step-action :move-number [b k v]\n  (assoc b :move-offset (Integer/parseInt v)))\n\n(defmethod step-action :markup [b k v]\n  (update b :annotations\n          concat (map (fn [sv] {:type k :point sv}) (rectangle-point-list v))))\n\n(defmethod step-action :line [b k v]\n  (let [[f t] (seq (.split (str v) \":\"))]\n    (update b :annotations conj {:type k :from f :to t})))\n\n(defmethod step-action :label [b k v]\n  (let [[p l] (seq (.split (str v) \":\"))]\n    (update b :annotations conj {:type k :point p :text l})))\n\n(defmethod step-action :movequality [b k v]\n  (assoc b :movequality [k v]))\n\n(defmethod step-action :positionquality [b k v]\n  (assoc b :positionquality [k v]))\n\n(defmethod step-action :size [b k v]\n  (let [[width height] (map #(Integer/parseInt %) (seq (.split (str v) \":\")))\n        height (or height width)]\n    (assoc b :size [width height])))\n\n(defmethod step-action :player-start [b k v]\n  (let [v (if (= v \"W\") :white :black)]\n    (assoc b\n      :player-start v\n      :player-turn v)))\n\n;; Onto actual interesting board related stuff.\n(defn set-color [color b k v]\n  ;; Does not consider board size, simply adds it since the board size might be changed.\n  (reduce #(assoc-in %1 [:board %2 :stone] color) b (rectangle-point-list v)))\n\n(defmethod step-action :add-black [b k v]\n  (set-color :black b k v))\n\n(defmethod step-action :add-white [b k v]\n  (set-color :white b k v))\n\n(defmethod step-action :add-erase [b k v]\n  (set-color nil b k v))\n\n(defn inside-point? [{[width height] :size :as board} p]\n  (let [a (int \\a)\n        [x y] (map int p)]\n    (and\n      (= (count p) 2)\n      (>= x a) (< x (+ a width))\n      (>= y a) (< y (+ a height)))))\n\n(defn neighbour-points\n  \"Return a set of neighbour coords for a given point on a board, taking edges and corners into account\"\n  [board p]\n  (when p\n    (let [[x y] (map int p)]\n      (->>\n        [[(dec x) y] [(inc x) y] [x (dec y)] [x (inc y)]]\n        (filter (partial inside-point? board))\n        (map (fn [[x y]] (str (char x) (char y))))\n        set))))\n\n(defn find-group [board point]\n  (let [color (get-in board [:board point :stone])]\n    (loop [[p & po] [point]\n           group #{}]\n      (let [neighbours (set/difference (neighbour-points board p) group)]\n        (cond\n          (nil? p) group\n          (= color (get-in board [:board p :stone]))\n          (recur (concat po neighbours) (conj group p))\n          :else\n          (recur po group))))))\n\n(defn count-liberties [board group]\n  (let [neighbours (set/difference (set (mapcat (partial neighbour-points board) group)) group)\n        liberties (filter nil? (map #(get-in board [:board % :stone]) neighbours))]\n    (count liberties)))\n\n(defn group-alive? [board group]\n  (pos? (count-liberties board group)))\n\n(defn remove-captured-group [board group]\n  (let [color (get-in board [:board (first group) :stone])]\n    (->\n      (reduce #(assoc-in %1 [:board %2 :stone] nil) board group)\n      (update-in [:captures (if (= color :white) :black :white)] (fnil + 0) (count group)))))\n\n(defn check-capture-around [board color point]\n  (let [opp-color (if (= color :white) :black :white)\n        neighbours (neighbour-points board point)\n        opp-points (filter #(= opp-color (get-in board [:board % :stone])) neighbours)\n        captured-groups (remove (partial group-alive? board) (set (map (partial find-group board) opp-points)))]\n    (reduce remove-captured-group board captured-groups)))\n\n(defn check-suicide [board point]\n  (let [group (find-group board point)]\n    (if (group-alive? board group)\n      board\n      (remove-captured-group board group))))\n\n(defn place-stone [board color point]\n  ;; If the stone is 'placed' outside the board (either '' or 'tt') then count as a 'pass'\n  (if (inside-point? board point)\n    (-> board\n        (assoc-in [:board point :stone] color)\n        (assoc-in [:board point :movenumber] (:movenumber board))\n        (check-capture-around color point)\n        (check-suicide point))\n    (assoc board :player-passed color)))\n\n(defn move [board color point]\n  (-> board\n      (assoc :player-turn (if (= color :white) :black :white))\n      (place-stone color point)\n      (update :movenumber (fnil inc 0))))\n\n(defmethod step-action :black [b k v]\n  (move b :black v))\n\n(defmethod step-action :white [b k v]\n  (move b :white v))\n\n(defmethod step-action :branches [b k v]\n  b)\n\n;; Tying it all together.\n\n(defn step-node [board node]\n  (reduce\n    (fn [b [k v]]\n      (reduce #(step-action %1 k %2) b v))\n    (dissoc\n      board\n      :player-passed\n      :name\n      :comment\n      :movequality\n      :positionquality\n      :annotations\n      :value-to-white)\n    node))\n\n(defn construct-board [rootnode path]\n  (let [nodelist (current-branch-node-list path rootnode)]\n    (reduce step-node {:size [19 19] :player-turn :black :movenumber 0 :moveoffset 0} nodelist)))"
  },
  {
    "path": "src/igoki/simulated.clj",
    "content": "(ns igoki.simulated\n  (:require\n    [igoki.litequil :as lq]\n    [igoki.util :as util]\n    [igoki.ui.util :as ui.util]\n    [seesaw.core :as s]\n    [clojure.java.io :as io])\n  (:import\n    (java.io File)\n    (org.opencv.core Mat Point Scalar MatOfPoint MatOfByte)\n    (de.schlichtherle.truezip.fs FsEntryNotFoundException)\n    (java.awt.event MouseEvent)\n    (org.opencv.imgproc Imgproc)\n    (org.opencv.imgcodecs Imgcodecs)\n    (javax.swing JComboBox)))\n\n;; This view simulates a camera for testing igoki's behaviour without having a board and camera handy\n(defonce simctx (atom {:sketchconfig {:framerate 5 :size [640 480]}}))\n\n(defn blank-board [size]\n  (vec\n    (for [y (range size)]\n      (vec (for [x (range size)] nil)))))\n\n(defn stone-point [[mx my] grid-start cell-size]\n  [(int (/ (+ (- mx grid-start) (/ cell-size 2)) cell-size))\n   (int (/ (+ (- my grid-start) (/ cell-size 2)) cell-size))])\n\n(defn grid-spec [m]\n  (let [size (or (-> @simctx :sim :size) 19)\n        cellsize (max (/ (.rows m) (+ size 2)) 25)\n        grid-start (+ cellsize (/ cellsize 2))]\n    [cellsize grid-start]))\n\n(defn reset-board [ctx size]\n  (swap! ctx update :sim assoc :size size :board (blank-board size) :next :b :mode :alt))\n\n(defn stone-colors [c]\n  (if (= c :w)\n    [(Scalar. 255 255 255) (Scalar. 28 28 28)]\n    [(Scalar. 28 28 28) (Scalar. 0 0 0)]))\n\n(defn draw-stone [m x y c cellsize]\n  (when c\n    (let [[incolor bcolor] (stone-colors c)]\n      (Imgproc/circle m (Point. x y) (/ cellsize 4) incolor (/ cellsize 2))\n      (Imgproc/circle m (Point. x y) (/ cellsize 2) bcolor 2))))\n\n(defn draw-board [^Mat m]\n  (try\n    (when (and m (pos? (.rows m)))\n      #_(.setTo m (Scalar. 92 179 220))\n      (let [{{:keys [size board next]} :sim} @simctx\n            [cellsize grid-start] (grid-spec m)\n            mpos (lq/mouse-position)\n            [mx my]\n            (if mpos\n              [(* (/ (float (.getX mpos)) (lq/width)) (.width m))\n               (* (/ (float (.getY mpos)) (lq/height)) (.height m))]\n              [2000 2000])]\n\n        (doseq [x (range size)]\n          (let [coord (+ grid-start (* x cellsize))\n                extent (+ grid-start (* cellsize (dec size)))]\n\n            (Imgproc/line m (Point. coord grid-start) (Point. coord extent) (Scalar. 0 0 0))\n            (Imgproc/line m (Point. grid-start coord) (Point. extent coord) (Scalar. 0 0 0))))\n\n\n        (doseq [[x y] (util/star-points size)]\n          (Imgproc/circle m (Point. (+ grid-start (* x cellsize))\n                                 (+ grid-start (* y cellsize))) 2 (Scalar. 0 0 0) 2))\n\n        (doseq [[y rows] (map-indexed vector board)\n                [x v] (map-indexed vector rows)]\n          (when v\n            (draw-stone m (+ grid-start (* cellsize x)) (+ grid-start (* cellsize y)) v cellsize)))\n\n        (let [[x y] (stone-point [mx my] grid-start cellsize)]\n          (Imgproc/circle m (Point. (+ grid-start (* x cellsize))\n                                 (+ grid-start (* y cellsize))) (/ cellsize 2) (Scalar. 0 0 255) 1))\n\n        (draw-stone m mx my (-> @simctx :sim :next) cellsize)\n\n        (util/with-release [pts (MatOfPoint.)]\n          (util/vec->mat pts (map (fn [[x y]] [(+ (or mx 100) x) (+ (or my 100) y)]) [[0 0] [120 0] [120 55] [200 200] [55 120] [0 120] [0 0]]))\n          (Imgproc/fillPoly m [pts] (Scalar. 96 90 29)))))\n\n    (catch Exception e\n      (.printStackTrace e)))\n  m)\n\n(defn simulate []\n  (let [context @simctx]\n    (cond\n      (= (:mode context) :replay)\n      (when (-> context :camera :raw)\n        (swap! simctx\n          update\n          :camera assoc\n          :raw (-> context :camera :raw)\n          :pimg\n          (util/mat-to-pimage (-> context :camera :raw)\n            (-> context :camera :pimg :bufimg))))\n\n      :else\n      (when (-> context :sim :background)\n        (let [m (.clone (-> context :sim :background))]\n          (draw-board m)\n          (swap! simctx\n            update :camera assoc\n            :raw m\n            :pimg\n            (util/mat-to-pimage m\n              (-> context :camera :pimg :bufimg))))))))\n\n\n(defn next-stone [{:keys [next mode]}]\n  (case mode\n    :black :b\n    :white :w\n    :erase nil\n    (if (= next :w) :b :w)))\n\n(defn mouse-pressed [ctx ^MouseEvent e]\n  (swap! ctx\n    (fn [{{:keys [raw]} :camera :keys [sim] :as c}]\n      (let [[cs gs] (grid-spec raw)\n            mpos (lq/mouse-position)\n            [px py]\n            (stone-point\n              [(* (/ (float (.getX mpos)) (lq/width)) (.width raw))\n               (* (/ (float (.getY mpos)) (lq/height)) (.height raw))] gs cs)\n            current (get-in sim [:board py px] :outside)]\n        (println \"[cs gs]\" [cs gs])\n        (println \"[px py]\" [px py])\n        (println \"current\" current)\n        (cond\n          (= current :outside) c\n          :else\n          (-> c\n              (assoc-in [:sim :board py px] (:next sim))\n              (assoc-in [:sim :next] (next-stone sim))))))))\n\n(defn alternate-mode [{:keys [next] :as sim}]\n  (assoc sim :mode :alt :next (if (= next :w) :b :w)))\n\n(defn set-mode [sim c]\n  (assoc sim :mode c :next (case c :white :w :black :b nil)))\n\n(defn step-file-index [ctx nextfn]\n  (try\n    (let [{{:keys [file index]} :replay\n           {:keys [pimg]} :camera} @ctx\n          nextindex (nextfn index)\n          image (util/zip-read-file file (str nextindex \".jpg\"))\n          raw (Imgcodecs/imdecode (MatOfByte. image) Imgcodecs/IMREAD_UNCHANGED)\n          pimg (util/mat-to-pimage raw (:bufimg pimg))]\n      (swap!\n        ctx\n        (fn [c]\n          (-> c\n              (update :camera assoc :raw raw :pimg pimg)\n              (update :replay assoc :index nextindex)))))\n    (catch FsEntryNotFoundException e (.printStackTrace e))))\n\n(defn load-zip [ctx file]\n  (println \"Loading:\" file)\n  (swap! ctx assoc :mode :replay :replay {:file file :index 0})\n  (step-file-index ctx identity))\n\n(defn load-img [ctx file]\n  (swap! ctx assoc :mode :replay :replay {:file file :index 0})\n  (step-file-index ctx identity))\n\n(defn set-board-size [ctx size]\n  (swap! ctx assoc-in [:sim :size] size))\n\n(defn key-pressed [simctx e]\n  ;; TODO: Add these are buttons on the panel.\n  (case (lq/key-code e)\n    ;; Left\n    37\n    ;; Right\n    39 (step-file-index simctx inc)\n    ;; L\n    76 (ui.util/load-dialog #(load-zip simctx (.getAbsolutePath %)) (str (System/getProperty \"user.dir\") \"/capture\"))\n    (println \"Unhandled key-down: \" (lq/key-code e))))\n\n\n\n(defn setup [ctx]\n  (lq/smooth)\n  (lq/frame-rate 20)\n  (lq/background 200))\n\n(defn paint [ctx]\n  (simulate)\n  (let [{{:keys [^Mat raw pimg]} :camera\n         {:keys [frame index]} :replay\n         :keys [stopped mode]} @ctx\n        [cellsize grid-start] (if raw (grid-spec raw) [])\n        tx (- (lq/width) 180)]\n    (lq/background 128 64 78)\n    (lq/rect 0 0 (lq/width) (lq/height))\n    (cond\n      stopped\n      (lq/shadow-text \"Select 'simulation' camera...\" 20 35)\n\n      (nil? pimg)\n      (lq/shadow-text \"Image not built yet, please wait...\" 10 25)\n\n      (= mode :replay)\n      (do\n        (lq/image (:bufimg pimg) 0 0 (lq/width) (lq/height))\n        (lq/shadow-text (str \"Frame \" index) 10 25))\n\n      :else\n      (lq/image (:bufimg pimg) 0 0 (lq/width) (lq/height)))))\n\n(defn start-simulation [ctx]\n  (try\n    ;; This tmp song and dance because Imgcodes takes a filename string :'/\n    (let [tmp (File/createTempFile \"background\" \"jpg\")]\n      (io/copy (io/input-stream (io/resource \"wooden-background.jpg\")) tmp)\n\n      (swap! simctx\n        (fn [s]\n          (->\n            s\n            (assoc :stopped false)\n            (update :sim assoc :background (Imgcodecs/imread (.getAbsolutePath tmp)))))))\n    (catch Exception e\n      (.printStackTrace e)))\n\n  (reset-board simctx 19)\n\n  (doto\n    (Thread.\n      #(when-not (-> @simctx :stopped)\n        (let [{{:keys [raw pimg]} :camera} @simctx]\n          (swap! ctx update :camera assoc :raw raw :pimg pimg)\n          (Thread/sleep (or (-> @ctx :camera :read-delay) 500))\n          (recur))))\n    (.setDaemon true)\n    (.start)))\n\n(defn set-board-mode [mode]\n  (case mode\n    \"Alternating\"\n    (swap! simctx update :sim alternate-mode)\n\n    \"Black\"\n    (swap! simctx update :sim set-mode :black)\n\n    \"White\"\n    (swap! simctx update :sim set-mode :white)\n\n    \"Clear\"\n    (swap! simctx update :sim set-mode :erase)))\n\n(defn button-panel [simctx]\n  (let [mode (:mode @simctx)\n\n        panel\n        (s/flow-panel\n          :hgap 15\n          :items\n          (cond\n            (= mode :replay)\n            [(s/button\n               :id :sim-back\n               :text \"Back to Simulation\")\n\n             [20 :by 10]\n             (s/button\n               :id :sim-zip-left\n               :text \"<\"\n               :listen\n               [:action\n                (fn [e]\n                  (step-file-index simctx dec))])\n\n             [10 :by 10]\n             (s/button\n               :id :sim-zip-right\n               :text \">\"\n               :listen\n               [:action\n                (fn [e]\n                  (step-file-index simctx inc))])]\n            :else\n            [\"Size: \"\n             (s/combobox\n               :listen\n               [:action\n                (fn [e]\n                  (let [sel (.getSelectedIndex ^JComboBox (.getSource e))]\n                    (set-board-size simctx (nth [9 13 19] sel))))]\n               :model [\"9x9\" \"13x13\" \"19x19\"]\n               :selected-index 2)\n             [20 :by 10]\n             \"Place Mode: \"\n             (s/combobox\n               :listen\n               [:action\n                (fn [e]\n                  (set-board-mode (s/value (.getSource e))))]\n               :model [\"Alternating\" \"Black\" \"White\" \"Clear\"])\n\n             [20 :by 10]\n             (s/button\n               :text \"Reset\"\n               :listen\n               [:action\n                (fn [e]\n                  (when\n                    (s/confirm \"Are you sure?\" :confirm-type :yes-no)\n                    (reset-board simctx (-> @simctx :sim :size))))])\n             [40 :by 10]\n             (s/button\n               :id :sim-load-zip\n               :text \"Load Captured ZIP\")]))]\n\n    (cond\n      (= mode :replay)\n      (s/listen (s/select panel [:#sim-back])\n        :action\n        (fn [e]\n          (swap! simctx\n            (fn [c]\n              (->\n                c\n                (dissoc :replay)\n                (assoc :mode :alt))))\n          (s/replace! (.getParent panel) panel (button-panel simctx))))\n\n      :else\n      (s/listen (s/select panel [:#sim-load-zip])\n        :action\n        (fn [e]\n          (ui.util/load-dialog\n            (fn [f]\n              (load-zip simctx (.getAbsolutePath f))\n              (s/replace! (.getParent panel) panel (button-panel simctx)))\n            (str (System/getProperty \"user.dir\") \"/capture\")))))\n    panel))\n\n(defn simulation-panel [ctx]\n  (let [sketch\n        (lq/sketch-panel\n          {:draw (partial #'paint simctx)\n           :setup (partial #'setup simctx)\n           :mouse-pressed (partial #'mouse-pressed simctx)\n           :key-pressed (partial #'key-pressed simctx)})\n\n        buttons (button-panel simctx)\n        ]\n    (swap! simctx assoc :sketch sketch)\n    (s/border-panel\n      :center (:panel sketch)\n      :south buttons)))\n\n(defn stop []\n  (swap! simctx assoc :stopped true))\n"
  },
  {
    "path": "src/igoki/sound/announce.clj",
    "content": "(ns igoki.sound.announce\n  (:require\n    [clojure.string :as str]\n    [igoki.sgf :as sgf]\n    [clojure.java.io :as io]\n    [igoki.sound.sound :as snd])\n  (:import (java.util.concurrent ThreadPoolExecutor TimeUnit LinkedBlockingQueue)))\n\n;; Japanese words generated via google translate\n;; Enlglish words generated via https://www.naturalreaders.com/online/\n\n(comment\n  ;; Words..\n  \"\n  1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,\n  Ichi, Ni, San, Yon, Go, Roku, Nana, Hachi, Kyu, Ju,\n  Ju-ichi, Ju-ni, Ju-san, Ju-yon, Ju-go, Ju-roku, Ju-nana, Ju-hachi, Ju-kyu\n  Agohimo, Akisankaku, Atari, Atekomi, Boshi, Botsugi,\n  Daidaigeima, Dan, Dango, Degiri, Fukure, Geta, Goken-biraki,\n  Gote, Gote no sente, Gyaku komi, Hamete, Hana zuke, Hane,\n  Hane-dashi, Hane-komi, Hara-zuke, Hasami, Hasami tsuke,\n  Hazama tobi, Hekomi, Hiraki, Horikomi, Ikken tobi,\n  Joseki, Kakari, Kaketsugi, Kannon-biraki, Kuruma no ato-oshi,\n  Katatsuki, Kiri, Komi, Kosumi, Kosumi-dashi, Kosumi-tsuke,\n  Kyu, Moku, Mokai komoku, Kenka Komoku, Nageru, Hanekaeshi,\n  Nidan-bane, Niken biraki, Niken tobi, Nigiri, Nirensei,\n  Nobi, Ogeima, Onadare, Owari, Ponnuki, Ryojimari, Ryoatari,\n  Sagari, Sangen biraki, Sanrensei, Sente, Sente no gote,\n  Shico, Shico-atari, Shimari, Kuro, Shiro, Shodan, Suberi,\n  Susaoki, Susogakari, Tagai sen, Taisha, Takefu, Te, Teai, Tenuki, Tesuji, Tobi,\n  Tetchu, Tsuke, Tsuke-koshi, Tsuki, Tsume-biraki, Warikomi,\n  Wariuchi, Komoku, Hoshi, Sansan, Mokuhazushi, Takamoku, Oomokuhazushi,\n  Ootakamoku, Gonogo, Tengen, Hoshishita, Hoshiwaki, NiNoIchi,\n  Hidariue, migiue, hidarishita, migishita, keima kakari,\n  ikken takagakari, ogeima takagakari, niken takagakari\"\n  )\n\n(def sound-executor\n  (ThreadPoolExecutor. 1 1 60 TimeUnit/MINUTES (LinkedBlockingQueue. 24)))\n\n(defn announce [lang parts]\n  (println \"Announce: \" parts)\n  (.submit sound-executor\n    (fn []\n      (doseq [p (remove #(or (nil? %) (str/blank? %)) parts)]\n        (case p\n          \",\" (Thread/sleep 250)\n          \"-\" (Thread/sleep 500)\n          (snd/sound (str \"public/sounds/\" (name (or lang :en)) \"/\" p \".wav\")))))))\n\n(def soundmapping\n  {:en\n   {:players {:white \"white\" :black \"black\"}\n    :coords\n    {:join nil\n     :x\n     (->>\n       (range 1 20)\n       (map\n         (fn [i]\n           (let [c (char (+ 64 i (if (> i 8) 1 0)))]\n             [i (str c)])))\n       (into {}))\n\n     :y\n     (->>\n       (range 1 20)\n       (map\n         (fn [i] [i (str \"post\" i)]))\n       (into {}))}}\n   :jp\n   {:players {:white \"Shiro\" :black \"Kuro\"}\n    :coords\n    {:join \"no\"\n     :x\n     {1 \"Ichi\" 2 \"Ni\" 3 \"San\" 4 \"Yon\" 5 \"Go\" 6 \"Roku\" 7 \"Nana\" 8 \"Hachi\" 9 \"Kyu\" 10 \"Ju\"\n      11 \"Ju-ichi\" 12 \"Ju-ni\" 13 \"Ju-san\" 14 \"Ju-yon\" 15 \"Ju-go\" 16 \"Ju-roku\"\n      17 \"Ju-nana\" 18 \"Ju-hachi\" 19 \"Ju-kyu\"}\n\n     :y\n     {1 \"Ichi\" 2 \"Ni\" 3 \"San\" 4 \"Yon\" 5 \"Go\" 6 \"Roku\" 7 \"Nana\" 8 \"Hachi\" 9 \"Kyu\" 10 \"Ju\"\n      11 \"Ju-ichi\" 12 \"Ju-ni\" 13 \"Ju-san\" 14 \"Ju-yon\" 15 \"Ju-go\" 16 \"Ju-roku\"\n      17 \"Ju-nana\" 18 \"Ju-hachi\" 19 \"Ju-kyu\"}}}})\n\n(def named-points\n  {[3 3]   [\"Hidariue\" \"Sansan\"]\n   [3 16]  [\"Hidarishita\" \"Sansan\"]\n   [16 3]  [\"Migiue\" \"Sansan\"]\n   [16 16] [\"Migishita\" \"Sansan\"]\n   [10 10] [\"Tengen\"]})\n\n(def opening-point\n  {[3 3] \"Sansan\"\n   [4 4] \"Hoshi\"\n   [3 4] \"Komoku\"\n   [3 5] \"Mokuhazushi\"\n   [4 5] \"Takamoku\"\n   [3 6] \"Oomokuhazushi\"\n   [4 6] \"Ootakamoku\"\n   [5 5] \"Gonogo\"\n   [3 9] \"Hoshishita\"\n   [3 10] \"Hoshiwaki\"\n   [1 2] \"NiNoIchi\"})\n\n(defn normalize [[x y]]\n  (sort\n    [(- 10 (Math/abs (int (- x 10))))\n     (- 10 (Math/abs (int (- y 10))))]))\n\n(defn board-area [[x y]]\n  (if (or (= x 10) (= y 10))\n    nil\n    (if (< x 10)\n      (if (< y 10)\n        \"Hidariue\"\n        \"Hidarishita\")\n      (if (< y 10)\n        \"Migiue\"\n        \"Migishita\"))))\n\n(defn lookup-sound [language path]\n  (get-in soundmapping (concat [language] path)))\n\n(defn comment-move [ctx node board]\n  (let [{:keys [player language] :or {language :en}} (:announce @ctx)\n        {:keys [black white]} node\n        moves (or black white)\n        position (first moves)\n        [x y :as p] (map inc (sgf/convert-sgf-coord position))\n        #_#_named (named-points p)\n        #_#_opening [(board-area p) (opening-point (normalize p))]]\n\n    (when\n      (and\n        ;; There shouldn't be black _and_ white moves to announce, else we'll just bombard\n        (not (and black white))\n\n        ;; There should also only be one move to announce, else, again, we'll bombard.\n        (= 1 (count moves))\n\n        ;; And the user should have requested which players to announce, specifically.\n        (or\n          (and white (:white player))\n          (and black (:black player))))\n\n      (announce\n        language\n        (concat\n          (when (> (count player) 1)\n            [(lookup-sound language [:players (if black :black :white)])\n             \",\"])\n          [(lookup-sound language [:coords :x x])\n           (lookup-sound language [:coords :join])\n           (lookup-sound language [:coords :y y])] #_(or named opening))))))\n\n(defn set-announce-player [ctx player]\n  (swap! ctx assoc-in [:announce :player]\n    (case player\n      :black #{:black}\n      :white #{:white}\n      :both #{:black :white}\n      #{})))\n\n(defn set-announce-language [ctx langkey]\n  (swap! ctx assoc-in [:announce :language] langkey))\n\n"
  },
  {
    "path": "src/igoki/sound/sound.clj",
    "content": "(ns igoki.sound.sound\n  (:import\n    (javax.sound.sampled AudioSystem LineListener LineEvent LineEvent$Type)\n    (java.util.concurrent CountDownLatch)))\n\n(def get-clip\n  (memoize\n    (fn [file]\n      #_(println \"Loading:\" file)\n      (let [ais  (AudioSystem/getAudioInputStream (ClassLoader/getSystemResource file))\n            clip (AudioSystem/getClip)]\n        (.open clip ais)\n        clip))))\n\n(defn sound [file]\n  (try\n    (let [clip (get-clip file)\n          latch (CountDownLatch. 1)\n          listener\n          (proxy [LineListener] []\n            (update [^LineEvent e]\n              (when (= (.getType e) LineEvent$Type/STOP)\n                (.countDown latch))))]\n      (.addLineListener clip listener)\n      (.setFramePosition clip 0)\n      (.start clip)\n      (.await latch)\n      (.removeLineListener clip listener))\n    (catch Exception e)))\n\n(def sounds\n  {:click  \"public/sounds/click.wav\"\n   :undo   \"public/sounds/back.wav\"\n   :submit \"public/sounds/submit.wav\"})\n\n(defn play-sound [soundkey]\n  (if-let [s (get sounds soundkey)]\n    (doto (Thread. #(sound s))\n      (.setDaemon true)\n      (.start))))\n"
  },
  {
    "path": "src/igoki/ui/calibration.clj",
    "content": "(ns igoki.ui.calibration\n  (:require\n    [seesaw.core :as s]\n    [igoki.camera :as camera]\n    [igoki.litequil :as lq]\n    [igoki.util :as util]\n    [igoki.projector :as projector]\n    [clojure.java.io :as io]\n    [igoki.ui.util :as ui.util])\n  (:import\n    (javax.swing JComboBox BorderFactory)\n    (java.awt Cursor Insets)))\n\n(defn calibration-options [ctx]\n  (s/flow-panel\n    :items\n    [\"Size: \"\n     (s/combobox\n       :listen\n       [:action\n        (fn [e]\n          (let [sel (.getSelectedIndex ^JComboBox (.getSource e))]\n            (camera/set-board-size ctx (nth [9 13 19] sel))))]\n       :model [\"9x9\" \"13x13\" \"19x19\"]\n       :selected-index 2)\n     [20 :by 10]\n     \"Camera: \"\n     (s/combobox\n       :listen\n       [:action\n        (fn [e]\n          (if (.getParent (.getSource e))\n            (.grabFocus (.getParent (.getSource e))))\n          (doto\n            (Thread.\n              #(camera/select-camera ctx (- (.getSelectedIndex ^JComboBox (.getSource e)) 2)))\n            (.setDaemon true)\n            (.start)))]\n       :model\n       (concat\n         [\"Off\"\n          \"Simulated\"]\n         (for [x (range 5)]\n           (str \"Camera \" (inc x))))\n       :selected-index 0)\n     [20 :by 10]\n\n     [20 :by 10]\n     (s/button\n       :id :kofi-button\n       :icon (io/resource \"kofi.png\")\n       :text \"Support me on Ko-fi\"\n       :focusable? false\n       :listen\n       [:action\n        (fn [e]\n          (ui.util/open \"https://ko-fi.com/cmdrdats\"))])]))\n\n(defn construct [ctx]\n  (lq/smooth)\n  (lq/frame-rate 5)\n  (lq/background 200))\n\n(defn convert-point [bufimg [px py]]\n  [(/ (* px (lq/width)) (.getWidth bufimg))\n   (/ (* py (lq/height)) (.getHeight bufimg))])\n\n(def pn [\"A1\" \"T1\" \"T19\" \"A19\"])\n\n(defn draw [ctx]\n  (lq/background 128 64 78)\n  (lq/rect 0 0 (lq/width) (lq/height))\n\n  (let [c (camera/camera-image ctx)]\n    (cond\n      (nil? c)\n      (lq/shadow-text \"Could not acquire image?\" 10 25)\n\n      :else\n      (let [{{:keys [size edges points lines flat flat-view? camerapoints]} :goban\n             board :board} @ctx\n\n            points (map (partial convert-point c) points)\n            edges (map #(map (partial convert-point c) %) edges)]\n        (lq/image c 0 0 (lq/width) (lq/height))\n        (lq/shadow-text \"Please select the corners of the board\" 10 25)\n\n\n        (lq/color 255 255 255 128)\n        (lq/stroke-weight 0.5)\n        (when (and camerapoints board size)\n          (doseq [[idx p] (map-indexed vector camerapoints)\n                  :let [[px py] (convert-point c p)\n                        stone (get-in board [(int (/ idx size)) (mod idx size)])]\n                  :when stone]\n            (if (= stone :b)\n              (do (lq/background 0 0 0) (lq/color 255 255 255))\n              (do (lq/background 255 255 255) (lq/color 0 0 0)))\n            (lq/ellipse px py 10 10)))\n\n\n\n        (lq/color 255 255 255 96)\n        (lq/stroke-weight 1)\n        (when lines\n          (doseq [[p1 p2] lines]\n            (lq/line (convert-point c p1) (convert-point c p2)))\n          (lq/shadow-text\n            (str size \"x\" size)\n            (/ (reduce + (map first points)) 4)\n            (/ (reduce + (map second points)) 4)\n            :center :bottom))\n\n        (lq/color 78 64 255 128)\n        (lq/stroke-weight 2)\n        (doseq [[p1 p2] edges]\n          (lq/line p1 p2))\n\n\n        (doseq [[p [x y]] (map-indexed vector points)]\n          (lq/text (get pn p) x (- y 5)\n            {:align [:center :bottom]})\n          (lq/ellipse x y 2 2))\n        (when (and flat flat-view?)\n          (lq/image (:bufimg flat) 0 0 (lq/width) (lq/height)))))))\n\n\n(defn mouse-dragged [ctx e]\n  (when-let [size (camera/camera-size ctx)]\n    (let [[cx cy] size\n          p\n          [(/ (* (lq/mouse-x) cx) (lq/width))\n           (/ (* (lq/mouse-y) cy) (lq/height))]\n\n          points (get-in @ctx [:goban :points])\n          points (util/update-closest-point points p)]\n      (camera/update-corners ctx points))))\n\n(defn mouse-pressed [ctx e]\n  (when-let [size (camera/camera-size ctx)]\n    (let [[cx cy] size\n          p\n          [(/ (* (lq/mouse-x) cx) (lq/width))\n           (/ (* (lq/mouse-y) cy) (lq/height))]\n\n          points (get-in @ctx [:goban :points])\n          points\n          (if (> (count points) 3)\n            (util/update-closest-point points p)\n            (vec (conj points p)))]\n      (camera/update-corners ctx points))))\n\n\n;; TODO: This isn't even working - this needs to all become UI elements or just straight\n;; out dropped.\n(defn key-typed [ctx e]\n  (case (lq/key-code e)\n    32 (swap! ctx update-in [:goban :flat-view?] (fnil not false))\n    67 (camera/cycle-corners ctx)\n    (println \"Unhandled key-down: \" (lq/key-code e))))\n\n(defn calibration-panel [ctx]\n  (let [panel\n        (:panel\n          (lq/sketch-panel\n            {:setup (partial #'construct ctx)\n             :draw (partial #'draw ctx)\n             :mouse-dragged (partial #'mouse-dragged ctx)\n             :mouse-pressed (partial #'mouse-pressed ctx)\n             :key-typed (partial #'key-typed ctx)}))]\n    (.setCursor panel (Cursor/getPredefinedCursor Cursor/CROSSHAIR_CURSOR))\n    (s/border-panel\n      :minimum-size [10 :by 10]\n      :id :calibration-panel\n      :south (calibration-options ctx)\n      :center panel)))"
  },
  {
    "path": "src/igoki/ui/game.clj",
    "content": "(ns igoki.ui.game\n  (:require\n    [igoki.util :as util]\n    [igoki.litequil :as lq]\n    [igoki.sgf :as sgf]\n    [igoki.ui.util :as ui.util]\n    [igoki.game :as game]\n    [seesaw.core :as s]\n    [seesaw.color :as sc]\n    [igoki.sound.announce :as announce]\n    [seesaw.mig :as sm])\n  (:import\n    (java.io File)))\n\n\n(defn export-sgf [ctx]\n  (ui.util/save-dialog\n    (:current-file @ctx)\n    #(spit % (game/convert-sgf ctx))))\n\n(defn load-sgf [ctx]\n  (ui.util/load-dialog\n    (fn [^File f]\n      (println \"Opening sgf: \" (.getAbsolutePath f))\n      (game/load-sgf ctx f))))\n\n\n(def move-colours\n  {0 {:white [0 0 0] :black [255 255 255]}\n   1 {:white [255 64 64] :black [255 96 96]}\n   2 {:white [0 150 0] :black [64 255 64]}\n   3 {:white [32 32 255] :black [128 128 255]}\n   4 {:white [255 255 0] :black [255 255 0]}\n   5 {:white [0 255 255] :black [0 255 255]}\n   6 {:white [255 0 255] :black [255 0 255]}})\n\n(defn draw [ctx]\n  (lq/stroke-weight 1)\n  (lq/color 0)\n  (lq/background 255 255 255)\n  (lq/rect 0 0 (lq/width) (lq/height))\n\n  ;; Draw the board\n  (let [{{:keys [submit kifu-board constructed movenumber] :as game} :kifu\n         {:keys [pimg flattened-pimage]} :camera\n         board :board\n         {:keys [size ] :or {size 19}} :goban} @ctx\n        cellsize (/ (lq/height) (+ size 2))\n        grid-start (+ cellsize (/ cellsize 2))\n        board-size (* cellsize (dec size))\n        extent (+ grid-start board-size)\n        tx (+ (lq/height) (/ cellsize 2))\n        visiblepath (take (or movenumber 0) (mapcat identity (:current-branch-path game)))\n        actionlist (sgf/current-branch-node-list [visiblepath] (:moves game))\n        lastmove (last actionlist)\n        canvas-size (max 250 (min (lq/width) (lq/height)))]\n\n\n\n\n    (when flattened-pimage\n      (lq/image (:bufimg flattened-pimage)\n        (- grid-start cellsize) (- grid-start cellsize)\n        (+ board-size (* cellsize 2)) (+ board-size (* cellsize 2))))\n\n    (lq/color 220 179 92 150)\n    (lq/fillrect 0 0 canvas-size canvas-size)\n\n\n    (lq/stroke-weight 0.8)\n    (lq/color 0 196)\n    (lq/background 0)\n\n    ;; Draw the grid\n    (lq/text-font \"helvetica-20pt\")\n\n    (doseq [x (range size)]\n      (let [coord (+ grid-start (* x cellsize))\n            letter (char (+ 65 x (if (> x 7) 1 0)))]\n        (lq/text (str letter) coord (- grid-start (/ cellsize 2))\n          {:align [:center :bottom]})\n        (lq/text (str letter) coord (+ extent (/ cellsize 2))\n          {:align [:center :top]})\n\n        (lq/text (str (inc x))\n          (- grid-start (/ cellsize 2)) coord\n          {:align [:right :center]})\n        (lq/text (str (inc x)) (+ extent (/ cellsize 2)) coord\n          {:align [:left :center]})\n\n        (lq/line coord grid-start coord extent)\n        (lq/line grid-start coord extent coord)))\n\n    ;; Draw star points\n    (doseq [[x y] (util/star-points size)]\n      (lq/stroke-weight 1)\n      (lq/color 0 32)\n      (lq/background 0)\n      (lq/ellipse\n        (+ grid-start (* x cellsize))\n        (+ grid-start (* y cellsize)) 6 6))\n\n    ;; Draw camera board (shadow)\n    (doseq [[y row] (map-indexed vector board)\n            [x d] (map-indexed vector row)]\n      (when d\n        (lq/stroke-weight 1)\n        (lq/color 0 32)\n        (lq/background (if (= d :w) 255 0) 32)\n        (lq/ellipse\n          (+ grid-start (* x cellsize))\n          (+ grid-start (* y cellsize))\n          (- cellsize 3) (- cellsize 3))))\n\n    (lq/text-size 12)\n\n    ;; Draw the constructed sgf board stones\n    (doseq [[pt {:keys [stone] mn :movenumber}] (:board constructed)]\n      (let [[x y :as p] (sgf/convert-sgf-coord pt)]\n        (when (and p stone)\n          (lq/stroke-weight 0.5)\n          (lq/color 0)\n          (lq/background (if (= stone :white) 255 0))\n          (lq/ellipse (+ grid-start (* x cellsize))\n            (+ grid-start (* y cellsize)) (- cellsize 2) (- cellsize 2))\n\n          (lq/background (if (= stone :white) 0 255)))\n\n        (when (and (not stone) mn)\n          (lq/stroke-weight 0)\n          (lq/color 220 179 92)\n          (lq/background 220 179 92)\n          (lq/ellipse (+ grid-start (* x cellsize))\n            (+ grid-start (* y cellsize)) 20 20)\n\n          (lq/background 0))\n\n        (when (and mn (< (- movenumber mn) 40))\n          (let [movediff (- movenumber mn)\n                movenum (mod (inc mn) 100)\n                movecol (get-in move-colours [(int (/ mn 100)) (or stone :black)] [0 0 0])\n                movecol\n                (if (> movediff 20)\n                  (conj movecol (- 255 (* 255 (/ (- movediff 20) 20))))\n                  movecol)]\n            (apply lq/color movecol)\n\n            (lq/text-size 12)\n            (lq/text (str movenum) (+ grid-start (* x cellsize)) (- (+ grid-start (* y cellsize)) 1)\n              {:align [:center :center]})))))\n\n    ;; TODO: This should go out to its own panel.\n    (when (:comment lastmove)\n      (lq/color 0)\n      (lq/text-size 12)\n      (lq/text (first (:comment lastmove)) tx 240 (- (lq/width) tx) (lq/height)\n        {:align [:left :top]}))\n\n\n    ;; Draw labels\n    (doseq [label (:label lastmove)]\n      (let [[pt text] (.split label \":\" 2)\n            [x y :as p] (sgf/convert-sgf-coord pt)\n            stone (nth (nth kifu-board y) x)]\n\n        (cond\n          (= stone :w)\n          (do\n            (lq/background 255)\n            (lq/color 255))\n\n          (= stone :b)\n          (do\n            (lq/background 0)\n            (lq/color 0))\n\n          :else\n          (do\n            (lq/background 220 179 92)\n            (lq/color 220 179 92)))\n\n        (lq/stroke-weight 0)\n        (lq/ellipse (+ grid-start (* x cellsize))\n          (+ grid-start (* y cellsize)) (/ cellsize 1.5) (/ cellsize 1.5))\n        (lq/background (if (= stone :b) 255 0))\n\n        (lq/color 0)\n        (lq/text\n          text\n          (+ grid-start (* x cellsize))\n          (- (+ grid-start (* y cellsize)) 1)\n          {:align [:center :center]})))\n\n    ;; Draw annotated triangles.\n    (doseq [pt (:triangle lastmove)]\n      (let [[x y :as p] (sgf/convert-sgf-coord pt)\n            stone (nth (nth kifu-board y) x)]\n\n        (lq/stroke-weight 0)\n        (apply lq/background (cond (= stone :b) [0] (= stone :w) [255] :else [220 179 92]))\n        (lq/ellipse (+ grid-start (* x cellsize))\n          (+ grid-start (* y cellsize)) (/ cellsize 1.1) (/ cellsize 1.1))\n\n        (lq/stroke-weight 2)\n        (lq/color (if (= stone :b) 255 0))\n        (lq/triangle\n          (+ grid-start (* x cellsize)) (- (+ grid-start (* y cellsize)) 6)\n          (- (+ grid-start (* x cellsize)) 6) (+ (+ grid-start (* y cellsize)) 4.5)\n          (+ (+ grid-start (* x cellsize)) 6) (+ (+ grid-start (* y cellsize)) 4.5))))\n\n    ;; If in the process of submitting, mark that stone.\n    (when submit\n      #_(let [[x y _ d] (:move submit)]\n          (lq/stroke-weight 1)\n          (lq/stroke 0 128)\n          (lq/background (if (= d :w) 255 0) 128)\n          (lq/ellipse\n            (+ grid-start (* x cellsize))\n            (+ grid-start (* y cellsize))\n            (- cellsize 3) (- cellsize 3))\n          (lq/background (if (= d :w) 0 255))\n          (lq/text \"?\" (+ grid-start (* xcellsize)) (+ grid-start (* y cellsize))\n            {:align [:center :center]})))\n\n    ;; Mark the last move\n    (when lastmove\n      (let [{:keys [black white]} lastmove]\n        (doseq [m (or black white)]\n          (let [[x y :as p] (sgf/convert-sgf-coord m)]\n            (when p\n              (lq/color (if white 0 255))\n              (lq/stroke-weight 3)\n              (lq/background 0 0)\n              (lq/ellipse (+ grid-start (* x cellsize))\n                (+ grid-start (* y cellsize)) (/ cellsize 2) (/ cellsize 2))))))\n\n      ;; Mark next branches\n      (when (:show-branches game)\n        (doseq [[idx {:keys [black white]}] (map-indexed vector (:branches lastmove))\n                m (or black white)]\n          (let [[x y :as p] (sgf/convert-sgf-coord m)]\n            (when p\n              (if (zero? idx)\n                (lq/color (if white 255 0))\n                (apply lq/color (if white [255 0 0] [0 0 255])))\n              (lq/stroke-weight 3)\n              (lq/background 0 0)\n              (lq/ellipse (+ grid-start (* x cellsize))\n                (+ grid-start (* y cellsize)) (/ cellsize 2) (/ cellsize 2))\n              (lq/background 0)\n\n              (when (pos? idx)\n                (lq/text-size 9)\n                (lq/text\n                  (str idx)\n                  (- (+ grid-start (* x cellsize)) 9)\n                  (- (+ grid-start (* y cellsize)) 9))))))))\n\n    ;; Highlight differences between constructed and camera board (visual syncing)\n    (when (and board kifu-board)\n      (doseq [[x y _ _]\n              (game/board-diff kifu-board board)]\n        (lq/stroke-weight 3)\n        (lq/color 255 0 0)\n        (lq/background 0 0)\n        (lq/ellipse (+ grid-start (* x cellsize))\n          (+ grid-start (* y cellsize)) (- cellsize 3) (- cellsize 3))))))\n\n\n(defn game-panel [ctx]\n  (let [panel\n        (:panel\n          (lq/sketch-panel\n            {:draw (partial #'draw ctx)}))\n\n        container\n        (s/border-panel\n          :minimum-size [10 :by 10]\n          :south\n          (sm/mig-panel\n            :constraints [\"center\"]\n            :items\n            [[(s/label :text \"\" :id :record-status) \"spanx, wrap\"]\n             [(s/toggle :text \"Debug ZIP\" :id :record-debug\n                :selected? false\n                :listen\n                [:action\n                 (fn [e]\n                   (swap! ctx assoc :debug-capture (s/value (.getSource e))))])\n              \"\"]\n             [(s/button :text \"<\"\n                :listen\n                [:action (fn [e] (game/move-backward ctx))])\n              \"\"]\n             [(s/button :text \">\"\n                :listen\n                [:action (fn [e] (game/move-forward ctx))])\n              \"\"]\n             [[20 :by 10] \"\"]\n             [(s/button :text \"Pass\"\n                :listen\n                [:action (fn [e] (game/pass ctx))])\n              \"\"]\n             [[20 :by 10] \"\"]\n             [(s/toggle :text \"Show Branches\"\n                :listen\n                [:action\n                 (fn [e]\n                   (game/toggle-branches ctx (s/value (.getSource e))))])\n              \"wrap\"]\n             [[20 :by 10] \"grow\"]\n             [(s/label :text \"Announce \") \"\"]\n             [(s/combobox\n                :listen\n                [:action\n                 (fn [e]\n                   (announce/set-announce-player ctx\n                     (case (s/value (.getSource e))\n                       \"Black\" :black\n                       \"White\" :white\n                       \"Both\" :both\n                       nil)))]\n                :model [\"None\" \"Black\" \"White\" \"Both\"])\n              \"\"]\n             [(s/label :text \" in \") \"\"]\n             [(s/combobox\n                :listen\n                [:action\n                 (fn [e]\n                   (announce/set-announce-language ctx\n                     (case (s/value (.getSource e))\n                       \"English\" :en\n                       \"Japanese\" :jp)))]\n                :model [\"English\" \"Japanese\"])\n              \"\"]\n             [[20 :by 10] \"\"]\n             [(s/label :text \"\" :id :game-status) \"\"]\n             ])\n          :center panel)]\n\n    (util/add-watch-path ctx :kifu\n      [:kifu]\n      (fn [k r o {:keys [movenumber constructed] :as game}]\n        (s/config! (s/select container [:#record-status]) :text\n          (str \"Img:\" (:camidx game) \" at \" (:filename game)))\n        (s/config! (s/select container [:#game-status]) :text\n          (str \"Move \" (inc (or movenumber 0)) \", \" (if (= (:player-turn constructed) :black) \"Black\" \"White\") \" to play\"))))\n\n    container))"
  },
  {
    "path": "src/igoki/ui/main.clj",
    "content": "(ns igoki.ui.main\n  (:require\n    [seesaw.core :as s]\n    [igoki.ui.game :as ui.game]\n    [igoki.ui.calibration :as calibration]\n    [igoki.ui.ogs :as ogs]\n    [igoki.ui.tree :as tree]\n    [igoki.ui.robot :as robot]\n    [igoki.game :as game]\n    [igoki.simulated :as sim]\n    [igoki.camera :as camera]\n    [igoki.ui.util :as ui.util]\n    [igoki.ui.projector :as ui.projector])\n  (:import (javax.swing JFrame)))\n\n(s/native!)\n\n(defn ogs-panel [ctx]\n  (s/tabbed-panel\n    :minimum-size [10 :by 10]\n    :placement :bottom\n    :overflow :scroll\n    :tabs\n    [{:title \"OGS\"\n      :tip \"Online-go.com integration\"\n      :content (ogs/ogs-panel ctx)}\n     {:title \"Manual\"\n      :tip \"Manual screen integration\"\n      :content (robot/robot-panel ctx)}\n     {:title \"Projector\"\n      :type \"Projector setup/settings\"\n      :content (ui.projector/projector-panel ctx)}\n     {:title \"Screen\"\n      :tip \"Simulation (dev tools)\"\n      :content\n      (sim/simulation-panel ctx)}]))\n\n(defn tree-panel [ctx]\n  (s/tabbed-panel\n    :minimum-size [10 :by 10]\n    :placement :bottom\n    :overflow :scroll\n    :tabs\n    [{:title \"Tree\"\n      :tip \"SGF Move tree\"\n      :content\n      (tree/tree-panel ctx)}\n     #_{:title \"Log\"\n      :tip \"Output log (dev tools)\"\n      :content\n      (logging/log-panel ctx)\n      }]))\n\n(defn primary-splits [ctx]\n  (let [cl\n        (s/top-bottom-split\n          (calibration/calibration-panel ctx)\n          (ogs-panel ctx)\n          :border 0\n          :resize-weight 0.5\n          :divider-location 0.5)\n\n        gt\n        (s/top-bottom-split\n          (ui.game/game-panel ctx)\n          (tree-panel ctx)\n          :border 0\n          :resize-weight 0.5\n          :divider-location 0.5)]\n    (s/left-right-split\n      cl gt\n      :resize-weight 0.5\n      :divider-location 0.5)))\n\n\n\n(defn main-menu [ctx]\n  (s/menubar\n    :items\n    [(s/menu :text \"File\"\n       :items\n       [(s/action\n          :mnemonic \\n\n          :name \"New SGF...\"\n          :key \"menu N\"\n          :handler\n          (fn [e]\n            (when\n              (s/confirm \"Reset to new SGF recording, are you sure?\"\n                :title \"New SGF\"\n                :type :warning\n                :option-type :yes-no)\n              (game/reset-kifu ctx))))\n        (s/action\n          :mnemonic \\o\n          :name \"Open SGF\"\n          :key \"menu O\"\n          :handler\n          (fn [e]\n            (ui.game/load-sgf ctx)))\n\n        (s/action\n          :mnemonic \\s\n          :name \"Save SGF\"\n          :key \"menu S\"\n          :handler\n          (fn [e]\n            (ui.game/export-sgf ctx)))\n\n\n        :separator\n        (s/action\n          :mnemonic \\x\n          :name \"Exit\"\n          :handler\n          (fn [e]\n            (when\n              (s/confirm \"Exiting, are you sure?\"\n                :title \"Exit\"\n                :type :warning\n                :option-type :yes-no)\n              (camera/stop-read-loop ctx)\n              (System/exit 0))))])\n\n     (s/menu :text \"Help\"\n       :items\n       [(s/action\n          :name \"Website\"\n          :handler\n          (fn [e]\n            (ui.util/open \"https://github.com/cmdrdats/igoki\")))\n        (s/action\n          :name \"Update\"\n          :handler\n          (fn [e]\n            (ui.util/open \"https://github.com/CmdrDats/igoki/releases\")))\n\n        :separator\n        (str \"Version: \" (System/getProperty \"igoki.version\"))])\n     ]))\n\n(defn frame-content [ctx]\n  (s/border-panel\n    :center (primary-splits ctx)))\n\n(defonce app-frame (atom nil))\n(defn main-frame [ctx]\n  (let [frame\n        (s/frame\n          :icon \"igoki48.png\"\n          :title \"igoki\"\n          :size [1024 :by 768]\n          :menubar (main-menu ctx)\n          :on-close :exit)]\n    (.setExtendedState frame JFrame/MAXIMIZED_BOTH)\n    (-> frame s/show!)\n    (s/config! frame :content (frame-content ctx))\n    (reset! app-frame frame))\n  #_(open\n    {:title \"igoki\"\n     :body\n     [:button \"Push me\"]}))\n\n(defn refresh [ctx]\n  (s/config! @app-frame :menubar\n    (main-menu ctx)\n    :content (frame-content ctx)))"
  },
  {
    "path": "src/igoki/ui/ogs.clj",
    "content": "(ns igoki.ui.ogs\n  (:require\n    [seesaw.core :as s]\n    [seesaw.mig :as sm]\n    [seesaw.color :as sc]\n    [igoki.integration.ogs :as ogs]\n    [igoki.ui.util :as ui.util]\n    [igoki.util :as util])\n  (:import\n    (java.awt.event KeyEvent)\n    (java.awt Graphics2D)\n    (java.awt.geom Ellipse2D$Double)\n    (javax.swing DefaultListCellRenderer)))\n\n(declare game-list-panel)\n(defn game-info-panel [ctx]\n  (let [{:keys [game_name players json]} (get-in @ctx [:ogs :game])\n        {:keys [black white]} players\n\n        panel\n        (sm/mig-panel\n          :constraints [\"center\"]\n          :items\n          [[\"Game Details\" \"wrap, span, center, gapbottom 20\"]\n           [\"Game Name:\" \"align label\"]\n           [(str game_name) \"wrap\"]\n           [\"Black: \" \"align label\"]\n           [(ogs/str-player black) \"wrap\"]\n           [\"White: \" \"align label\"]\n           [(ogs/str-player white) \"wrap\"]])\n\n        container\n        (s/border-panel\n          :south\n          (s/flow-panel\n            :align :center\n            :items\n            [(s/button :text \"Disconnect\" :id :ogs-game-disconnect)])\n\n          :center\n          (s/scrollable panel :vscroll :always))]\n\n    (s/listen (s/select container [:#ogs-game-disconnect])\n      :action\n      (fn [e]\n        (ogs/disconnect-record ctx)\n        (s/replace!\n          (.getParent container) container (game-list-panel ctx))))\n\n    container))\n\n\n(defn paint-game-panel [ctx game c ^Graphics2D g]\n  (.setColor g (sc/color \"#dcb35c\"))\n  (let [[w h] [(.getWidth c) (.getHeight c)]\n        board-width (:width game)\n        ;; TODO: Need to do a few fixes for non-square boards....\n        board-height (:height game)\n        cellsize (/ h (+ board-width 2))\n        grid-start (+ cellsize (/ cellsize 2))\n        board-size (* cellsize (dec board-width))\n        extent (+ grid-start board-size)\n        tx (+ h (/ cellsize 2))\n\n        constructed (:constructed game)\n        kifu-board (get-in constructed [:kifu :kifu-board])\n        ]\n    (.fillRect g 0 0 w h)\n\n    (.setColor g (sc/color :black))\n    (doseq [x (range board-width)]\n      (let [coord (+ grid-start (* x cellsize))]\n        (.drawLine g coord grid-start coord extent)\n        (.drawLine g grid-start coord extent coord)))\n\n    ;; Draw star points\n    (doseq [[x y] (util/star-points board-width)]\n      (let [e (Ellipse2D$Double. (+ grid-start (- (* x cellsize) 1.5))\n                (+ grid-start (- (* cellsize y) 1.5)) 4 4)]\n        (.fill g e)))\n\n\n    (doseq [y (range board-height)]\n      (doseq [x (range board-width)]\n        (let [stone (nth (nth kifu-board y) x)]\n          (when stone\n            (let [e (Ellipse2D$Double. (+ grid-start (- (* x cellsize) 4))\n                      (+ grid-start (- (* cellsize y) 4)) 8 8)]\n              (.setColor g (sc/color (if (= stone :w) :white :black)))\n              (.fill g e)\n              (when (= stone :w)\n                (.setColor g (sc/color :black))\n                (.draw g e)))))))))\n\n(defn game-panel [ctx game selected?]\n  (let [black (:black game)\n        white (:white game)]\n    (sm/mig-panel\n      :background (if selected? :steelblue nil)\n      :constraints [\"\"]\n      :items\n      [[(s/canvas\n          :size [200 :by 200]\n          :paint (partial #'paint-game-panel ctx game)) \"left, spany\"]\n       [(ogs/str-player black) \"wrap, gapleft 10\"]\n       [(ogs/str-player white) \"wrap, gapleft 10\"]])))\n\n(declare ogs-login-panel)\n(defn game-list-panel [ctx]\n  (let [setup-model\n        #(map\n           (fn [g]\n            (assoc g :constructed\n              (ogs/initialize-game {} (:json g))))\n           (get-in @ctx [:ogs :overview :active_games]))\n\n        gamelist\n        (s/listbox\n          :id :ogs-game-list\n          :model (setup-model)\n          :renderer\n          (proxy [DefaultListCellRenderer] []\n            (getListCellRendererComponent [component value index selected? foxus?]\n              (game-panel ctx value selected?))))\n\n        status-label (s/label :text \"\" :id :ogs-connect-status)\n        container\n        (s/border-panel\n          :south\n          (s/flow-panel\n            :align :center\n            :items\n            [(s/button :text \"Disconnect\" :id :ogs-disconnect)\n             [40 :by 10]\n             (s/button :text \"Refresh\" :id :ogs-refresh)\n             (s/button :text \"Connect to selected\" :id :ogs-connect-selected)\n             status-label])\n          :center\n          (s/scrollable gamelist\n            :vscroll :always))]\n\n    (s/listen\n      (s/select container [:#ogs-connect-selected])\n      :action\n      (fn [e]\n        (let [game (s/value gamelist)\n              ogs (:ogs @ctx)\n\n              {:keys [success msg]}\n              (ogs/connect-record ctx (:socket ogs)\n                (str (:id game)) (:auth ogs))]\n          (if success\n            (s/replace! (.getParent container) container (game-info-panel ctx))\n            (s/config! status-label :text msg)))))\n\n    (s/listen\n      (s/select container [:#ogs-refresh])\n      :action\n      (fn [e]\n        (ogs/refresh-games ctx)\n        (s/config!\n          (s/select container [:#ogs-game-list])\n          :model (setup-model))))\n\n    (s/listen\n      (s/select container [:#ogs-disconnect])\n      :action\n      (fn [e]\n        (ogs/disconnect ctx)\n        (s/replace!\n          (.getParent container) container (ogs-login-panel ctx))))\n    container))\n\n(defn ogs-login-panel [ctx]\n  (let [settings (ogs/load-settings)\n        login-panel\n        (sm/mig-panel\n          :constraints [\"center\" \"\" \"\"]\n          :items\n          [[\"Online Go Bot Credentials\" \"span, center, gapbottom 15\"]\n           [\"Generate API details:\" \"align label\"]\n           [(s/button :text \"Open Browser\" :id :open) \"wrap\"]\n           [\"Client ID: \" \"align label\"]\n           [(s/text :id :client-id :columns 32) \"wrap\"]\n           [\"Client Secret: \" \"align label\"]\n           [(s/password :id :client-secret :columns 24) \"wrap\"]\n           [\"Client Type: \" \"align label\"]\n           [\"Confidential\" \"wrap\"]\n           [\"Authorization Grant Type: \" \"align label\"]\n           [\"Password\" \"wrap\"]\n           [\"\" \"span, center, gapbottom 15\"]\n           [\"User login credentials\" \"span, center, gapbottom 15\"]\n           [\"Username: \" \"align label\"]\n           [(s/text :id :username :columns 24) \"wrap\"]\n           [\"Password: \" \"align label\"]\n           [(s/password :id :password :columns 20) \"wrap\"]\n           [\"\" \"align label\"]\n           [(s/checkbox :id :remember :text \"Remember password\") \"wrap\"]\n           [(s/label :id :progress :text \"Progress\")]\n           [(s/button :text \"Connect\" :id :save) \"tag ok, span, split 3, sizegroup bttn, gaptop 15\"]])\n\n\n        login\n        (fn []\n          (doto\n            (Thread.\n              #(let [result\n                     (ogs/connect ctx (s/value login-panel)\n                       (fn [e]\n                         (s/config! (s/select login-panel [:#progress])\n                           :text (str e))))]\n                 (s/config! (s/select login-panel [:#progress])\n                   :text\n                   (if (:success result)\n                     (str \"Connected.\")\n                     (:message result)))\n                 (s/replace! (.getParent login-panel) login-panel (game-list-panel ctx))))\n\n            (.setDaemon true)\n            (.start)))]\n    (s/value! login-panel settings)\n\n    ;; Form listening for submit.\n    (s/listen\n      (s/select login-panel [:#open])\n      :action\n      (fn [e]\n        (ui.util/open \"https://online-go.com/oauth2/applications/\")))\n\n    (s/listen\n      (concat\n        (s/select login-panel [:JPasswordField])\n        (s/select login-panel [:JTextField]))\n      :key-released\n      (fn [e]\n        (when (= KeyEvent/VK_ENTER (.getKeyCode e))\n          (login))))\n\n    (s/listen\n      (s/select login-panel [:#save])\n      :action (fn [e] (login)))\n    login-panel))\n\n\n\n(defn ogs-panel [ctx]\n  (let [login-panel (ogs-login-panel ctx)\n        panel (s/border-panel :id :ogs-panel :center login-panel)]\n    panel))"
  },
  {
    "path": "src/igoki/ui/projector.clj",
    "content": "(ns igoki.ui.projector\n  (:require [seesaw.core :as s]\n            [igoki.projector :as projector]\n            [seesaw.mig :as sm]))\n\n(defn refresh-button-states [ctx container]\n  (let [{:keys [sketch calibrate?] :as s} @projector/proj-ctx\n        _ (println \"proj-ctx\" s)\n        {:keys [frame] :as f} (when sketch @sketch)\n        _ (println \"sketch\" f)\n        _ (println \"frame\" frame)\n        {:keys [robot]} @ctx\n        states\n        [[:#projector-open-button (not frame)]\n         [:#projector-calibration-grid (and frame (not calibrate?))]\n         [:#projector-calibration-accept (and frame calibrate?)]\n         [:#projector-close-button frame]]]\n\n    (println \"STATES\" states)\n    (doseq [[id state] states]\n      (println \"Setting \" (s/select container [id])\n        \"to \" state)\n      ((if state s/show! s/hide!)\n       (s/select container [id])))))\n\n(defn projector-panel [ctx]\n  (let [container (s/border-panel)\n        refresh\n        #(refresh-button-states ctx container)]\n    (s/config! container :center\n      (s/border-panel\n        :north\n        (sm/mig-panel\n          :constraints [\"center\"]\n          :items\n          [[(s/button\n              :id :projector-open-button\n              :text \"Projector Window\"\n              :listen\n              [:action\n               (fn [e]\n                 (projector/start-cframe ctx refresh)\n                 (refresh))]) \"wrap\"]\n           [(s/button\n              :id :projector-calibration-grid\n              :text \"Calibration Grid\"\n              :listen\n              [:action\n               (fn [e]\n                 (projector/show-calibration)\n                 (refresh))]) \"wrap\"]\n\n           [(s/button\n              :id :projector-calibration-accept\n              :text \"Accept Calibration\"\n              :listen\n              [:action\n               (fn [e]\n                 (projector/accept-calibration ctx)\n                 (refresh))]) \"wrap\"]\n\n           [(s/button\n              :id :projector-close-button\n              :text \"Close capture frame\"\n              :listen\n              [:action\n               (fn [e]\n                 (projector/stop-cframe ctx refresh))]) \"wrap\"]])\n\n        #_#_:center\n            (game-setup-panel ctx container)\n\n        #_#_:south\n        (s/flow-panel\n          :items\n          [(s/button\n             :id :robot-start-capture\n             :text \"Start Recording\"\n             :visible? false\n             :listen\n             [:action\n              (fn [e] (robot-start-capture ctx container))])\n\n           (s/button\n             :id :robot-pause-capture\n             :text \"Pause Recording\"\n             :visible? false\n             :listen\n             [:action\n              (fn [e] (robot-pause-capture ctx container))])\n\n           (s/button\n             :id :robot-unpause-capture\n             :text \"Unpause Recording\"\n             :visible? false\n             :listen\n             [:action\n              (fn [e] (robot-unpause-capture ctx container))])\n\n           (s/button\n             :id :robot-stop-capture\n             :text \"Stop Recording\"\n             :visible? false\n             :listen\n             [:action\n              (fn [e] (robot-stop-capture ctx container))])])\n        ))\n    (refresh)\n    container))"
  },
  {
    "path": "src/igoki/ui/robot.clj",
    "content": "(ns igoki.ui.robot\n  (:require\n    [seesaw.core :as s]\n    [seesaw.color :as sc]\n    [igoki.litequil :as lq]\n    [igoki.util :as util]\n    [igoki.integration.robot :as i.robot]\n    [igoki.camera :as camera]\n    [seesaw.mig :as sm])\n  (:import\n    (java.awt.event MouseEvent)\n    (javax.swing JFrame)\n    (java.awt Rectangle Cursor Graphics2D BasicStroke)\n    (java.awt.image BufferedImage)\n    (org.nd4j.linalg.exception ND4JIllegalStateException)))\n\n\n(defn get-mouse-op [ctx x y sx sy]\n  (let [started? (get-in @ctx [:robot :started])]\n    (cond\n      started? [:none :none]\n\n      :else\n      [(cond\n         (< x 25) :west\n         (> x (- sx 25)) :east\n         :else :move)\n\n       (cond\n         (< y 25) :north\n         (> y (- sy 25)) :south\n         :else :move)])))\n\n(defn apply-xop [^Rectangle bounds [xop yop] mdx]\n  ;; Yes, creating duplicate objects. Mutation sucks. A hill I'll die on.\n  (let [result (Rectangle. bounds)]\n    (case xop\n      :west\n      (.setBounds result\n        (+ (.getX bounds) mdx) (.getY bounds)\n        (- (.getWidth bounds) mdx) (.getHeight bounds))\n      :east\n      (.setSize result\n        (+ (.getWidth bounds) mdx) (.getHeight bounds))\n\n      ;; Only move if the other is _also_ move, else it's non-standard behaviour\n      :move\n      (when (= yop :move)\n        (.setLocation result\n          (+ (.getX bounds) mdx) (.getY bounds)))\n\n      nil)\n    result))\n\n(defn apply-yop [^Rectangle bounds [xop yop] mdy]\n  ;; Yes, creating duplicate objects. Mutation sucks. A hill I'll die on.\n  (let [result (Rectangle. bounds)]\n    (case yop\n      :north\n      (.setBounds result\n        (.getX bounds) (+ (.getY bounds) mdy)\n        (.getWidth bounds) (- (.getHeight bounds) mdy))\n      :south\n      (.setSize result\n        (.getWidth bounds) (+ (.getHeight bounds) mdy))\n\n      ;; Only move if the other is _also_ move, else it's non-standard behaviour\n      :move\n      (when (= xop :move)\n        (.setLocation result\n          (.getX bounds) (+ (.getY bounds) mdy)))\n\n      nil)\n    result))\n\n(defn setup-resize-bounds [ctx ^JFrame frame]\n  (let [state (atom {})]\n    (s/listen frame\n      :mouse-moved\n      (fn [^MouseEvent e]\n        (let [size (.getSize frame)\n              [xop yop] (get-mouse-op ctx (.getX e) (.getY e) (.getWidth size) (.getHeight size))\n              cursor\n              (Cursor/getPredefinedCursor\n                (cond\n                  (and (= yop :north) (= xop :east)) Cursor/NE_RESIZE_CURSOR\n                  (and (= yop :north) (= xop :west)) Cursor/NW_RESIZE_CURSOR\n                  (and (= yop :south) (= xop :east)) Cursor/SE_RESIZE_CURSOR\n                  (and (= yop :south) (= xop :west)) Cursor/SW_RESIZE_CURSOR\n                  (= yop :north) Cursor/N_RESIZE_CURSOR\n                  (= yop :south) Cursor/S_RESIZE_CURSOR\n                  (= xop :east) Cursor/E_RESIZE_CURSOR\n                  (= xop :west) Cursor/W_RESIZE_CURSOR\n                  (= xop :none) Cursor/DEFAULT_CURSOR\n                  :else Cursor/MOVE_CURSOR))]\n          (.setCursor frame cursor)))\n\n      :mouse-pressed\n      (fn [^MouseEvent e]\n        (let [size (.getSize frame)\n              op (get-mouse-op ctx (.getX e) (.getY e) (.getWidth size) (.getHeight size))]\n          (swap! state assoc\n            :op op\n            :x (.getXOnScreen e) :y (.getYOnScreen e))))\n\n      :mouse-dragged\n      (fn [^MouseEvent e]\n        (let [{:keys [x y mx my op] :as s} @state\n              dx (- (.getXOnScreen e) x)\n              dy (- (.getYOnScreen e) y)\n              bounds\n              (->\n                (.getBounds frame)\n                (apply-xop op dx)\n                (apply-yop op dy))]\n\n          ;; Only shift bounds if we've not making it too small.\n          (when\n            (and\n              (> (.getWidth bounds) 150)\n              (> (.getHeight bounds) 150))\n\n            (.setBounds frame bounds)))\n\n        (swap! state assoc :x (.getXOnScreen e) :y (.getYOnScreen e))))))\n\n(defn paint-robot-frame [ctx frame ^Graphics2D g2d]\n  (let [{:keys [robot goban]} @ctx\n\n        framesize (.getSize frame)\n        size (or (:size goban) 19)\n\n        cellwidth (/ (.getWidth framesize) size)\n        cellheight (/ (.getHeight framesize) size)\n        gridx-start (/ cellwidth 2)\n        gridy-start (/ cellheight 2)\n        boardx-size (* cellwidth (dec size))\n        boardy-size (* cellheight (dec size))\n        extentx (+ gridx-start boardx-size)\n        extenty (+ gridy-start boardy-size)]\n\n    (cond\n      (:started robot)\n      ;; Recording started, full transparent and red border.\n      (do\n        (.setBackground g2d (sc/color 0 0 0 0))\n        (.setColor g2d\n          (sc/color (if (= :paused (:started robot)) :red :green)))\n\n        (.drawRect g2d 0 0 (dec (.getWidth framesize)) (dec (.getHeight framesize)))\n        ;; DEBUG: Show what we're interpreting from\n        #_(when (:scaled robot)\n          (let [scaled (:scaled robot)\n                bufimg (BufferedImage. (.getWidth scaled) (.getHeight scaled) BufferedImage/TYPE_INT_ARGB)\n                bgd (.getGraphics bufimg)]\n            (try\n              (.setBackground g2d (sc/color :red))\n              (.fillRect g2d 0 0 (+ 2 (.getWidth scaled)) (+ 2 (.getHeight scaled)))\n              (.drawImage bgd scaled 1 1 nil)\n              (.drawImage g2d bufimg 1 1 nil)\n              (finally (.dispose bgd))))\n\n\n          (doseq [y (range size)]\n            (doseq [x (range size)]\n              (try\n                (let [pt\n                      (.getSubimage (:scaled robot)\n                        (* x camera/block-size) (* y camera/block-size)\n                        camera/block-size camera/block-size)]\n                  (.setBackground g2d (sc/color :red))\n                  (.fillRect g2d (* x cellwidth) (* y cellwidth) 15 15)\n                  (.drawImage g2d pt (* x cellwidth) (* y cellheight)\n                    cellwidth cellheight nil)\n                  #_(.drawString g2d (str [x y]) (int (* x cellwidth)) (int (+ 10 (* y cellheight))))\n                  #_[b e w]\n                  #_(cond\n                      (> b 0.5) :b\n                      (> w 0.3) :w))\n                (catch Exception e)))))\n\n        ;; DEBUG - show the board state\n        #_(when (:board robot)\n          (try\n            (doseq [[y rows] (map-indexed vector (:board robot))\n                    [x stone] (map-indexed vector rows)]\n              (when stone\n                (.setStroke g2d (BasicStroke. 3))\n                (.setColor g2d (sc/color :red))\n                (.setBackground g2d (sc/color (if (= stone :b) :black :white)))\n                #_(.drawString g2d (str (vec (map #(int (* 10 %)) stone)))\n                  (int (+ gridx-start (* x cellwidth)))\n                  (int (+ gridy-start (* y cellheight))))\n                (lq/ellipse g2d\n                  (+ gridx-start (* x cellwidth))\n                  (+ gridx-start (* y cellheight))\n                  (- cellwidth 2) (- cellheight 2))))\n            (catch Exception e))))\n\n      :else\n      (do\n        (.setColor g2d (sc/color 0 0 0 128))\n        (.fillRect g2d 0 0 (dec (.getWidth framesize)) (dec (.getHeight framesize)))\n        (.setColor g2d (sc/color :white))\n        (doseq [x (range size)]\n          (let [coordx (+ gridx-start (* x cellwidth))\n                coordy (+ gridy-start (* x cellheight))]\n            (.drawLine g2d gridx-start coordy extentx coordy)\n            (.drawLine g2d coordx gridy-start coordx extenty)))\n\n        ;; Draw star points\n        (doseq [[x y] (util/star-points size)]\n          (lq/ellipse g2d\n            (+ gridx-start (* x cellwidth))\n            (+ gridy-start (* y cellheight)) 6 6))))))\n\n;; Because, gosh, life is too short to be managing this state manually.\n;; I really miss Rum/React.\n(defn refresh-button-states [ctx container]\n  (let [{:keys [robot]} @ctx\n        states\n        [[:#robot-open-button (not (:frame robot))]\n         [:#robot-close-button (:frame robot)]\n         [:#robot-game-detail-panel (and (:frame robot) (not (:started robot)))]\n         [:#robot-start-capture\n          (and (:frame robot) (not (:started robot)))]\n         [:#robot-stop-capture\n          (and (:frame robot) (:started robot))]\n         [:#robot-pause-capture\n          (and (:frame robot) (true? (:started robot)))]\n         [:#robot-unpause-capture\n          (and (:frame robot) (= :paused (:started robot)))]]]\n\n    (doseq [[id state] states]\n      ((if state s/show! s/hide!)\n       (s/select container [id])))))\n\n(defn robot-close-frame [ctx container]\n  (let [{:keys [robot]} @ctx]\n    (when (:frame robot)\n      (s/dispose! (:frame robot))\n      (swap! ctx update :robot dissoc :frame))\n\n    (i.robot/stop-capture ctx)\n    (refresh-button-states ctx container)))\n\n(defn robot-capture-frame [ctx container]\n  (let [{:keys [robot]} @ctx]\n    ;; Get rid of an existing frame\n    (robot-close-frame ctx container)\n\n\n    (try\n      (let [frame\n            ;; Size chosen selfishly as my OGS default size (half-left screen)\n            (s/window\n              :width 714 :height 714\n              :content (s/canvas :paint (partial #'paint-robot-frame ctx)))]\n        (swap! ctx update :robot assoc :frame frame :started false)\n        #_(.setUndecorated frame true)\n        #_(.setResizable frame true)\n        (.setBackground frame (sc/color 0 0 0 0))\n        (.setVisible frame true)\n        (.setAlwaysOnTop frame true)\n\n        (setup-resize-bounds ctx frame)\n        (refresh-button-states ctx container))\n      (catch Exception e\n        (.printStackTrace e))))\n\n  #_(.getGraphicsConfiguration (JFrame.)))\n\n\n\n(defn robot-start-capture [ctx container]\n  (let [{:keys [robot]} @ctx\n        frame (:frame robot)\n        bounds [(.getX frame) (.getY frame) (.getWidth frame) (.getHeight frame)]]\n\n    ;; Get the actual frame out the way before\n    (.setVisible frame false)\n    #_(Thread/sleep 10)\n\n    (i.robot/start-capture ctx (.getDevice (.getGraphicsConfiguration frame)) bounds\n      (s/value (s/select container [:#robot-game-detail])))\n\n    (.setVisible frame true)\n    (refresh-button-states ctx container)\n    (.repaint frame)\n    ))\n\n(defn robot-pause-capture [ctx container]\n  (let [{:keys [robot]} @ctx\n        frame (:frame robot)]\n    (i.robot/pause-capture ctx)\n    (refresh-button-states ctx container)\n    (.repaint frame)))\n\n(defn robot-unpause-capture [ctx container]\n  (let [{:keys [robot]} @ctx\n        frame (:frame robot)]\n    (i.robot/unpause-capture ctx)\n    (refresh-button-states ctx container)\n    (.repaint frame)))\n\n(defn robot-stop-capture [ctx container]\n  (let [{:keys [robot]} @ctx\n        frame (:frame robot)]\n    (i.robot/stop-capture ctx)\n    (refresh-button-states ctx container)\n    (.repaint frame)))\n\n(defn game-setup-panel [ctx container]\n  (s/scrollable\n    (sm/mig-panel\n      :constraints [\"center\" \"\" \"\"]\n      :id :robot-game-detail\n      :items\n      [[\"Game Setup\" \"span, center, gapbottom 15\"]\n       [\"Next Player (NB!): \" \"align label\"]\n       [(s/combobox\n          :id :initial-player\n          :model [\"Black\" \"White\"]) \"wrap\"]\n\n       [\"igoki player: \" \"align label\"]\n       [(s/combobox\n          :id :robot-player\n          :model [\"None\" \"Black\" \"White\" \"Both\"]) \"wrap\"]\n\n       [\"Game Details (optional)\" \"span, center, gapbottom 15\"]\n       [\"Game name:\" \"align label\"]\n       [(s/text :id :game-name :columns 64) \"wrap\"]\n       [\"Black Player \" \"span, center, gapbottom 15\"]\n       [\"Name: \" \"align label\"]\n       [(s/text :id :black-name :columns 32) \"wrap\"]\n       [\"Rank: \" \"align label\"]\n       [(s/text :id :black-rank :columns 10) \"wrap\"]\n\n       [\"White Player \" \"span, center, gapbottom 15\"]\n       [\"Name: \" \"align label\"]\n       [(s/text :id :white-name :columns 32) \"wrap\"]\n       [\"Rank: \" \"align label\"]\n       [(s/text :id :white-rank :columns 10) \"wrap\"]])\n    :id :robot-game-detail-panel\n    :visible? false\n    :hscroll :never))\n\n(defn robot-panel [ctx]\n  (let [container (s/border-panel)]\n    (s/config! container :center\n      (s/border-panel\n        :north\n        (s/flow-panel\n          :items\n          [(s/button\n             :id :robot-open-button\n             :text \"Open capture frame\"\n             :visible? true\n             :listen\n             [:action\n              (fn [e]\n                (robot-capture-frame ctx container))])\n\n           (s/button\n             :id :robot-close-button\n             :text \"Close capture frame\"\n             :visible? false\n             :listen\n             [:action\n              (fn [e] (robot-close-frame ctx container))])])\n\n        :center\n        (game-setup-panel ctx container)\n\n        :south\n        (s/flow-panel\n          :items\n          [(s/button\n             :id :robot-start-capture\n             :text \"Start Recording\"\n             :visible? false\n             :listen\n             [:action\n              (fn [e] (robot-start-capture ctx container))])\n\n           (s/button\n             :id :robot-pause-capture\n             :text \"Pause Recording\"\n             :visible? false\n             :listen\n             [:action\n              (fn [e] (robot-pause-capture ctx container))])\n\n           (s/button\n             :id :robot-unpause-capture\n             :text \"Unpause Recording\"\n             :visible? false\n             :listen\n             [:action\n              (fn [e] (robot-unpause-capture ctx container))])\n\n           (s/button\n             :id :robot-stop-capture\n             :text \"Stop Recording\"\n             :visible? false\n             :listen\n             [:action\n              (fn [e] (robot-stop-capture ctx container))])])\n        ))\n    container))"
  },
  {
    "path": "src/igoki/ui/tree.clj",
    "content": "(ns igoki.ui.tree\n  (:require\n    [seesaw.core :as s]\n    [seesaw.border :as sb]\n    [clojure.pprint :as ppr]\n    [clojure.tools.logging :as log]\n    [seesaw.color :as sc])\n  (:import\n    (java.awt Graphics2D)\n    (javax.swing JPanel)))\n\n\n(defn prerender-branch\n  ([idx node]\n   (println idx)\n   (let [n\n         {:x (+ 10 (* idx 7))\n          :y (or (:y node) 10)\n          :width 7\n          :height 10\n          :colour :red}\n\n         branches\n         (reduce\n           (fn [acc b]\n             (let [r\n                   (prerender-branch (inc idx)\n                     (if (> idx 500)\n                       (->\n                         b\n                         (assoc :y (:y acc))\n                         (dissoc :branches))\n\n                       (->\n                         b\n                         (assoc :y (:y acc)))))]\n               (->\n                 acc\n                 (update :height + 2 (:height r))\n                 (update :width max (:width r))\n                 (update :y + 2 (:height r))\n                 (update :nodes conj r))))\n           {:y (:y n)\n            :height 0\n            :width 0\n            :nodes []}\n           (:branches node))]\n\n     #_(println (:y branches))\n     (assoc n\n       :height (max (:height branches) (:height n))\n       :width (+ (:width branches) (:width n))\n       :branches (when-not (empty? (:nodes branches)) (:nodes branches))\n       :idx idx\n       :colour\n       (cond\n         (:black node) :black\n         (:white node) :white\n         :else :red)))))\n\n#_(defn node-labels [{:keys [x y width height branches colour]}]\n  (concat\n    [(s/label\n       :background colour :border (sb/line-border :color :black :thickness 1)\n       :text \" \" :bounds [x y 7 10])]\n    (mapcat node-labels branches)))\n\n(defn paint-tree [ctx node c ^Graphics2D g]\n  (.setColor g (sc/color (:colour node)))\n  (.fillRect g (:x node) (:y node) 7 10)\n  (.setColor g (sc/color :black))\n  (.drawLine g (:x node) (:y node) (+ 7 (:x node)) (:y node))\n  (.drawLine g (:x node) (+ 10 (:y node)) (+ 7 (:x node)) (+ 10 (:y node)))\n  (doseq [n (:branches node)]\n    (paint-tree ctx n c g)))\n\n(defn paint-tree-atom [ctx tree-atom c g]\n  (paint-tree ctx @tree-atom c g))\n\n(defn rendered-tree-panel [ctx]\n  (let [prerendered (prerender-branch 0 (get-in @ctx [:kifu :moves]))\n        tree-atom (atom prerendered)\n        xyz\n        (s/canvas\n          :paint (partial #'paint-tree-atom ctx tree-atom)\n          :size [(:width prerendered) :by (:height prerendered)])]\n    (add-watch ctx ::tree-panel\n      (fn [k r o n]\n        (when (not= (-> o :kifu :moves) (-> n :kifu :moves))\n          (let [rendered (prerender-branch 0 (get-in n [:kifu :moves]))]\n            (s/config! xyz :size [(:width rendered) :by (:height rendered)])\n            (reset! tree-atom rendered)\n            (.repaint ^JPanel xyz)))))\n\n    (s/scrollable\n      xyz\n      :id :rendered-tree\n      :hscroll :always\n      :vscroll :always)))\n\n(defn tree-panel [ctx]\n  (let [tree (rendered-tree-panel ctx)\n        container\n        (s/border-panel\n          :center tree)]\n\n    container))"
  },
  {
    "path": "src/igoki/ui/util.clj",
    "content": "(ns igoki.ui.util\n  (:require\n    [clojure.java.io :as io]\n    [seesaw.core :as s])\n  (:import\n    (javax.swing SwingUtilities JFrame JFileChooser)\n    (javax.swing.filechooser FileNameExtensionFilter)\n    (java.awt Desktop Desktop$Action)\n    (java.net URI)))\n\n\n(defn save-dialog [current-file success-fn]\n  (SwingUtilities/invokeLater\n    #(let [frame (JFrame. \"Save\")\n           chooser (JFileChooser.)]\n       (try\n         (.setAlwaysOnTop frame true)\n\n         (doto chooser\n           (.setSelectedFile (or current-file (io/file \"game.sgf\")))\n           (.setFileFilter (FileNameExtensionFilter. \"SGF Files\" (into-array [\"sgf\"]))))\n\n         (when\n           (= JFileChooser/APPROVE_OPTION (.showSaveDialog chooser frame))\n           (success-fn (.getSelectedFile chooser)))\n         (finally (.dispose frame))))))\n\n(defn load-dialog [success-fn & [start-dir]]\n  (SwingUtilities/invokeLater\n    #(let [frame (JFrame. \"Load\")\n           chooser (if start-dir (JFileChooser. ^String start-dir) (JFileChooser.))]\n       (try\n         (.setAlwaysOnTop frame true)\n         (doto chooser\n           (.setFileFilter (FileNameExtensionFilter. \"SGF Files\" (into-array [\"sgf\"]))))\n\n         (when\n           (= JFileChooser/APPROVE_OPTION (.showOpenDialog chooser frame))\n           (success-fn (.getSelectedFile chooser)))\n         (finally (.dispose frame))))))\n\n\n(defn open [^String url]\n  (cond\n    ;; Linux support\n    (not= -1\n      (try\n        (.read\n          (.getInputStream\n            (.exec (Runtime/getRuntime)\n              (into-array [\"which\" \"xdg-open\"]))))\n        (catch Exception e -1)))\n    (.exec (Runtime/getRuntime) (into-array [\"xdg-open\" url]))\n\n    ;; If can browse\n    (and (Desktop/isDesktopSupported)\n      (.isSupported (Desktop/getDesktop) Desktop$Action/BROWSE))\n    (.browse (Desktop/getDesktop) (URI. url))\n\n    :else\n    (s/alert (str \"Cannot browse directly, open your browser to the URL: \" url)\n      :type :warning)))"
  },
  {
    "path": "src/igoki/util/crypto.clj",
    "content": "(ns igoki.util.crypto\n  (:import\n    [javax.crypto Cipher]\n    [javax.crypto.spec SecretKeySpec]\n    [java.security MessageDigest]\n    [java.util Base64 Base64$Encoder Base64$Decoder]))\n\n;; This is just meant to do basic crypto over your local settings file so that it's not just plaintext.\n;; If you're not comfortable with that, simply don't 'remember me'.\n;; PR's welcome if you have a better idea?\n\n(def SECRET\n  \"igoki-not-that-secure\")\n\n\n(def ^SecretKeySpec KEY\n  (let [sha (MessageDigest/getInstance \"SHA-1\")\n        ba  (->> (.digest sha (.getBytes SECRET \"UTF-8\"))\n              (take 16)\n              byte-array)]\n    (SecretKeySpec. ba \"AES\")))\n\n\n(def ^Base64$Encoder b64-encoder\n  (.withoutPadding\n    (Base64/getUrlEncoder)))\n\n(def ^Base64$Decoder b64-decoder (Base64/getUrlDecoder))\n\n\n(def ^Cipher encrypter\n  (doto (Cipher/getInstance \"AES\")\n    (.init Cipher/ENCRYPT_MODE KEY)))\n\n(def ^Cipher decrypter\n  (doto (Cipher/getInstance \"AES\")\n    (.init Cipher/DECRYPT_MODE KEY)))\n\n\n(defn encrypt [s]\n  (->> (.doFinal encrypter (.getBytes s \"UTF-8\"))\n    (.encodeToString b64-encoder)))\n\n\n(defn -decrypt [^String s]\n  (String.\n    (->> (.decode b64-decoder s)\n      (.doFinal decrypter))\n    \"UTF-8\"))\n\n\n(defn decrypt [s]\n  (try\n    (-decrypt s)\n    (catch Exception e\n      nil)))"
  },
  {
    "path": "src/igoki/util.clj",
    "content": "(ns igoki.util\n  (:require\n    [clojure.java.io :as io])\n  (:import\n    (org.opencv.core Mat Size CvType Point MatOfPoint2f MatOfPoint)\n    (org.opencv.utils Converters)\n    (java.awt.image BufferedImage DataBufferByte)\n    (de.schlichtherle.truezip.file TFile TArchiveDetector)\n    (java.io InputStream ByteArrayInputStream ByteArrayOutputStream)\n    (org.opencv.imgcodecs Imgcodecs)))\n\n(defn star-points [size]\n  (case size\n    9 [[2 2] [4 4] [2 6] [6 2] [6 6]]\n    13 [[3 3] [6 6] [3 9] [9 3] [9 9]]\n    (for [x (range 3) y (range 3)]\n      [(+ 3 (* x 6)) (+ 3 (* y 6))])))\n\n(defn mat-to-buffered-image [^Mat frame ^BufferedImage bimg]\n  (let [type\n        (case (.channels frame)\n          1 BufferedImage/TYPE_BYTE_GRAY\n          3 BufferedImage/TYPE_3BYTE_BGR)\n\n        image\n        (cond\n          (and bimg\n            (= type (.getType bimg))\n            (= (.getWidth bimg) (.width frame))\n            (= (.getHeight bimg) (.height frame)))\n          bimg\n\n          :else\n          (BufferedImage. (.width frame) (.height frame) type))\n\n        raster (.getRaster image)\n        data-buffer ^DataBufferByte (.getDataBuffer raster)\n        data (.getData data-buffer)]\n    (.get frame 0 0 data)\n    image))\n\n(defn mat-to-pimage [^Mat frame ^BufferedImage oldbuffer]\n  (when (and (> (.rows frame) 0) (> (.cols frame) 0))\n    {:bufimg (mat-to-buffered-image frame oldbuffer)}))\n\n(defmacro with-release\n  \"A let block, calling .release on each provided binding at the end, in a finally block.\"\n  [bindings & body]\n  (let [release (map (fn [b] `(.release ~(first b))) (partition 2 bindings))]\n    `(let ~bindings\n       (try\n         ~@body\n         (finally\n           ~@release)))))\n\n(defn line-length-squared [[[x1 y1] [x2 y2] :as line]]\n  (+ (* (- y2 y1) (- y2 y1)) (* (- x2 x1) (- x2 x1))))\n\n(defn line-length [[[x1 y1] [x2 y2] :as line]]\n  (Math/sqrt (line-length-squared line)))\n\n(defn line-to-point-dist [[[x1 y1] [x2 y2] :as line] [x0 y0]]\n  (/ (Math/abs (+ (- (* x0 (- y2 y1)) (* y0 (- x2 x1)) (* y2 x1)) (* x2 y1)))\n     (line-length line)))\n\n(defn point-along-line [[[p1x p1y] [p2x p2y] :as line] percent]\n  [(+ p1x (* (- p2x p1x) percent))\n   (+ p1y (* (- p2y p1y) percent))])\n\n(defn update-edges [points]\n  (cond\n    (< (count points) 4)\n    []\n\n    :else\n    (partition 2 (interleave points (take 4 (drop 1 (cycle points)))))))\n\n(defn update-closest-point [points p]\n  (let [indexed-dist\n        (->>\n          points\n          (map-indexed (fn [i g] [(line-length-squared [g p]) i]))\n          sort)\n        [_ i :as e] (first indexed-dist)]\n    (assoc points i p)))\n\n(defn flipped-line [[p1 p2]]\n  [p2 p1])\n\n\n(defn matrix [w h & data]\n  (doto (Mat. w h (CvType/CV_64F))\n    (.put 0 0 (double-array data))))\n\n(defn translate [m x y]\n  (doto m\n    (.put 0 2 (double-array [(+ x (first (.get m 0 2)))]))\n    (.put 1 2 (double-array [(+ y (first (.get m 1 2)))]))))\n\n(defn read-mat \"Ai, really?\" [{:keys [size type data]}]\n  (doto (Mat. (Size. (first size) (second size)) type)\n    (.put 0 0 (double-array data)))  )\n\n(defn write-mat \"Yes, really.\" [mat]\n  {:type (.type mat)\n   :size [(.width mat) (.height mat)]\n   :data (read-string (.replace (.dump mat) \";\\n \" \",\"))})\n\n(defmulti mat->seq class)\n(defmethod mat->seq :default [m]\n  (throw (RuntimeException. (str \"Unknown type for conversion to vec: \" (class m)))))\n\n(defmethod mat->seq MatOfPoint2f [m]\n  (map (fn [i] [(.-x i) (.-y i)]) (seq (.toArray m))))\n\n(defmethod mat->seq MatOfPoint [m]\n  (map (fn [i] [(.-x i) (.-y i)]) (seq (.toArray m))))\n\n(defn vec->mat\n  [mat vec]\n  (doto mat\n    (.fromList (map (fn [[x y]] (Point. x y)) vec))))\n\n(defmacro -->\n  \"Threads images through the forms. Passing images from call to call and relasing\n   all but the last image.\"\n  ([x] `(let [next# (Mat.)] (.copyTo ~x next#) next#))\n  ([x form]\n   `(let [img# ~x\n          next# (Mat.)]\n      (~(first form) img# next# ~@(next form))\n      next#))\n  ([x form & more]\n   `(--> (--> ~x ~form) ~@more)))\n\n(defn add-watch-path\n  \"Similar to add-watch, but takes a ks arg that narrow the scope down to watched changes on a\n  single path of an atom. The contents of the atom needs to be traversable with get-in\"\n  [atom watch-key ks f]\n  (add-watch\n    atom watch-key\n    (fn [_ _ old new]\n      (let [o (get-in old ks)\n            n (get-in new ks)]\n        (if-not\n          (= o n)\n          (f watch-key atom o n))))))\n\n\n(defn interleave-all \"Greedy version of `interleave`, Ref. http://goo.gl/KvzqWb.\"\n  ([] '())\n  ([c1] (lazy-seq c1))\n  ([c1 c2]\n   (lazy-seq\n     (let [s1 (seq c1) s2 (seq c2)]\n       (cond\n         (and s1 s2)\n         (cons (first s1) (cons (first s2)\n                                (interleave-all (rest s1) (rest s2))))\n         s1 s1\n         s2 s2))))\n  ([c1 c2 & colls]\n   (lazy-seq\n     (let [ss (filter identity (map seq (conj colls c2 c1)))]\n       (concat (map first ss)\n               (apply interleave-all (map rest ss)))))))\n\n(defn iupdate-in\n  \"update-in using loop/recur so that it doesn't blow the stack\"\n  [m ks f & args]\n  (loop [oks ks\n         a (get-in m ks)\n         s (apply f a args)]\n    (if oks\n      (let [r (butlast oks)]\n        (recur r (get-in m oks) (assoc (get-in m r) (last oks) s)))\n      s)))\n\n(defn std-dev\n  \"Find the standard deviation of a given number of samples (list of numbers)\"\n  [samples]\n  (let [n (count samples)\n        mean (if (zero? n) 0 (/ (reduce + samples) n))\n        intermediate (map #(Math/pow (- %1 mean) 2) samples)]\n    (if (zero? n)\n      0 (Math/sqrt\n          (/ (reduce + intermediate) n)))))\n\n\n(defn zip-add-file [zipname destname ^InputStream input]\n  (let [f (TFile. ^String zipname ^String destname (TArchiveDetector.\".zip\"))]\n    (TFile/cp input f)))\n\n(defn zip-add-file-string [zipname destname ^String input]\n  (zip-add-file zipname destname (ByteArrayInputStream. (.getBytes input))))\n\n\n(defn zip-read-file [zipname filename]\n  (let [out (ByteArrayOutputStream.)]\n    (.output (TFile. ^String zipname ^String filename) out)\n    (.toByteArray out)))\n\n(defn hamming-dist [s1 s2]\n  (let [n (min (count s1) (count s2))\n        dist* (fn [^long n ^long i ^long a]\n                (if (zero? n)\n                  a\n                  (recur (dec n)\n                         (inc i)\n                         (if (not= (get s1 i) (get s2 i))\n                           (inc a)\n                           a))))]\n    (dist* n 0 (- (max (count s1) (count s2)) n))))"
  }
]