Full Code of CmdrDats/igoki for AI

master ed463901650b cached
40 files
214.4 KB
64.2k tokens
1 requests
Download .txt
Showing preview only (226K chars total). Download the full file or copy to clipboard to get everything.
Repository: CmdrDats/igoki
Branch: master
Commit: ed463901650b
Files: 40
Total size: 214.4 KB

Directory structure:
gitextract_2g4iif2j/

├── .github/
│   └── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── doc/
│   └── intro.md
├── launch4j.xml
├── project.clj
├── raw/
│   ├── sgf/
│   │   ├── 147.sgf
│   │   └── 1988-06-20.sgf
│   └── splash.xcf
├── resources/
│   ├── convnet.cnet
│   ├── log4j.properties
│   ├── logging.properties
│   ├── mycertfile.pem
│   ├── ogs.truststore
│   └── supersimple.cnet
└── src/
    └── igoki/
        ├── camera.clj
        ├── core.clj
        ├── game.clj
        ├── inferrence.clj
        ├── integration/
        │   ├── ogs.clj
        │   └── robot.clj
        ├── litequil.clj
        ├── projector.clj
        ├── scratch/
        │   ├── scratch.clj
        │   └── training.clj
        ├── sgf.clj
        ├── simulated.clj
        ├── sound/
        │   ├── announce.clj
        │   └── sound.clj
        ├── ui/
        │   ├── calibration.clj
        │   ├── game.clj
        │   ├── main.clj
        │   ├── ogs.clj
        │   ├── projector.clj
        │   ├── robot.clj
        │   ├── tree.clj
        │   └── util.clj
        ├── util/
        │   └── crypto.clj
        └── util.clj

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

ko_fi: cmdrdats


================================================
FILE: .gitignore
================================================
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
/.creds
capture
resources/*.edn
*.log
node_modules
resources/samples
/.idea
/igoki.iml
/ogs.edn
/winbuild/

================================================
FILE: LICENSE
================================================
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.

1. DEFINITIONS

"Contribution" means:

a) in the case of the initial Contributor, the initial code and
documentation distributed under this Agreement, and

b) in the case of each subsequent Contributor:

i) changes to the Program, and

ii) additions to the Program;

where such changes and/or additions to the Program originate from and are
distributed by that particular Contributor. A Contribution 'originates' from
a Contributor if it was added to the Program by such Contributor itself or
anyone acting on such Contributor's behalf. Contributions do not include
additions to the Program which: (i) are separate modules of software
distributed in conjunction with the Program under their own license
agreement, and (ii) are not derivative works of the Program.

"Contributor" means any person or entity that distributes the Program.

"Licensed Patents" mean patent claims licensable by a Contributor which are
necessarily infringed by the use or sale of its Contribution alone or when
combined with the Program.

"Program" means the Contributions distributed in accordance with this
Agreement.

"Recipient" means anyone who receives the Program under this Agreement,
including all Contributors.

2. GRANT OF RIGHTS

a) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free copyright license to
reproduce, prepare derivative works of, publicly display, publicly perform,
distribute and sublicense the Contribution of such Contributor, if any, and
such derivative works, in source code and object code form.

b) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free patent license under
Licensed Patents to make, use, sell, offer to sell, import and otherwise
transfer the Contribution of such Contributor, if any, in source code and
object code form.  This patent license shall apply to the combination of the
Contribution and the Program if, at the time the Contribution is added by the
Contributor, such addition of the Contribution causes such combination to be
covered by the Licensed Patents. The patent license shall not apply to any
other combinations which include the Contribution. No hardware per se is
licensed hereunder.

c) Recipient understands that although each Contributor grants the licenses
to its Contributions set forth herein, no assurances are provided by any
Contributor that the Program does not infringe the patent or other
intellectual property rights of any other entity. Each Contributor disclaims
any liability to Recipient for claims brought by any other entity based on
infringement of intellectual property rights or otherwise. As a condition to
exercising the rights and licenses granted hereunder, each Recipient hereby
assumes sole responsibility to secure any other intellectual property rights
needed, if any. For example, if a third party patent license is required to
allow Recipient to distribute the Program, it is Recipient's responsibility
to acquire that license before distributing the Program.

d) Each Contributor represents that to its knowledge it has sufficient
copyright rights in its Contribution, if any, to grant the copyright license
set forth in this Agreement.

3. REQUIREMENTS

A Contributor may choose to distribute the Program in object code form under
its own license agreement, provided that:

a) it complies with the terms and conditions of this Agreement; and

b) its license agreement:

i) effectively disclaims on behalf of all Contributors all warranties and
conditions, express and implied, including warranties or conditions of title
and non-infringement, and implied warranties or conditions of merchantability
and fitness for a particular purpose;

ii) effectively excludes on behalf of all Contributors all liability for
damages, including direct, indirect, special, incidental and consequential
damages, such as lost profits;

iii) states that any provisions which differ from this Agreement are offered
by that Contributor alone and not by any other party; and

iv) states that source code for the Program is available from such
Contributor, and informs licensees how to obtain it in a reasonable manner on
or through a medium customarily used for software exchange.

When the Program is made available in source code form:

a) it must be made available under this Agreement; and

b) a copy of this Agreement must be included with each copy of the Program.

Contributors may not remove or alter any copyright notices contained within
the Program.

Each Contributor must identify itself as the originator of its Contribution,
if any, in a manner that reasonably allows subsequent Recipients to identify
the originator of the Contribution.

4. COMMERCIAL DISTRIBUTION

Commercial distributors of software may accept certain responsibilities with
respect to end users, business partners and the like. While this license is
intended to facilitate the commercial use of the Program, the Contributor who
includes the Program in a commercial product offering should do so in a
manner which does not create potential liability for other Contributors.
Therefore, if a Contributor includes the Program in a commercial product
offering, such Contributor ("Commercial Contributor") hereby agrees to defend
and indemnify every other Contributor ("Indemnified Contributor") against any
losses, damages and costs (collectively "Losses") arising from claims,
lawsuits and other legal actions brought by a third party against the
Indemnified Contributor to the extent caused by the acts or omissions of such
Commercial Contributor in connection with its distribution of the Program in
a commercial product offering.  The obligations in this section do not apply
to any claims or Losses relating to any actual or alleged intellectual
property infringement. In order to qualify, an Indemnified Contributor must:
a) promptly notify the Commercial Contributor in writing of such claim, and
b) allow the Commercial Contributor tocontrol, and cooperate with the
Commercial Contributor in, the defense and any related settlement
negotiations. The Indemnified Contributor may participate in any such claim
at its own expense.

For example, a Contributor might include the Program in a commercial product
offering, Product X. That Contributor is then a Commercial Contributor. If
that Commercial Contributor then makes performance claims, or offers
warranties related to Product X, those performance claims and warranties are
such Commercial Contributor's responsibility alone. Under this section, the
Commercial Contributor would have to defend claims against the other
Contributors related to those performance claims and warranties, and if a
court requires any other Contributor to pay any damages as a result, the
Commercial Contributor must pay those damages.

5. NO WARRANTY

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
appropriateness of using and distributing the Program and assumes all risks
associated with its exercise of rights under this Agreement , including but
not limited to the risks and costs of program errors, compliance with
applicable laws, damage to or loss of data, programs or equipment, and
unavailability or interruption of operations.

6. DISCLAIMER OF LIABILITY

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGES.

7. GENERAL

If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of the
remainder of the terms of this Agreement, and without further action by the
parties hereto, such provision shall be reformed to the minimum extent
necessary to make such provision valid and enforceable.

If Recipient institutes patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Program itself
(excluding combinations of the Program with other software or hardware)
infringes such Recipient's patent(s), then such Recipient's rights granted
under Section 2(b) shall terminate as of the date such litigation is filed.

All Recipient's rights under this Agreement shall terminate if it fails to
comply with any of the material terms or conditions of this Agreement and
does not cure such failure in a reasonable period of time after becoming
aware of such noncompliance. If all Recipient's rights under this Agreement
terminate, Recipient agrees to cease use and distribution of the Program as
soon as reasonably practicable. However, Recipient's obligations under this
Agreement and any licenses granted by Recipient relating to the Program shall
continue and survive.

Everyone is permitted to copy and distribute copies of this Agreement, but in
order to avoid inconsistency the Agreement is copyrighted and may only be
modified in the following manner. The Agreement Steward reserves the right to
publish new versions (including revisions) of this Agreement from time to
time. No one other than the Agreement Steward has the right to modify this
Agreement. The Eclipse Foundation is the initial Agreement Steward. The
Eclipse Foundation may assign the responsibility to serve as the Agreement
Steward to a suitable separate entity. Each new version of the Agreement will
be given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the
Agreement under which it was received. In addition, after a new version of
the Agreement is published, Contributor may elect to distribute the Program
(including its Contributions) under the new version. Except as expressly
stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
licenses to the intellectual property of any Contributor under this
Agreement, whether expressly, by implication, estoppel or otherwise. All
rights in the Program not expressly granted under this Agreement are
reserved.

This Agreement is governed by the laws of the State of New York and the
intellectual property laws of the United States of America. No party to this
Agreement will bring a legal action under this Agreement more than one year
after the cause of action arose. Each party waives its rights to a jury trial
in any resulting litigation.


================================================
FILE: README.md
================================================
# igoki

Bridge the gap between playing Go on a physical board and digitally.

Reasons for wanting to play on a physical board vary from person to person, for me:
 - I really love the tactile feel of the stones and board and the aesthetic of the game as the stones jostle around.
 - Prefer the serenity of not having to stare at a screen for the game.

igoki lets you do the above while still having the ability to connect and play with people across the internet!

Some things you can do with igoki:
 - Play online (currently online-go.com is implemented, would like to implement more backends)
 - Record a live game between two players
 - Review SGF's

# Support

If you like this project, I would love if you want to jump in and help out with the codebase most of all - 
igoki started out as a proof of concept for me and then gradually grew into what it is today. That means there's
plenty of cobwebs and weird design issues that stem from the lack of coherent upfront design :)

Take a look at the project board here: https://github.com/CmdrDats/igoki/projects/1 - I actively use that to
manage what to focus my efforts on next.

However, if you can't contribute to the codebase, please consider supporting me with a ko-fi donation:

[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/D1D76ML2E)

Every little bit will help this project along immensely!
 
# Setup

## Hardware 
It may seem daunting, but the minimum equipment you _really_ need is a webcam and a way to point it with sufficient angle at
a Go board. You can get creative here, even a laptop webcam will do the trick, albeit not the most comfortable.

However, if you want to get the best experience out of the app, you'll want:

 - A PC/Laptop with **Windows**, **Linux** or **OSX** operating systems should all work (please report your mileage)
 - A decent **HD webcam** - really, search for 'webcam hd' on Amazon or wherever - it should do the trick.
 - A **portable projector** of some kind - optional, but really neato - search 'portable projector' - make sure it works 
   with your PC - a standard camera mount screw support will make it easier to attach to tripod.
   I have a rubbish 320x240 low brightness projector, it's a bit a pain, but it works ok.
 - A **tripod** of some description - to mount your webcam and projector. It can be el-cheapo.
 - **USB extension** - usually webcam cables aren't that long, and I find I need an extension to be
   able to put the camera where I want it.

And that's really it, you're good to go.

## Software

Head on over to the [Releases](https://github.com/CmdrDats/igoki/releases) for a universal
java binary that should run most anywhere-ish. You will need a Java runtime installed in order to run it though:

- For Windows (64bit only), the 0.7 release has a wrapper executable + Java runtime bundled - download the `igoki-windows.zip` **and** the
`igoki.jar` files - extract the `zip` and place the `jar` file in the same folder, then run the `igoki.exe`

- 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 
  download or [java.com](java.com) for java 1.8, though I'll be working off latest Java SDK.
- Once Java is installed, you should be able to double-click the `igoki.jar` and it should run.
- Failing that, on windows you can try:
    - win-r, type `cmd`, hit enter
    - type `cd Downloads` and hit enter (unless you put it somewhere else than in downloads)
    - type `java -jar igoki.jar` and hit enter.
    - It should start. if not, you should get some kind of error. Ping me with a new issue on github
      with that message if that is the case.
- On linux/osx, if you can `java -version` and it gives you the Java version, then you can just:
    - `cd` to the installation folder and `java -jar igoki.jar` 

## Process

To get going, you'll go through the following steps:

1. Setup camera, select the board corners and check that it's reading alright.
2. For Online-Go: Setup API keys, then username/password to login
3. For Manual integration: Setup frame, enter details, start recording
4. For Game Review: Load SGF file
5. For Game recording... just play and Save SGF when you're done!

---

When you start the app, you'll be greeted with:

![Initial Screen](doc/images/screen1.png)

Going through the various panels:

 - **Camera setup screen** (Top left)
    - Here you will select your board size, camera source 
    - You're also able to open the projector window (more on that later, in the 'Project Setup' section)
 - **Game state screen** (Top right)
    - Here you will see an overview of the current game state and you'll see highlights where igoki
      thinks the stones are, or should be placed.
    - 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
    - The `<` and `>` lets you move forward or backward in the game, `pass` lets you record a pass.
    - `Show Branches` will show you next moves if you aren't at the end of the game - useful for reviewing
    - Announce lets you setup a voice to announce play coordinates. Useful if you don't have a projector, or just want that
      tournament-ish feel where your every recorded move is spoke aloud.
 - **OGS tab** (Bottom left)
    - Online-go integration - we'll cover these in a bit.
 - **Simulation tab** (Bottom left)
    - You can specify a simulation camera in the camera dropdown, and then this will let you
      pretend to have a real camera board. This is useful for when I do dev on various features
      so that I don't have to set everything up.
    - I don't imagine it's particularly useful for anything else, so you can ignore.
 - **Tree tab** (Bottom right
    - Just a tree showing the current game with branches. Nothing fancy.

### Camera calibration

Select a camera in the dropdown until you see your camera feed where you see your board.

Then, point your `finger` to the upper left corner of the board. Click on that corner on the camera
view and then the other 3 corners in a clockwise fashion. I find that provides the most sensible results.

You can click and drag the corners to fine tune them - sometimes you may want to make the view larger
so that you can be a bit more accurate here.

![Camera Grid Image](doc/images/camera.png)

You should see a grid pretty much exactly over your board lines.

Put a bunch of stones down on the intersections around the board and adjust a little so that the
intersections roughly lies on the center on _top_ of the stones. I find that gives a super solid reading result.

![Camera Grid Image with Stones](doc/images/camera_stones.png)

### Record your game

If you wanted to simply record your game, that's it, go to `File` -> `New SGF` in case it recorded
some stuff during caliration, and you're all ready to go. `File` -> `Save SGF...` when you're done

If you want to announce your moves while you're playing, set those up in the game panel (upper right)


### Projector setup (optional)

![Projector setup](doc/images/projector.jpg)

Connect up your projector and get it working in your OS as an extended display - 
some guidance if you need, for windows or OSX: [At this link](https://www.bu.edu/comtech/faculty-staff/classroom-av/instructor-station-desktop-mirroring/)

Once that is setup, make sure the projector is pointed at and covering your entire board with a bit
of space to spare.

Now click the `Projector` tab (next to OGS) - you'll see a 'Projector Window button'. Click that,
and it should open a new blank window - move that to your projector monitor and maximize it. 

Then, click the 'Calibration Grid' button and you'll have something like this showing:

![Projector Checkerboard](doc/images/projector-checker.png)

the checkerboard should ideally fit on your game board and be in the camera frame

Get a blank white sheet of paper and place it so that the checkerboard pattern is fully on the page - 
this will let the camera correctly see the checkerboard pattern easiest.

You 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.

The window should go black and it's all ready to roll!

### Announcer

![Announcer options](doc/images/announcer.png)

Not a hardware setup, but if you want to hear coordinates,
remember to select the color you want to announce for and the language. Particularly useful if no
projector.

### Review a game

At this point, you can very effectively review games.

If you want to review a game, you can `File` -> `Load SGF...` to open the SGF you'd like to review.
Click `Show Branches` for the best experience here.

**A feature to note here:** when you pull stones off the board, igoki will do a backward search for
a previous game state like the one you've gone to, and automatically jump to that point in the game
if it can. If you play a different way, it'll start branching in the SGF. Quite handy for reviewing and
recording variations, I would say!

### Play online on online-go.com

![OGS](doc/images/ogs.png)

For this you'll need to setup your igoki instance on OGS's API thingy. Click the 'Open Browser' or browse
to [OGS Applications](https://online-go.com/oauth2/applications/) to set this up. I have tried to 
keep the interface consistent with OGS, so the settings will be the same (though I've seen 'Client Type' as 'Secret'
and sometimes 'Confidential' - I'm not sure - both seem the same)

Once you have your application setup and you have a Client ID and Client Secret, plug those in, along
with your normal Username/Password - igoki will remember all the settings, besides password unless you check
that option.

If that's successful, you'll see the game list:

![OGS game list](doc/images/ogs-gamelist.png)

Just click on one and `Connect to selected` - and you should be good to go - Start playing!

### Play online using manual screen capture (very early implementation)

As a stopgap to interact with most other Go clients or programs, you can setup a manual frame
to capture and relay mouse clicks directly on screen.

To set this up, click on the `Manual` tab at the bottom of the screen, then click the
`Open capture frame` button. This will create a floating window that you can drag and align with the game
board you want to integrate with.

Because igoki won't be able to tell move order or anything, you need to let it know who the next
player is that needs to go, and who it will be simulating mouse clicks for. There is a few
more options mostly for the saved SGF at the end:

![Manual integration](doc/images/manual.png)

You can pause the capturing or stop it at any time.

A few notes and tips:

[Sabaki](https://sabaki.yichuanshen.de/) highlights the last move quite strongly, the strong black mark
on the white stone confuses igoki's neural net. I found I get better mileage when I make the sabaki window
quite small if that is a problem.

[Katrain](https://github.com/sanderland/katrain) on windows really wants to have focus before the move
is made, this makes testing with the simulation mode somewhat tricky (it doesn't work unless I jimmy a double click of sorts xD)
- You might find this is the case with different platforms - so focus the app after setup before you start playing.

On jdk 8, there's a bug where the first 'mouse move' command doesn't go to remotely the right place,
so I've just worked around it by telling it to move the mouse 5 times. hopefully that's enough to coerce it, but please
report back.

## Usage
 
 Head on over to the [Releases](https://github.com/CmdrDats/igoki/releases) for a universal
 java binary. 

 This project is written in clojure so if you want to build from scratch, you need to install
 [Leiningen](http://leiningen.org) 
 
 Once Leiningen is installed, clone this repo and run `lein run`, 
 it will start up the frame and guide you through calibration.
 
 Alternatively, if you are doing development on this project, fire up a `lein repl` and it'll
 be all setup to connect an IDE repl to it.

use Launch4j to build the exe, if you want that.

 
## License

Copyright © 2021 Deon Moolman

Distributed under the Eclipse Public License either version 1.0 or (at
your option) any later version.


================================================
FILE: doc/intro.md
================================================
# Introduction to badukpro

TODO: write [great documentation](http://jacobian.org/writing/what-to-write/)


================================================
FILE: launch4j.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<launch4jConfig>
  <dontWrapJar>true</dontWrapJar>
  <headerType>console</headerType>
  <jar>igoki.jar</jar>
  <outfile>C:\Users\Deon\IdeaProjects\igoki\winbuild\igoki.exe</outfile>
  <errTitle></errTitle>
  <cmdLine></cmdLine>
  <chdir>.</chdir>
  <priority>normal</priority>
  <downloadUrl>http://java.com/download</downloadUrl>
  <supportUrl></supportUrl>
  <stayAlive>false</stayAlive>
  <restartOnCrash>false</restartOnCrash>
  <manifest></manifest>
  <icon>C:\Users\Deon\IdeaProjects\igoki\raw\igoki48.ico</icon>
  <jre>
    <path>jdk-17.0.1</path>
    <bundledJre64Bit>true</bundledJre64Bit>
    <bundledJreAsFallback>false</bundledJreAsFallback>
    <minVersion>1.8</minVersion>
    <maxVersion></maxVersion>
    <jdkPreference>preferJdk</jdkPreference>
    <runtimeBits>64/32</runtimeBits>
  </jre>
</launch4jConfig>

================================================
FILE: project.clj
================================================
(defproject igoki "0.8.0"
  :description "Igoki, physical Go board/OGS interface"
  :url "http://github.com/CmdrDats/igoki"
  :license
  {:name "Eclipse Public License"
   :url "http://www.eclipse.org/legal/epl-v10.html"}

  :dependencies
  [[org.clojure/clojure "1.10.3"]
   #_[com.google.guava/guava "20.0"]
   [clj-http "3.12.3"]
   [seesaw "1.5.0"]
   [org.openpnp/opencv "4.5.1-2"]
   [cheshire "5.10.1"]
   [de.schlichtherle.truezip/truezip-file "7.7.10"]
   [de.schlichtherle.truezip/truezip-driver-zip "7.7.10"]

   [io.socket/socket.io-client "0.9.0"]

   [org.clojure/tools.logging "1.1.0"]

   [log4j "1.2.17"]
   [org.slf4j/slf4j-api "1.7.32"]
   [org.slf4j/jul-to-slf4j "1.7.32"]
   [org.slf4j/slf4j-log4j12 "1.7.32"]

   [org.nd4j/nd4j "1.0.0-M1.1" :extension "pom"]
   [org.nd4j/nd4j-native-platform "1.0.0-M1.1"]
   [org.deeplearning4j/deeplearning4j-core "1.0.0-M1.1"]]

  :main igoki.core

  :repl-options
  {:welcome "Welcome to igoki"
   :init-ns igoki.core
   :init (-main)}

  #_#_:min-lein-version "2.5.0"

  :uberjar-name "igoki.jar")


================================================
FILE: raw/sgf/147.sgf
================================================
(;
GM[1]
SZ[19]
AW[dd][ci][dk][dl][dn][dp][gp][ip][lp]
AB[fq][fp][fo][fm][ek][ej][in][pq][pd]
LB[ln:A][ir:D][np:B][po:C]
C[Problem 147. Black to play.  

The 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. ]
(;B[ln]
TE[2]
C[Correct Answer.  

Black should play a large-scale move by capping at 1. ]
(;W[dr]
LB[gq:A]
C[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. <= ]
)
(;W[nq]
C[If White 2 here, ... ]
;B[po]
C[... Black 3 has a good feel to it. <= ]
))
(;B[np]
C[Failure.  

Black 1 is very bad because ... ]
;W[ln]
C[... it drives White into the center, compromising Black's thickness there. <= ]
)
)


================================================
FILE: raw/sgf/1988-06-20.sgf
================================================
(;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]
;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]
;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]
;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]
;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]
;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]
;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]
;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]
;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]
;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]
;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]
;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]
;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]
;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]
;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]
;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]
;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.])

================================================
FILE: resources/log4j.properties
================================================
# Active appenders and base log level
log4j.rootLogger=ERROR, console

# Console appender
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{DATE} %-5p %c - %m%n

# Log Levels
log4j.global=FINEST
log4j.logger.io=FINEST
log4j.logger.igoki=TRACE

# 3rd party logging
log4j.logger.ring=ERROR


================================================
FILE: resources/logging.properties
================================================
 handers = org.slf4j.bridge.SLF4JBridgeHandler


================================================
FILE: resources/mycertfile.pem
================================================
-----BEGIN CERTIFICATE-----
MIIHvDCCBqSgAwIBAgIHB5t1bc/BmTANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE
BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE
aWdpdGFsIENlcnRpZmljYXRlIFNpZ25pbmcxODA2BgNVBAMTL1N0YXJ0Q29tIENs
YXNzIDIgUHJpbWFyeSBJbnRlcm1lZGlhdGUgU2VydmVyIENBMB4XDTE1MDcwNTAz
NDk1MFoXDTE3MDcwNDIxMDEzN1owgZAxCzAJBgNVBAYTAlVTMRcwFQYDVQQIEw5O
b3J0aCBDYXJvbGluYTEPMA0GA1UEBxMGRHVyaGFtMRMwEQYDVQQKEwpBa2l0YSBO
b2VrMRowGAYDVQQDExF3d3cub25saW5lLWdvLmNvbTEmMCQGCSqGSIb3DQEJARYX
d2VibWFzdGVyQG9ubGluZS1nby5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQCqm4qoq4U9CjchLV6D16aN7xDN4SaODHfsfOPCYHynyXTEp79r8/iA
mwIqY7uBv8fjKDl8D5AtEOQowaUYQEyvD/TbxY1KKWfq2EYNCslK49/FySbzCv1Y
VRVhTrQI3qMrstSfse1oprIp76Mw1NvphQfHz4udCLSshLqcMdmKwJQ3KknkdQ01
Bf3M/VyoudvomP+gZxvBbR8buaKFczn0nSA75xcb7y694v8JmIYhG0RM9Axgsc6O
Tj+QxeVZ7DzbaSMs8fQ2VwMcUnzdUje5V3D255IzhUzbNV/gvYgBKYVUKltGnFjz
diRimCBxd44pzik5cRUDQqbMplHT6uWtAgMBAAGjggQbMIIEFzAJBgNVHRMEAjAA
MAsGA1UdDwQEAwIDqDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwHQYD
VR0OBBYEFBgjlsMpQwe38n6TuZs6s/y4tCx2MB8GA1UdIwQYMBaAFBHbI0X9VMxq
cW+EigPXvvcBLyaGMIIBVQYDVR0RBIIBTDCCAUiCEXd3dy5vbmxpbmUtZ28uY29t
gg1vbmxpbmUtZ28uY29tghFnZ3Mub25saW5lLWdvLmNvbYISYmV0YS5vbmxpbmUt
Z28uY29tghRmb3J1bXMub25saW5lLWdvLmNvbYIVZ2dzYmV0YS5vbmxpbmUtZ28u
Y29tghZ3d3cuYmV0YS5vbmxpbmUtZ28uY29tgg9zLm9ubGluZS1nby5jb22CEnR1
cm4ub25saW5lLWdvLmNvbYISc3R1bi5vbmxpbmUtZ28uY29tghR3ZWJydGMub25s
aW5lLWdvLmNvbYIXdHJhbnNsYXRlLm9ubGluZS1nby5jb22CEWFwaS5vbmxpbmUt
Z28uY29tghJhcGkxLm9ubGluZS1nby5jb22CEmFwaTIub25saW5lLWdvLmNvbYIV
a3VyZW50by5vbmxpbmUtZ28uY29tMIIBVgYDVR0gBIIBTTCCAUkwCAYGZ4EMAQIC
MIIBOwYLKwYBBAGBtTcBAgMwggEqMC4GCCsGAQUFBwIBFiJodHRwOi8vd3d3LnN0
YXJ0c3NsLmNvbS9wb2xpY3kucGRmMIH3BggrBgEFBQcCAjCB6jAnFiBTdGFydENv
bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTADAgEBGoG+VGhpcyBjZXJ0aWZpY2F0
ZSB3YXMgaXNzdWVkIGFjY29yZGluZyB0byB0aGUgQ2xhc3MgMiBWYWxpZGF0aW9u
IHJlcXVpcmVtZW50cyBvZiB0aGUgU3RhcnRDb20gQ0EgcG9saWN5LCByZWxpYW5j
ZSBvbmx5IGZvciB0aGUgaW50ZW5kZWQgcHVycG9zZSBpbiBjb21wbGlhbmNlIG9m
IHRoZSByZWx5aW5nIHBhcnR5IG9ibGlnYXRpb25zLjA1BgNVHR8ELjAsMCqgKKAm
hiRodHRwOi8vY3JsLnN0YXJ0c3NsLmNvbS9jcnQyLWNybC5jcmwwgY4GCCsGAQUF
BwEBBIGBMH8wOQYIKwYBBQUHMAGGLWh0dHA6Ly9vY3NwLnN0YXJ0c3NsLmNvbS9z
dWIvY2xhc3MyL3NlcnZlci9jYTBCBggrBgEFBQcwAoY2aHR0cDovL2FpYS5zdGFy
dHNzbC5jb20vY2VydHMvc3ViLmNsYXNzMi5zZXJ2ZXIuY2EuY3J0MCMGA1UdEgQc
MBqGGGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tLzANBgkqhkiG9w0BAQsFAAOCAQEA
Fl+7hqCBCUbtZKcdSdJTdCwNtrmCsZ+TOWhmP1ZzkOf+HzizAG9BSsvBe+lw9gZL
doMTTVxihUiVFXWp1CR7G6DQo1gHRCH/N2c6WWUrE/n4DJmnLeV3wMBxEaSw5l46
v0VIpcoYiqDuvjyND4HOXMyIgzRlS2GpFSolFwCs+JebRVqrDVjbQkrg/+o2Dwl7
ZtPUs8JcgZAHeKqi3TcZ+OxI4MuaVCEXwrKRA70/8WeJ8PDSl2+BnZTO6FYTXClm
bjzF8sKEJbL0GN2NFIMlOhsG9yueiGQXzMdtESXrONifbEeEZFeHP8ZuZl98/HrK
kcn5yT6QYm1ACSIRCllvBA==
-----END CERTIFICATE-----


================================================
FILE: src/igoki/camera.clj
================================================
(ns igoki.camera
  (:require
    [igoki.util :as util]
    [igoki.simulated :as sim]
    [clojure.java.io :as io])
  (:import
    (org.opencv.core MatOfPoint2f Mat Rect Size Core)
    (org.opencv.videoio Videoio VideoCapture)
    (org.opencv.imgcodecs Imgcodecs)
    (java.util LinkedList UUID)
    (java.io File)
    (org.opencv.imgproc Imgproc)
    (org.datavec.image.loader ImageLoader)
    (org.deeplearning4j.util ModelSerializer)
    (org.opencv.calib3d Calib3d)
    (org.nd4j.linalg.api.ndarray INDArray)
    (org.deeplearning4j.nn.multilayer MultiLayerNetwork)
    (java.awt.image BufferedImage)))

;; Step 1. Setup camera
(defn setup-camera [ctx camidx]
  (let [^VideoCapture video (VideoCapture. ^int camidx Videoio/CAP_ANY)]
    (swap! ctx update
      :camera assoc
      :video video
      :stopped false)
    video))

;; Step 2. Read camera

(defn camera-read [ctx video]
  (let [camera (:camera @ctx)]
    (cond
      (or (:stopped camera) (not video)) nil

      (not (.isOpened video))
      (println "Error: Camera not opened")

      :else
      (try
        (let [frame (or (:frame camera) (Mat.))]
          (when (.read video frame)

            (swap!
              ctx update :camera
              #(assoc %
                 :raw frame
                 ;; TODO: this chows memory - better to have a hook on update for each specific
                 ;; view - this will only be needed on the first screen.
                 :pimg (util/mat-to-pimage frame (get-in % [:pimg :bufimg]))))))
        (Thread/sleep (or (-> @ctx :camera :read-delay) 500))
        (catch Exception e
          (println "exception thrown")
          (.printStackTrace e))))))

;; Helpful for debugging
(defn read-single [ctx camidx]
  (let [video (VideoCapture. (int camidx) Videoio/CAP_ANY)
        frame (Mat.)]
    (Thread/sleep 500)
    (.read video frame)
    (swap!
      ctx
      update :camera
      #(assoc %
         :raw frame
         :pimg (util/mat-to-pimage frame (get-in % [:pimg :bufimg]))))
    (.release video)))

;; For simulation stepping (debugging)
(defn read-file [ctx fname]
  (let [frame (Imgcodecs/imread (str "resources/" fname))]
    (swap!
      ctx update :camera
      #(assoc %
         :raw frame
         :pimg (util/mat-to-pimage frame (get-in % [:pimg :bufimg]))))))


;; Step 3. Specify points & find homography
(def block-size 10)

(defn target-points [size]
  (let [extent (* block-size size)]
    [[block-size block-size] [extent block-size] [extent extent] [block-size extent]]))

(defn update-homography [ctx]
  (util/with-release
    [target (MatOfPoint2f.)
     origpoints (MatOfPoint2f.)]
    (let [{{:keys [points size]} :goban :as context} ctx
          target (util/vec->mat target (target-points size))
          origpoints (util/vec->mat origpoints points)
          homography
          (Calib3d/findHomography ^MatOfPoint2f origpoints ^MatOfPoint2f target
            Calib3d/FM_RANSAC 3.0)]
      (if homography
        (assoc-in ctx [:view :homography] homography)
        ctx))))

;; Step 4. Flatten image
(defn sample-points [corners size]
  (let [[ctl ctr cbr cbl] corners
        divide
        (fn [[x1 y1] [x2 y2]]
          (let [xf (/ (- x2 x1) (dec size))
                yf (/ (- y2 y1) (dec size))]
            (map (fn [s] [(+ x1 (* s xf)) (+ y1 (* s yf))]) (range size))))
        leftedge (divide ctl cbl)
        rightedge (divide ctr cbr)]
    (map
      (fn [left right] (divide left right))
      leftedge rightedge)))



(defn gather-reference [context homography]
  (let [{{:keys [size]} :goban} context
        samplecorners (target-points size)
        samplepoints (sample-points samplecorners size)]
    {:homography homography
     :shift [0 0]
     :samplecorners samplecorners
     :samplepoints samplepoints}))

(defn update-reference [ctx]
  (let [{{:keys [homography]} :view :as context} @ctx]
    (when homography
      (swap! ctx assoc :view (gather-reference context homography)))))

(defn reverse-transform [ctx]
  (cond
    (<  (count (-> @ctx :goban :edges)) 4)
    (swap! ctx
      (fn [c]
        (->
          c
          (update :view dissoc :homography)
          (assoc-in [:goban :lines] []))))

    :else
    (do
      (swap! ctx update-homography)
      (let [context @ctx
            homography (-> context :view :homography)
            size (-> context :goban :size)
            d (dec size)
            [topleft topright bottomright bottomleft] (target-points size)]

        (when homography
          (util/with-release
            [ref (MatOfPoint2f.)
             pts (MatOfPoint2f.)]
            (util/vec->mat
              pts
              (mapcat
                (fn [t]
                  [(util/point-along-line [topleft topright] (/ t (dec size)))
                   (util/point-along-line [bottomleft bottomright] (/ t (dec size)))
                   (util/point-along-line [topleft bottomleft] (/ t (dec size)))
                   (util/point-along-line [topright bottomright] (/ t (dec size)))])
                (range 0 size)))
            (Core/perspectiveTransform pts ref (.inv (-> @ctx :view :homography)))

            (swap! ctx assoc-in [:goban :lines] (partition 2 (util/mat->seq ref)))
            (update-reference ctx)))))))

;; Step 5. Read board state.
(defn load-net [^File nm]
  (ModelSerializer/restoreMultiLayerNetwork nm))

(def net
  (let [tmp (File/createTempFile "igoki" "cnet")
        cnet (io/input-stream (io/resource "supersimple.cnet"))]
    (io/copy cnet tmp)
    (let [net (load-net tmp)]
      (.delete tmp)
      net)))

(def loader (ImageLoader. 10 10 3))

(defn ref-size-vec [size]
  [(* block-size (inc size)) (* block-size (inc size))])

(defn ref-size [size]
  (Size. (* block-size (inc size)) (* block-size (inc size))))

(defn eval-spot [img]
  (let [^INDArray d (.asMatrix loader img)
        _ (.divi d 255.0)
        d (.reshape d (int-array [1 300]))
        ^INDArray o (.output ^MultiLayerNetwork net d)]
    (for [i (range 3)]
      (try
        (.getFloat o (int i))
        (catch Exception e
          (.printStackTrace e))))))

(defn eval-net [flat px py]
  (let [smat (.submat flat (Rect. (- px (/ block-size 2)) (- py (/ block-size 2)) 10 10))
        img (util/mat-to-buffered-image smat nil)
        result (eval-spot img)]
    (.release smat)
    result))

(defn read-board [ctx]
  (let [{{:keys [homography samplepoints]} :view
         {:keys [raw flattened flattened-pimage]} :camera
         {:keys [size]} :goban} ctx
        new-flat (or flattened (Mat.))]

    (cond
      (not homography) ctx

      :else
      (do
        (Imgproc/warpPerspective raw new-flat homography (ref-size size))
        (let [brightness (/ (apply + (take 3 (seq (.val (Core/mean new-flat))))) 3.0)]
          (.convertTo new-flat new-flat -1 1 (- 140 brightness))
          (Core/normalize new-flat new-flat 0 255 Core/NORM_MINMAX))


        (let [board
              (mapv
                (fn [row]
                  (mapv
                    (fn [[px py]]
                      (let [[b e w] (eval-net new-flat px py)]
                        (cond
                          (> b 0.6) :b
                          (> w 0.8) :w)))
                    row))
                samplepoints)]
          (->
            ctx
            (assoc-in [:camera :flattened] new-flat)
            (assoc-in [:camera :flattened-pimage]
              (util/mat-to-pimage new-flat
                (:bufimg flattened-pimage)))
            (assoc :board board)))))))

(defn read-stones [ctx]
  (let [{:keys [view projector]} @ctx
        {:keys [samplepoints homography]} view]
    (when (and homography (not (:setting-up projector)))
      ;; TODO: this multiple swap thing, no good.
      (swap! ctx read-board)

      (util/with-release [src (MatOfPoint2f.) dst (MatOfPoint2f.)]
        (Core/perspectiveTransform
          (util/vec->mat src
            (apply concat samplepoints))
          dst
          (.inv homography))

        (swap! ctx assoc-in [:goban :camerapoints] (util/mat->seq dst))))))


;; Step 6. Repeat
(defn read-loop [ctx camidx]
  (when-not (-> @ctx :camera :stopped)
    (let [video (setup-camera ctx camidx)]
      (doto
        (Thread.
          ^Runnable
          #(when-not (-> @ctx :camera :stopped)
             (camera-read ctx video)
             (recur)))
        (.setDaemon true)
        (.start)))))

(defn stop-read-loop [ctx]
  (if-let [video ^VideoCapture (-> @ctx :camera :video)]
    (.release video))
  (swap! ctx update :camera assoc :stopped true :video nil))

(defn switch-read-loop [ctx camidx]
  (stop-read-loop ctx)
  (Thread/sleep (* 2 (or (-> @ctx :camera :read-delay) 1000)))
  (swap! ctx assoc-in [:camera :stopped] false)
  (read-loop ctx camidx))


(defn camera-updated [wk ctx old new]
  (try
    (read-stones ctx)
    (catch Exception e (.printStackTrace e))))

(defn update-corners [ctx points]
  (swap! ctx update :goban
    (fn [goban]
      (assoc
        goban
        :points points
        :edges (util/update-edges points))))
  (reverse-transform ctx))


(defn reset-board [ctx]
  (swap! ctx assoc :goban
    {:points []
     :size   19}))

(defn start-calibration [ctx]
  (when-not (:goban @ctx)
    (reset-board ctx))
  (util/add-watch-path ctx :goban-camera [:camera] #'camera-updated))

(defn stop-calibration [ctx]
  (remove-watch ctx :goban-camera))

(defn camera-image [ctx]
  (get-in @ctx [:camera :pimg :bufimg]))

(defn set-board-size [ctx size]
  (swap! ctx assoc-in [:goban :size] size)
  (reverse-transform ctx))

(defn cycle-size [ctx]
  (swap! ctx update-in [:goban :size]
    (fn [s]
      (case s 19 9 9 13 19)))
  (reverse-transform ctx))

(defn camera-size [ctx]
  (let [^BufferedImage c (camera-image ctx)]
    (cond
      (not c) nil
      :else [(.getWidth c) (.getHeight c)])))

(defn cycle-corners [ctx]
  (update-corners ctx (vec (take 4 (drop 1 (cycle (-> @ctx :goban :points)))))))

(defn select-camera [ctx camera-idx]
  (sim/stop)
  (stop-read-loop ctx)
  (update-corners ctx [])
  (case camera-idx
    -2 nil
    -1 (sim/start-simulation ctx)
    (switch-read-loop ctx camera-idx)))




;; Older testing code...

;; This was a good idea, but need to retrain the network to recognize these, so not
;; entirely sure it's worthwhile? Would have to measure.
(defn illuminate-correct [m]
  (util/with-release [lab-image (Mat.) equalized (Mat.)]
    (let [planes (LinkedList.)]
      (Imgproc/cvtColor m lab-image Imgproc/COLOR_BGR2Lab)
      (Core/split lab-image planes)
      (Imgproc/equalizeHist (first planes) equalized)
      (.copyTo equalized (first planes))
      (Core/merge planes lab-image)
      (Imgproc/cvtColor lab-image m Imgproc/COLOR_Lab2BGR)
      m)))


;; This is likely used to prep training data?
(defn dump-points [ctx]
  (let [flat (-> ctx :camera :flattened)
        samplepoints (-> ctx :view :samplepoints)
        board (-> ctx :board)
        id (first (.split (.toString (UUID/randomUUID)) "[-]"))]
    (when flat
      (doseq [[py rows] (map-indexed vector samplepoints)]
        (doseq [[px [x y]] (map-indexed vector rows)]
          (let [r (Rect. (- x (/ block-size 2)) (- y (/ block-size 2)) block-size block-size)
                p (get-in board [py px])]
            (Imgcodecs/imwrite (str "samples/" (if p (name p) "e") "-" px "-" py "-" id ".png") (.submat ^Mat flat r)))
          ))
      samplepoints)))

;; Again - older code, not sure what it was doing, think it was pre-neural net days
;; Super naive implementation - needs work.
(defn closest-samplepoint [samplepoints [x y :as p]]
  (first
    (reduce
      (fn [[w l :as winningpoint] s]
        (let [length (util/line-length-squared [p s])]
          (cond
            (or (nil? w) (< length l)) [s length]
            :else winningpoint)))
      nil
      (mapcat identity samplepoints))))



================================================
FILE: src/igoki/core.clj
================================================
(ns igoki.core
  (:require
    [igoki.ui.main :as ui.main]
    [igoki.camera :as camera]
    [igoki.game :as game]
    [clojure.java.io :as io]
    [seesaw.core :as s])
  (:gen-class)
  (:import
    (org.slf4j.bridge SLF4JBridgeHandler)
    (nu.pattern OpenCV)
    (java.util.logging LogManager Level)))

(OpenCV/loadShared)
(SLF4JBridgeHandler/install)
(.setLevel (.getLogger (LogManager/getLogManager) "") Level/INFO)


(defonce ctx (atom {}))
(defn start []
  ;; TODO: these are gross. refactor out these init steps.
  ;; The spice should just flow.
  (camera/start-calibration ctx)
  (game/init ctx)

  (ui.main/main-frame ctx))

(defn -main [& args]
  (start))


================================================
FILE: src/igoki/game.clj
================================================
(ns igoki.game
  (:require
    [igoki.util :as util]
    [igoki.sgf :as sgf]
    [igoki.inferrence :as inferrence]
    [igoki.sound.sound :as snd]
    [igoki.sound.announce :as announce])
  (:import
    (java.io File ByteArrayInputStream)
    (java.util Date UUID)
    (java.text SimpleDateFormat)
    (org.opencv.core MatOfByte)
    (de.schlichtherle.truezip.file TVFS)
    (org.opencv.imgcodecs Imgcodecs)))

;; ========================================================
;; TODO: Display sibling branches
;; TODO: Support swapping last move to a different point (traversing to the applicable branch)
;; TODO: Display other annotations (circle, mark, selected, square, territory-black, territory-white)
;; TODO: Cache last n moves for backtracking to prevent rebuilding it every time.
;; TODO: Mainline variation
;; TODO: 0 and 1 steps on SGF?
;; This will immensely speed up the end game performance.
;; ====================================================



(defn board-diff [b1 b2]
  (remove
    nil?
    (mapcat
      (fn [[y b1row] b2row]
        (map
          (fn [[x b1i] b2i]
            (if-not (= b1i b2i)
              [x y b1i b2i]))
          (map-indexed vector b1row) b2row))
      (map-indexed vector b1) b2)))

(defonce captured-boardlist (atom []))

(defn submit-move
  [ctx]
  (let [board (:board @ctx)]
    (println "Change detected, debouncing")
    (swap!
      ctx
      (fn [c]
        (-> c
            (update :kifu assoc :submit {:latch 2 :board board})
            ;; TODO: this read-delay.. whaat?? this should be a 'accept-delay' at this level.
            (update :camera assoc :read-delay 300))))))

(defn board-history [{:keys [current-branch-path movenumber moves] :as game}]
  (->>
    (range movenumber)
    ;; This is a slow operation, so just checking the last few moves.
    (take-last 20)
    (map
      (fn [m]
        (let [g (inferrence/reconstruct (assoc game :movenumber m))]
          [(:kifu-board g) g])))
    (into {})))

(defn board-updated [_ ctx _ board]
  #_(println "Board updated.")
  (swap! captured-boardlist conj board)
  (let [{{:keys [kifu-board dirty] :as game} :kifu
         ogs :ogs} @ctx

        nodes (sgf/current-branch-node-list (:current-branch-path game) (:moves game))
        lastmove (last nodes)
        [[_ _ mo mn :as mv] :as diff] (board-diff kifu-board board)

        ;; Disabled temporarily for issues with online integration.
        ;; TODO: This causes issues for online integration, obviously, so will
        ;; need to check if undo/move is all
        history-game
        (when-not (:gameid ogs)
          (if (and lastmove (> (count diff) 0)) (get (board-history game) board)))]
    (cond
      (and (empty? diff) dirty)
      (do
        (println "Clean state, marking as such.")
        (swap! ctx assoc-in [:kifu :dirty] false))

      (and (not (empty? diff)) dirty)
      (println "Not actioning board updates until clean state is reached")

      ;; Special case to undo last move
      history-game
      (do
        (snd/play-sound :undo)
        (swap! ctx (fn [c] (assoc c :kifu history-game))))

      :else
      (submit-move ctx))))

(defn dump-camera [filename camidx raw updatelist]
  (when (and raw filename)
    (util/with-release [out (MatOfByte.)]
      (println "Writing jpg: " filename "/" (str camidx ".jpg"))
      (Imgcodecs/imencode ".jpg" raw out)
      (util/zip-add-file filename (str camidx ".jpg") (ByteArrayInputStream. (.toArray out)))
      (util/zip-add-file-string filename (str camidx ".edn") (pr-str updatelist))
      (println "Done writing jpg: " filename "/" (str camidx ".jpg")))))


(defn camera-updated [wk ctx old new]
  (let [{{{:keys [latch board] :as submit} :submit
          :keys [filename camidx last-dump] :as game} :kifu
         {:keys [raw]} :camera
         debug-capture :debug-capture
         cboard :board} @ctx

        updatelist @captured-boardlist

        t (System/nanoTime)]
    (cond
      (nil? submit) nil

      (not= cboard board)
      (do
        (println "Debounce dirty - move discarded")
        (swap!
          ctx
          (fn [c]
            (-> c
                (update :kifu dissoc :submit)
                (update :camera dissoc :read-delay)))))

      (pos? latch)
      (swap! ctx update-in [:kifu :submit :latch] dec)

      :else
      (do
        ;; TODO: Sound playing shouldn't happen here, surely?
        ;;(snd/play-sound :submit)
        (println "Debounce success - move submitted")

        (let [new (inferrence/infer-moves game updatelist (last updatelist))]
          (if (and new (not= (:kifu-board new) (:kifu-board game)))
            (do
              (snd/play-sound :click)

              (announce/comment-move ctx
                (last
                  (sgf/current-branch-node-list
                    (take (:movenumber new) (:current-branch-path new)) (:moves new)))
                (:constructed new))
              (reset! captured-boardlist [])
              (swap! ctx assoc :kifu (assoc (dissoc new :submit) :cam-update true)))
            (swap! ctx update :kifu #(assoc (dissoc % :submit) :cam-update true))))
        (swap! ctx update :camera dissoc :read-delay)))

    ;; Dump camera on a regular basis, ignore time if board is updated.
    #_(println last-dump t (- t last-dump) (> (- t last-dump 20e9)))
    (when (or (get-in @ctx [:kifu :cam-update])
            (nil? last-dump)
            (> (- t last-dump) 20e9))
      (when debug-capture
        (dump-camera filename camidx raw updatelist))
      (swap! ctx update :kifu
        #(-> %
             (assoc :cam-update false :last-dump t)
             (update :camidx (fnil inc 0)))))))


(defn add-initial-points [node board]
  (let [initial
        (for [[y row] (map-indexed vector board)
              [x v] (map-indexed vector row)
              :when v]
          [v x y])
        black (seq (filter (comp #(= :b %) first) initial))
        white (seq (filter (comp #(= :w %) first) initial))]
    (cond->
      node
      black (assoc :add-black (map (fn [[_ x y]] (sgf/convert-coord x y)) black))
      white (assoc :add-white (map (fn [[_ x y]] (sgf/convert-coord x y)) white))
      (> (count black) (count white)) (assoc :player-start ["W"]))))

(defn reset-kifu [ctx]
  (let [context @ctx
        size
        (or (-> context :goban :size) 19)
        board (or (-> context :board) [])
        camfile (or (-> context :kifu :filename) (str "capture/" (.toString (UUID/randomUUID)) ".zip"))
        camidx (or (-> context :kifu :camidx) 0)
        new-game
        (->
          {:filename camfile
           :camidx (inc camidx)
           :moves
           (add-initial-points
             {:branches []
              :player-start ["B"]
              :application [(str "igoki v" (System/getProperty "igoki.version"))]
              :file-format ["4"]
              :gametype ["1"]
              :size [size]
              :date [(.format (SimpleDateFormat. "YYYY-MM-dd") (Date.))]
              :komi ["5.5"]}
             board)

           :movenumber 0
           :current-branch-path [[]]}
          inferrence/reconstruct)]

    (when-not (.exists (File. "capture"))
      (.mkdir (File. "capture")))

    (when (:debug-capture ctx)
      (util/zip-add-file-string
        (:filename new-game)
        (str camidx ".config.edn")
        (pr-str
          {:board (:board context)
           :goban (:goban context)
           :view (dissoc (:view context) :homography)
           :kifu new-game}))
      (dump-camera (:filename new-game) camidx (-> context :camera :raw) [board]))
    (swap! ctx assoc :kifu new-game :filename "game.sgf")))


;; In hundreds..
(defn find-last-move [ctx]
  (let [{{:keys [movenumber] :as game} :kifu} @ctx
        visiblepath (if movenumber (take movenumber (mapcat identity (:current-branch-path game))))
        actionlist (if visiblepath (sgf/current-branch-node-list [visiblepath] (:moves game)))]
    (last actionlist)))

(defn convert-sgf [ctx]
  (sgf/sgf (:moves (:kifu @ctx))))

(defn load-sgf [ctx file]
  (let [sgf-string (slurp file)
        moves (sgf/read-sgf sgf-string)]
    (swap! ctx assoc
      :kifu
      (inferrence/reconstruct
        {:moves moves :movenumber 0 :current-branch-path []})
      :current-file file)))

(defn toggle-branches [ctx show-branches?]
  (swap! ctx assoc-in [:kifu :show-branches] show-branches?))

(defn move-backward [ctx]
  (swap! ctx
    #(->
       %
       (update-in [:kifu :movenumber] (fnil (comp (partial max 0) dec) 1))
       (assoc-in [:kifu :dirty] true)
       (update-in [:kifu] inferrence/reconstruct))))

(defn move-forward [ctx]
  (let [{:keys [movenumber current-branch-path moves]} (:kifu @ctx)
        path (vec (take movenumber (mapcat identity current-branch-path)))
        {:keys [branches] :as node} (last (sgf/current-branch-node-list [path] moves))
        new-branch-path (if (<= (count path) movenumber) [(conj path 0)] current-branch-path)]
    (cond
      (zero? (count branches))
      ctx

      :else
      (swap! ctx update-in [:kifu]
        #(->
           %
           (update :movenumber (fnil inc 1))
           (assoc :dirty true :current-branch-path new-branch-path)
           (inferrence/reconstruct))))))

(defn pass [ctx]
  (swap! ctx
    (fn [{:keys [kifu] :as state}]
      (assoc state
        :kifu
        (inferrence/play-move kifu
          [-1 -1 nil
           ({:white :w :black :b}
            (-> kifu :constructed :player-turn))])))))


;; TODO: Not happy with this
;; The add-watch creates an implicit binding to data structure which makes the
;; whole thing hard to reason about.
(defn init [ctx]
  (when-not (-> @ctx :kifu)
    (TVFS/umount)
    (reset-kifu ctx))
  (util/add-watch-path ctx :kifu-camera [:camera :raw] #'camera-updated)
  (util/add-watch-path ctx :kifu-board [:board] #'board-updated))

================================================
FILE: src/igoki/inferrence.clj
================================================
(ns igoki.inferrence
  (:require
    [igoki.util :as util]
    [igoki.sgf :as sgf]))

(defn simple-board-view [{:keys [board size]}]
  (let [[width height] size]
    (for [y (range height)]
      (for [x (range width)]
        (case (get-in board [(sgf/convert-coord x y) :stone])
          :white :w :black :b nil)))))

(defn reconstruct [{:keys [moves current-branch-path movenumber] :as game}]
  (let [visiblepath (vec (take movenumber (mapcat identity current-branch-path)))
        constructed (sgf/construct-board moves [visiblepath])]
    (assoc game
      :constructed constructed
      :kifu-board (simple-board-view constructed))))

(defn play-move [{:keys [moves current-branch-path movenumber] :as game} [x y o n :as move]]
  (let [visiblepath (vec (take movenumber (mapcat identity current-branch-path)))
        [node path] (sgf/collect-node moves {(if (= n :b) :black :white) [(sgf/convert-coord x y)]} [visiblepath])
        updatedgame
        (->
          game
          (assoc :moves node :dirty false :current-branch-path path)
          (update :movenumber (fnil inc 0)))]
    (reconstruct updatedgame)))

(defn print-boards [& boards]
  (println
    (apply str
      (interpose "\n"
        (apply map
          #(apply str
             (interpose " | "
               (for [m %&]
                 (apply str (map (fn [i] (str " " (if i (name i) "."))) m)))))
          boards)))))

(defn walk-boards [cellfn & boards]
  (apply map
    (fn [& rs]
      (apply map cellfn rs))
    boards))

(defn subtract-board [a b]
  (walk-boards
    (fn [ca cb]
      (if cb nil ca)) a b))

(defn mask-board [a b]
  (walk-boards
    (fn [ca cb]
      (if (and ca cb) ca)) a b))

(defn point-map [board]
  (for [[y rows] (map-indexed vector board)
        [x cell] (map-indexed vector rows)
        :when cell]
    [x y cell]))

(defn clean-updatelist
  [initial updatelist final-position]
  (->>
    ;; We need to propagate the final-position backward
    (reverse updatelist)

    ;; Go through the board snapshot, ripping off any cells that were previously nil
    ;; so that only the stuff that stays put for the duration of the game is left
    (reduce
      (fn [[a result] u]
        (let [f (mask-board a u)]
          [f (conj result f)]))
      [(subtract-board final-position initial) []])
    ;; Get the result
    second
    ;; Remove some noise
    dedupe
    ;; Back to original direction (probably not needed)
    reverse
    ;; Convert into [x y c] point vectors
    (mapcat point-map)
    frequencies
    (group-by #(nth (first %) 2))
    (map (fn [[k v]] [k (map first (reverse (sort-by second v)))]))
    (into {})))

(defn infer-moves
  "From a base game state, a board updatelist and a final-position, infer the move
   sequence.

   The meat of the algorithm is the clean-updatelist - Once that's done, it can
   just interleave the black and white moves and play them to see if it ends up
   as the final-position. Will return nil if a match didn't come out of the woodwork."
  [game updatelist final-position]
  (let [{:keys [kifu-board constructed]} game
        {:keys [b w] :as clean} (clean-updatelist kifu-board updatelist final-position)
        moves (apply util/interleave-all (if (= (:player-turn constructed) :black) [b w] [w b]))
        inferred
        (reduce
          (fn [g [x y cell]]
            (if (= (-> g :constructed :player-turn) ({:b :black :w :white} cell))
              (play-move g [x y nil cell])
              g))
          game
          moves)]
    (print-boards (:kifu-board game) (:kifu-board inferred) final-position)
    (if (= (:kifu-board inferred) final-position) inferred)))

================================================
FILE: src/igoki/integration/ogs.clj
================================================
(ns igoki.integration.ogs
  (:require
    [clj-http.client :as client]
    [clojure.tools.logging :as log]
    [clojure.string :as str]
    [cheshire.core :as json]
    [igoki.util.crypto :as crypto]
    [igoki.inferrence :as inferrence]
    [igoki.sgf :as sgf]
    [igoki.sound.sound :as snd]
    [clojure.edn :as edn]
    [igoki.sound.announce :as announce])

  (:import
    (io.socket.client Socket IO Ack IO$Options)
    (io.socket.emitter Emitter$Listener)
    (org.json JSONObject)
    (java.util Date)
    (java.text SimpleDateFormat)))

;; http://docs.ogs.apiary.io/
;; https://ogs.readme.io/docs/real-time-api

(def url "https://online-go.com")
(def cm (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 10 :threads 3 :insecure? true}))

(comment
  (def cm (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 2 :threads 3 :insecure? true})))


(defn display-rank [ranking pro?]
  (cond
    (nil? ranking)
    "??"

    (< ranking 30)
    (str (int (- 30 (Math/floor ranking))) "k")

    :else
    (str (int (inc (- (Math/floor ranking) 30))) (if pro? "p" "d"))))

(defn str-player [{:keys [username ranking rank professional]}]
  (str username " [" (display-rank (or rank ranking) professional) "]"))

(defn ogs-auth
  [conn]
  (client/post
    (str url "/oauth2/token/")
    {:connection-manager cm
     :form-params (-> conn (assoc :grant_type "password") (dissoc :url))
     :insecure? true
     :as :json}))

(defn ogs-headers
  [auth]
  {:connection-manager cm
   :insecure? true
   :headers   {"Authorization" (str "Bearer " (-> auth :body :access_token))}
   :as :json})

(defn config
  [auth]
  (client/get
    (str url "/api/v1/ui/config/")
    (ogs-headers auth)))

(defn me
  [auth]
  (client/get
    (str url "/api/v1/me/")
    (ogs-headers auth)))

(defn my-settings
  [auth]
  (client/get
    (str url "/api/v1/me/settings/")
    (ogs-headers auth)))

(defn my-games
  [auth]
  (client/get
    (str url "/api/v1/me/games/")
    (ogs-headers auth)))

(defn overview
  [auth]
  (:body
    (client/get
      (str url "/api/v1/ui/overview")
      (ogs-headers auth))))


(defn game-detail
  [auth id]
  (client/get
    (str url "/api/v1/games/" id)
    (ogs-headers auth)))

(defn game-sgf
  [auth id]
  (client/get
    (str url "/api/v1/games/" id "/sgf/")
    (dissoc (ogs-headers auth) :as)))

(defn move
  [auth id coords]
  (client/post
    (str url "/api/v1/games/" id "/move/")
    (assoc
      (ogs-headers auth)
      :form-params {:move coords}
      :content-type :json)))

(defn socket-echo [& xs]
  (log/info "Echo: " xs))


(defn socket-listener [^Socket socket event lfn]
  (.on socket event
    (proxy [Emitter$Listener] []
      (call [xs]
        (log/info "Socket event: " event)
        #_(apply lfn (seq xs))
        (apply lfn (map #(if (instance? JSONObject %) (json/decode (.toString %) keyword) %) (seq xs)))))))

(defn socket-emit [sock event msg]
  (let [m (JSONObject.)]
    (doseq [[k v] msg]
      (.put m (name k) v))
    (.emit sock event
      (into-array JSONObject [m]))))

(defn socket-callback [sock event msg callback-fn]
  (let [m (JSONObject.)]
    (doseq [[k v] msg]
      (.put m (name k) v))
    (.emit sock event
      (into-array JSONObject [m])
      (proxy [Ack] []
        (call [xs] (apply callback-fn (seq xs)))))))

(defn socket-blocking [sock event msg callback-fn]
  (let [m (JSONObject.)
        response (promise)]
    (doseq [[k v] msg]
      (.put m (name k) v))
    (.emit sock event
      (into-array JSONObject [m])
      (proxy [Ack] []
        (call [xs]
          (deliver response (seq xs)))))
    (deref response 5000 :timeout)))

(defn setup-socket []
  (let [sock (IO/socket "https://online-go.com/"
               (let [options (IO$Options.)]
                 (set! (.-transports options) (into-array String ["websocket"]))
                 (println (seq (.-transports options)))
                 options))

        #_(IO/socket "http://online-go.com/socket.io")]
    (doseq [e [Socket/EVENT_CONNECT Socket/EVENT_CONNECT_ERROR
               Socket/EVENT_CONNECT_TIMEOUT Socket/EVENT_DISCONNECT
               Socket/EVENT_ERROR Socket/EVENT_MESSAGE
               Socket/EVENT_RECONNECT Socket/EVENT_RECONNECT_ATTEMPT
               Socket/EVENT_RECONNECT_ERROR Socket/EVENT_RECONNECT_FAILED
               Socket/EVENT_RECONNECTING]]
      (socket-listener sock e socket-echo))
    (.connect sock)
    sock))

(defn add-move [game [x y time]]
  (inferrence/play-move game [x y 0 (case (-> game :constructed :player-turn) :black :b :w)]))

(defn play-move [c data]
  (let [ogspath (-> c :ogs :current-branch-path)
        ogsmovenumber (-> c :ogs :movenumber)
        currentpath (-> c :kifu :current-branch-path)
        currentmovenumber (-> c :kifu :movenumber)
        kifu (inferrence/reconstruct (assoc (:kifu c) :current-branch-path ogspath :movenumber ogsmovenumber))
        ogsgame (add-move kifu (:move data))
        newpath (:current-branch-path ogsgame)
        newmovenumber (:movenumber ogsgame)
        game
        (if (or (not= ogsmovenumber currentmovenumber) (not= ogspath currentpath))
          (inferrence/reconstruct (assoc ogsgame :current-branch-path currentpath :movenumber currentmovenumber))
          ogsgame)]
    (->
      c
      (assoc :kifu game)
      (update :ogs assoc
        :game ogsgame
        :current-branch-path newpath
        :movenumber newmovenumber))))

(defn initialize-game [c game]
  (let [initial-node
        (cond->
          {:branches []
           :player-start [(case (:initial_player game) "white" "W" "B")]
           :application [(str "igoki v" (System/getProperty "igoki.version"))]
           :file-format ["4"]
           :gametype ["1"]
           :size [(:width game) (:height game)]
           :date [(.format (SimpleDateFormat. "YYYY-MM-dd") (Date. (* 1000 (:start_time game))))]
           :game-name [(:game_name game)]
           :black-rank [(-> game :players :black :rank)]
           :black-name [(-> game :players :black :name)]
           :white-rank [(-> game :players :white :rank)]
           :white-name [(-> game :players :white :name)]}
          (not (str/blank? (-> game :initial_state :white)))
          (assoc :add-white (map (partial apply str) (partition 2 (-> game :initial_state :white))))

          (not (str/blank? (-> game :initial_state :black)))
          (assoc :add-black (map (partial apply str) (partition 2 (-> game :initial_state :black)))))

        game-setup
        (inferrence/reconstruct
          {:moves initial-node
           :current-branch-path [[]]
           :movenumber 0})
        game-setup (reduce add-move game-setup (:moves game))]
    (->
      c
      (update :kifu merge game-setup)
      (update :ogs assoc
        :gameinfo game
        :current-branch-path (:current-branch-path game-setup)
        :movenumber (:movenumber game-setup)))))

(def game-events
  ["gamedata" "clock" "phase" "undo_requested" "undo_accepted" "move" "conditional_moves"
   "removed_stones" "removed_stones_accepted" "chat" "error" "reset"])

(defn disconnect-record [ctx]
  (let [ogs (:ogs @ctx)]
    (when (:gameid ogs)
      (doseq [en game-events]
        (.off (:socket ogs) (str "game/" (:gameid ogs) "/" en)))
      (remove-watch ctx (str "ogs." (:gameid ogs)))
      (socket-emit (:socket ogs) "game/disconnect" {:game_id (:gameid ogs)}))))

(defn check-submit-move [ctx old new]
  (let [ogspath (-> new :ogs :current-branch-path)
        oldpath (-> old :kifu :current-branch-path)
        newpath (-> new :kifu :current-branch-path)
        gameinfo (-> new :ogs :gameinfo)
        players (-> new :ogs :players)]
    ;; When either the kifu path or the ogspath changes - check for submission
    (when-not (and (= oldpath newpath)
                   (= (-> old :ogs :current-branch-path) ogspath))

      (let [flatogspath (mapcat identity ogspath)
            flatnewpath (mapcat identity newpath)]
        (when
          (and
            (> (count flatnewpath) (count flatogspath))
            (= flatogspath (take (count flatogspath) flatnewpath)))

          (let [{:keys [black white]}
                (->>
                  (sgf/current-branch-node-list newpath (-> new :kifu :moves))
                  (drop (inc (count flatogspath)))
                  (first))
                player (first (filter #(= (:id (:info %)) (get gameinfo (if black :black_player_id :white_player_id))) players))]
            (cond
              (and black player)
              (do
                (log/info "Submitting Black move: " black player)
                (socket-emit (-> new :ogs :socket)
                  "game/move"
                  {:game_id (:game_id gameinfo)
                   :move (first black)
                   :player_id (-> player :info :id)
                   :auth (:auth player)}))

              (and white player)
              (do
                (log/info "Submitting White move: " white player)
                (socket-emit (-> new :ogs :socket)
                  "game/move"
                  {:game_id (:game_id gameinfo)
                   :move (first white)
                   :player_id (-> player :info :id)
                   :auth (:auth player)})))))))))


(defn connect-record [ctx socket gameid auth & [auth2]]
  ;; Disconnect any existing first.
  (try
    (disconnect-record ctx)
    (catch Exception e
      (.printStackTrace e)))

  (try
    (let [game (:body (client/get (str url "/api/v1/games/" gameid) (ogs-headers auth)))
          player {:info (:body (me auth)) :auth (:auth game)}
          player2 (if auth2 {:info (:body (me auth2)) :auth (:body (client/get (str url "/api/v1/games/" gameid) (ogs-headers auth2)))})
          action #(str "game/" gameid "/" %)
          listen
          (fn [eventname]
            (socket-listener
              socket (action eventname)
              #(do
                 (log/info eventname ":" %)
                 (swap! ctx update-in [:ogs :event-stream]
                   (fnil conj []) {:eventname eventname :data %}))))]
      (doseq [en game-events]
        (listen en))

      (socket-listener
        socket (action "move")
        (fn [data]
          (snd/play-sound :click)
          (swap! ctx play-move data)
          (let [{:keys [ogs kifu]} @ctx]
            (println "announcing move :"
              (last (sgf/current-branch-node-list (take (:movenumber ogs) (:current-branch-path ogs)) (:moves kifu)))
              (igoki.inferrence/print-boards (-> ogs :game :kifu-board)))
            (announce/comment-move ctx
              (last (sgf/current-branch-node-list (take (:movenumber ogs) (:current-branch-path ogs)) (:moves kifu)))
              (-> ogs :game :kifu-board)))))

      (socket-listener
        socket (action "gamedata")
        (fn [data]
          (cond
            (= "play" (:phase data))
            (swap! ctx initialize-game data)


            (= "finished" (:phase data))
            (do
              (disconnect-record ctx)
              (let [game
                    {:sgf (:body (game-sgf auth gameid))
                     :event-stream (:event-stream (:ogs @ctx))
                     :gameid gameid
                     :auth auth}]
                (spit (str "resources/ogs-game." gameid ".edn")
                  (pr-str game)))))))

      (socket-emit socket "game/connect" {:game_id gameid :player_id (:id player) :chat true})

      (add-watch
        ctx (str "ogs." gameid)
        (fn [_ c o n]
          (check-submit-move c o n)))

      (swap! ctx update :ogs assoc
        :socket socket
        :gameid gameid
        :players (if player2 [player player2] [player])
        :game (:gamedata game))
      {:success true})
    (catch Exception e
      (.printStackTrace e)
      {:success false :msg (.getMessage e)})))

(defn save-settings [{:keys [client-id client-secret username password remember]}]
  (let [settings
        {:client-id client-id
         :client-secret client-secret
         :username username
         :remember remember}
        settings
        (if remember
          (assoc settings :password password)
          settings)]
    (spit "ogs.edn"
      (crypto/encrypt
        (pr-str settings)))))

(defn load-settings []
  (try
    (edn/read-string
      (crypto/decrypt
        (slurp "ogs.edn")))
    (catch Exception e {})))

(defn disconnect [ctx]
  (let [ogs (:ogs @ctx)]
    (when (:socket ogs)
      (.disconnect (:socket ogs)))
    (swap! ctx dissoc :ogs)))

(defn refresh-games [ctx]
  (swap! ctx assoc-in [:ogs :overview]
    (overview (get-in @ctx [:ogs :auth]))))

(defn connect
  [ctx {:keys [client-id client-secret username password] :as settings}
   progress-fn]
  (disconnect ctx)
  (save-settings settings)
  (progress-fn :saved)

  (let [auth
        (try
          (ogs-auth
            {:client_id client-id
             :client_secret client-secret
             :username username
             :password password})
          (catch Exception e nil))

        _ (progress-fn :logged-in)
        authconfig (when auth (:body (config auth)))
        _ (progress-fn :authconfig)
        socket (when authconfig (setup-socket))
        _ (progress-fn :socket)
        player
        (when socket (:body (me auth)))
        _ (progress-fn :player-info)
        overview
        (when socket
          (overview auth))
        _ (progress-fn :overview)]

    (cond
      (nil? auth)
      {:success false :message "Authentication Failure?"}

      (nil? authconfig)
      {:success false :message "Could not fetch config"}

      (nil? socket)
      {:success false :message "Could not connect websocket"}

      (nil? player)
      {:success false :message "Could not fetch player info"}

      :else
      (do
        (socket-emit socket "authenticate"
          {:auth (:chat_auth authconfig)
           :player_id (:id (:user authconfig))
           :username (:username (:user authconfig))})

        (progress-fn :socket-auth)

        (swap! ctx assoc :ogs
          {:settings settings
           :auth auth
           :authconfig authconfig
           :player player
           :socket socket
           :overview overview})

        {:success true}))))

(comment
  (.on Socket/EVENT_CONNECT
       (proxy [Emitter$Listener] []
         (call [xs] (apply socket-connect (seq xs)))))

  ;; Get clientid and secret by auth2 client here: https://online-go.com/developer
  ;; Get app password from user profile

  (def auth
    (ogs-auth
      {:client_id     ""
       :client_secret ""
       :username      ""
       :password      ""}))

  (def auth (ogs-auth (read-string (slurp ".creds"))))
  (def authconfig (:body (config auth)))
  (def socket (setup-socket))
  (socket-emit socket "authenticate"
    {:auth (:chat_auth authconfig)
     :player_id (:id (:user authconfig))
     :username (:username (:user authconfig))})

  (def player (:body (me auth)))
  #_(def game (:body (client/get (str url "/api/v1/games/3374557") (ogs-headers auth))))
  #_(def ctx (atom {}))
  (connect-record igoki.main/ctx socket "9567247" auth)
  (socket-emit socket "game/connect" {:game_id (:id game) :player_id (:id player) :chat false})
  (socket-emit socket "game/move" {:game_id (:id game) :move "rg" :player_id (:id player) :auth (:auth game)}))


================================================
FILE: src/igoki/integration/robot.clj
================================================
(ns igoki.integration.robot
  (:require
    [seesaw.core :as s]
    [igoki.camera :as camera]
    [igoki.integration.ogs :as ogs]
    [igoki.sgf :as sgf]
    [clojure.string :as str]
    [igoki.inferrence :as inferrence]
    [igoki.sound.sound :as snd]
    [igoki.sound.announce :as announce]
    [clojure.tools.logging :as log])
  (:import
    (java.awt Robot Rectangle GraphicsDevice Point MouseInfo)
    (java.awt.image BufferedImage)
    (org.nd4j.linalg.exception ND4JIllegalStateException)
    (java.util Date)
    (java.text SimpleDateFormat)
    (java.awt.event InputEvent)
    (javax.swing JWindow)))


; BufferedImage before = getBufferedImage(encoded);
; int w = before.getWidth();
; int h = before.getHeight();
; BufferedImage after = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
; AffineTransform at = new AffineTransform();
; at.scale(2.0, 2.0);
; AffineTransformOp scaleOp =
;    new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
; after = scaleOp.filter(before, after);

(defn rescale-image [bufimg [width height]]
  (let [result (BufferedImage. width height BufferedImage/TYPE_INT_RGB)
        g2d (.getGraphics result)]

    (.drawImage g2d bufimg 0 0 width height nil)
    (.dispose g2d)
    result))

(defn read-frame [ctx]
  (let [{:keys [goban] :as c} @ctx
        {:keys [frame bounds ^Robot robot captured-list]} (:robot c)
        size (or (:size goban) 19)
        [x y w h] bounds
        #_#__ (.setVisible frame false)
        #_#__ (Thread/sleep 50)
        bufimg (.createScreenCapture robot (Rectangle. x y w h))
        scaled (rescale-image bufimg (camera/ref-size-vec (dec size)))
        board
        (doall
          (for [y (range size)]
            (doall
              (for [x (range size)]
                (try
                  (let [pt
                        (.getSubimage scaled (* x camera/block-size) (* y camera/block-size)
                          camera/block-size camera/block-size)
                        [b e w]
                        (try
                          (camera/eval-spot pt)
                          (catch ND4JIllegalStateException e
                            (.printStackTrace e)))]
                    (cond
                      (> b 0.5) :b
                      (> w 0.5) :w))
                  (catch Exception e))))))]

    (swap! ctx update :robot assoc
      :scaled scaled
      :update-list (conj (or captured-list []) board)
      :board board)

    #_(.setVisible frame true)
    (.repaint frame)))

(defn read-robot-loop [ctx]
  (let [{:keys [goban robot]} @ctx]
    ;; If frame is paused, skip reading.
    (when (true? (:started robot))
      (read-frame ctx))
    (Thread/sleep 250)

    (when (:started robot)
      (recur ctx))))

(defn initialize [ctx game]
  (let [initial-node
        (cond->
          {:branches []
           :player-start [(case (:initial_player game) "white" "W" "B")]
           :application [(str "igoki v" (System/getProperty "igoki.version"))]
           :file-format ["4"]
           :gametype ["1"]
           :size [(:width game) (:height game)]
           :date [(.format (SimpleDateFormat. "YYYY-MM-dd") (Date.))]
           :game-name [(:game_name game)]
           :black-rank [(-> game :players :black :rank)]
           :black-name [(-> game :players :black :name)]
           :white-rank [(-> game :players :white :rank)]
           :white-name [(-> game :players :white :name)]}
          (not (str/blank? (-> game :initial_state :white)))
          (assoc :add-white (map (partial apply str) (partition 2 (-> game :initial_state :white))))

          (not (str/blank? (-> game :initial_state :black)))
          (assoc :add-black (map (partial apply str) (partition 2 (-> game :initial_state :black)))))

        game-setup
        (inferrence/reconstruct
          {:moves initial-node
           :current-branch-path [[]]
           :movenumber 0})]

    (swap! ctx
      (fn [c]
        (->
          c
          (update :kifu merge game-setup)
          (update :robot assoc
            :gameinfo game
            :current-branch-path (:current-branch-path game-setup)
            :movenumber (:movenumber game-setup)))))))

(defn initialize-game [ctx]
  (let [{:keys [goban robot]} @ctx
        {:keys [game-detail board]} robot

        converted
        (->>
          (for [[y rows] (map-indexed vector board)
                [x cell] (map-indexed vector rows)]
            (when cell
              [cell (sgf/convert-coord x y)]))
          (remove nil?))
        white
        (->>
          converted
          (filter (fn [[m _]] (= m :w)))
          (map second)
          (apply str))

        black
        (->>
          converted
          (filter (fn [[m _]] (= m :b)))
          (map second)
          (apply str))
        ]
    (initialize ctx
      {:initial_player (str/lower-case (:initial-player game-detail))
       :width (:size goban)
       :height (:size goban)
       :start_time (int (/ (System/currentTimeMillis) 1000))
       :game_name (:game-name game-detail)
       :players
       {:black {:rank (:black-rank game-detail)
                :name (:black-name game-detail)}
        :white {:rank (:white-rank game-detail)
                :name (:white-name game-detail)}}
       :moves []
       :initial_state
       {:white white
        :black black}})))

(defn infer-board-play [ctx {:keys [robot kifu]}]
  (let [{:keys [update-list]} robot
        new (inferrence/infer-moves kifu update-list (last update-list))]
    (when (and new (not= (:kifu-board new) (:kifu-board kifu)))
      (swap! ctx
        (fn [c]
          (-> c
              (assoc :kifu (dissoc new :submit))
              (update :robot assoc :update-list []))))
      (snd/play-sound :click)

      (announce/comment-move ctx
        (last
          (sgf/current-branch-node-list
            (take (:movenumber new) (:current-branch-path new)) (:moves new)))
        (:constructed new)))))


(defn check-submit-move [ctx old new]
  (let [robot (get-in new [:robot :robot])
        {:keys [robot-player]} (get-in new [:robot :game-detail])
        oldpath (-> old :kifu :current-branch-path)
        newpath (-> new :kifu :current-branch-path)
        ]
    ;; When either the kifu path or the ogspath changes - check for submission
    (when (not= oldpath newpath)
      (let [{:keys [black white]}
            (->>
              (sgf/current-branch-node-list newpath (-> new :kifu :moves))
              (last))
            move (first (or black white))]
        (when
          (or
            (and move (= robot-player "Both"))
            (and black (= robot-player "Black"))
            (and white (= robot-player "White")))
          (do
            (log/info "Submitting mouse click at: " move)
            (let [frame ^JWindow (get-in new [:robot :frame])

                  size (get-in new [:goban :size])
                  _ (println "size: " size)
                  cellwidth (int (/ (.getWidth frame) size))
                  cellheight (int (/ (.getHeight frame) size))
                  _ (println "cell: " [cellwidth cellheight])
                  [x y] (sgf/convert-sgf-coord move)
                  _ (println "coords: " [x y])
                  ;; Turns out this may not be needed? let's hope.
                  #_#_reference-point
                  (.getLocation
                    (.getBounds
                      (.getGraphicsConfiguration frame)))
                  frame-location (.getLocationOnScreen frame)
                  mx (int (+ (.getX frame-location) (* cellwidth x) (/ cellwidth 2)))
                  my (int (+ (.getY frame-location) (* cellheight y) (/ cellheight 2)))
                  mouse (.getLocation (MouseInfo/getPointerInfo))]
              (println "ref: " )
              (println "m" [mx my])
              (.getGraphicsConfiguration frame)
              ;; This doseq is due to a bug in older jre 8 - where it takes a few tries to get
              ;; the mouse in the right place... what?!
              (doseq [_ (range 5)]
                (.mouseMove robot mx my))
              (Thread/sleep 10)
              (.mousePress ^Robot robot InputEvent/BUTTON1_DOWN_MASK)
              (Thread/sleep 10)
              (.mouseRelease ^Robot robot InputEvent/BUTTON1_DOWN_MASK)
              (Thread/sleep 10)
              (doseq [_ (range 5)]
                (.mouseMove robot (.getX mouse) (.getY mouse))))))))))

(defn start-capture [ctx ^GraphicsDevice screen bounds game-detail]
  (try
    (swap! ctx update :robot assoc
      :started true :robot (Robot. screen) :bounds bounds
      :game-detail game-detail)

    (read-frame ctx)

    (initialize-game ctx)

    (add-watch ctx ::robot-capture
      (fn [k r o n]
        (try
          ;; See if there's a new board state in the capture
          (let [oldboard (get-in o [:robot :board])
                newboard (get-in n [:robot :board])]

            ;; See if there's a new board state that we need to infer
            (when
              (and
                (true? (get-in o [:robot :started]))
                (not= oldboard newboard))
              (infer-board-play ctx n)))
          (catch Exception e
            (.printStackTrace e)))))

    (add-watch ctx ::robot-submit
      (fn [k r o n]
        ;; See if we need to submit a click
        (try
          (check-submit-move ctx o n)

          ;; Any exception shouldn't bubble up and kill stuff.
          (catch Exception e
            (.printStackTrace e)))))

    (doto
      (Thread. (partial #'read-robot-loop ctx))
      (.setDaemon true)
      (.start))
    (catch Exception e
      (s/alert (str "Could not start capturing: " (.getName (.getClass e)) " - " (.getMessage e)) :type :error)
      (.printStackTrace e))))

(defn pause-capture [ctx]
  (when (-> @ctx :robot :started)
    (swap! ctx update :robot assoc :started :paused)))

(defn unpause-capture [ctx]
  (when (-> @ctx :robot :started)
    (swap! ctx update :robot assoc :started true)))

(defn stop-capture [ctx]
  (when (-> @ctx :robot :started)
    (remove-watch ctx ::robot-capture)
    (swap! ctx update :robot assoc :started false)))

================================================
FILE: src/igoki/litequil.clj
================================================
(ns igoki.litequil
  (:require [seesaw.core :as s])
  (:import
    (javax.swing JPanel SwingUtilities JFrame)
    (java.awt Graphics2D Dimension Color Image BasicStroke RenderingHints Font Polygon)
    (java.awt.event MouseListener MouseEvent MouseMotionListener KeyListener KeyEvent WindowStateListener)
    (java.awt.geom Ellipse2D$Double Rectangle2D)
    (javax.swing.event AncestorListener AncestorEvent)))

;; We desperately need to move off Processing. It doesn't compose well, so this implements the same
;; abstractions we use in quil, but directly in a jpanel, which will let us do more UI things
;; in basic Swing later.

(def ^:dynamic *sketch* (atom nil))
(def ^:dynamic ^JPanel panel nil)
(def ^:dynamic ^Graphics2D g2d nil)

(defn input-action [s local-panel options e k]
  (let [afn (get options k)]
    (if afn
      (with-bindings
        {#'*sketch* s
         #'panel local-panel
         #'g2d (.getGraphics local-panel)}
        (afn e)))))

(defn frame-sleep [sketch-atom]
  (let [{:keys [last-frametime frame-rate]} @sketch-atom
        ;; frame rate is frames per sec
        ;; 10 = 10 frames for 1000 millis 1000/10 = 100ms ideal time per frame
        target-time (/ 1000 frame-rate)
        now (System/currentTimeMillis)
        time-sleep (- target-time (- now (or last-frametime now)))]
    #_(println "Sleep time: " time-sleep)
    (when (pos? time-sleep)
      (Thread/sleep time-sleep))

    (swap! sketch-atom assoc :last-frametime now)))

(defn sketch-panel [options]
  (let [{:keys [draw setup close]} options
        sketch-atom (atom {:options options :stopped false :frame-rate 10})
        local-panel
        (proxy [JPanel] []
          (paintComponent [^Graphics2D local-g2d]
            (when draw
              (with-bindings
                {#'*sketch* sketch-atom
                 #'panel this
                 #'g2d local-g2d}
                (draw)))))]
    (.setFocusable local-panel true)

    (swap! sketch-atom assoc :panel local-panel)



    (.addMouseListener local-panel
      (proxy [MouseListener] []
        (mouseClicked [^MouseEvent e]
          (input-action sketch-atom local-panel options e :mouse-clicked))
        (mousePressed [^MouseEvent e]
          (input-action sketch-atom local-panel options e :mouse-pressed))
        (mouseReleased [^MouseEvent e]
          (input-action sketch-atom local-panel options e :mouse-released))
        (mouseEntered [^MouseEvent e]
          (input-action sketch-atom local-panel options e :mouse-entered))
        (mouseExited [^MouseEvent e]
          (input-action sketch-atom local-panel options e :mouse-exited))))

    (.addMouseMotionListener local-panel
      (proxy [MouseMotionListener] []
        (mouseDragged [^MouseEvent e]
          (input-action sketch-atom local-panel options e :mouse-dragged))
        (mouseMoved [^MouseEvent e]
          (input-action sketch-atom local-panel options e :mouse-moved))))

    (.addKeyListener local-panel
      (proxy [KeyListener] []
        (keyPressed [^KeyEvent e]
          (input-action sketch-atom local-panel options e :key-pressed))
        (keyReleased [^KeyEvent e]
          (input-action sketch-atom local-panel options e :key-released))
        (keyTyped [^KeyEvent e]
          (input-action sketch-atom local-panel options e :key-typed))))


    (.addAncestorListener local-panel
      (proxy [AncestorListener] []
        (ancestorAdded [^AncestorEvent event]
          (when
            (and (.isVisible (.getAncestor event))
              (not (:started @sketch-atom)))

            (swap! sketch-atom assoc :started true :stopped false)

            (when setup
              (with-bindings
                {#'*sketch* sketch-atom
                 #'panel local-panel
                 #'g2d (.getGraphics local-panel)}
                (setup)))

            (doto
              (Thread.
                (fn []
                  (while (not (:stopped @sketch-atom))
                    (frame-sleep sketch-atom)
                    (when (.isVisible local-panel)
                      (.repaint local-panel)))
                  (println "Paint thread stopped.")))

              (.setDaemon true)
              (.start))))

        (ancestorRemoved [event]
          (println "CLOSED!!!")
          (swap! sketch-atom assoc :stopped true :started false)
          (when close
            (close)))

        (ancestorMoved [event])))


    {:sketch-atom sketch-atom
     :panel local-panel}))

(defn sketch [options]
  (let [{:keys [title size draw setup close]} options
        ^JFrame local-frame
        (s/frame
          :title title
          :icon "igoki48.png"
          :resizable? true
          :on-close :dispose)

        sk (sketch-panel options)
        local-panel (:panel sk)
        sketch-atom (:sketch-atom sk)]

    (when size
      (let [[w h] size]
        (.setSize local-panel (Dimension. w h))
        (.setPreferredSize local-panel (Dimension. w h))))

    (.add (.getContentPane local-frame) ^JPanel local-panel)

    ;; Might not be needed now?
    #_(.addWindowListener local-frame
      (proxy [WindowAdapter] []
        (windowClosed [e]
          (swap! sketch-atom assoc :stopped true)
          (when close
            (close)))))
    (doto local-frame
      (.pack)
      (.setExtendedState JFrame/MAXIMIZED_BOTH)
      (.setVisible true)
      )

    (.grabFocus local-panel)
    (swap! sketch-atom assoc :frame local-frame)
    sketch-atom))

(defn smooth []
  (doto g2d
    (.setRenderingHint RenderingHints/KEY_ANTIALIASING
      RenderingHints/VALUE_ANTIALIAS_ON)
    (.setRenderingHint RenderingHints/KEY_FRACTIONALMETRICS
      RenderingHints/VALUE_FRACTIONALMETRICS_ON)
    (.setRenderingHint RenderingHints/KEY_INTERPOLATION
      RenderingHints/VALUE_INTERPOLATION_BICUBIC)))

(defn frame-rate [rate-per-sec]
  (when-not (or (nil? rate-per-sec) (zero? rate-per-sec))
    (swap! *sketch* assoc :frame-rate rate-per-sec)))

(defn width
  ([]
   (.getWidth panel))
  ([sketch-atom]
   (.getWidth (:panel @sketch-atom))))


(defn height
  ([]
   (.getHeight panel))
  ([sketch-atom]
   (.getHeight (:panel @sketch-atom))))

(defn color
  ([g]
   (.setColor g2d (Color. (int g) (int g) (int g))))
  ([g a]
   (.setColor g2d (Color. (int g) (int g) (int g) (int a))))
  ([r g b]
   (.setColor g2d (Color. (int r) (int g) (int b))))
  ([r g b a]
   (.setColor g2d (Color. (int r) (int g) (int b) (int a)))))

(defn background
  ([g]
   (.setBackground g2d (Color. (int g) (int g) (int g))))
  ([g a]
   (.setBackground g2d (Color. (int g) (int g) (int g) (int a))))
  ([r g b]
   (.setBackground g2d (Color. (int r) (int g) (int b))))
  ([r g b a]
   (.setBackground g2d (Color. (int r) (int g) (int b) (int a)))))

(defn stroke-weight [width]
  (.setStroke g2d (BasicStroke. width)))



(defn rect [x y w h]
  (.clearRect g2d x y w h)
  (.drawRect g2d x y w h))

(defn fillrect [x y w h]
  (.fillRect g2d x y w h))

(defn ellipse
  ([g2d x y w h]
   (let [e (Ellipse2D$Double. (- x (/ w 2)) (- y (/ h 2)) w h)
         bg (.getBackground g2d)
         c (.getColor g2d)]
     (.setColor g2d bg)
     (.fill g2d e)
     (.setColor g2d c)
     (.draw g2d e)))
  ([x y w h]
   (ellipse g2d x y w h)))

(defn triangle [x1 y1 x2 y2 x3 y3]
  (let [t (doto (Polygon.)
            (.addPoint x1 y1)
            (.addPoint x2 y2)
            (.addPoint x3 y3)
            (.addPoint x1 y1))
        bg (.getBackground g2d)
        c (.getColor g2d)]
    (.setColor g2d bg)
    (.fill g2d t)
    (.setColor g2d c)
    (.draw g2d t)))

(defn line
  ([[x1 y1] [x2 y2]]
   (line x1 y1 x2 y2))
  ([x1 y1 x2 y2]
   (.drawLine g2d x1 y1 x2 y2)))

(defn text-size [size]
  (.setFont g2d
    (.deriveFont (.getFont g2d) (float size))))

(defn calculate-horiz-offset [alignh ^Rectangle2D bounds]
  (case alignh
    :right (- (.getWidth bounds))
    :center (- (/ (.getWidth bounds) 2))
    0))

(defn calculate-vert-offset [alignv ^Rectangle2D bounds]
  (case alignv
    :bottom (- (.getHeight bounds))
    :center (- (/ (.getHeight bounds) 2))
    0))

(defn text [txt x y & [{:keys [align]}]]
  (let [txt (str txt)
        [alignh alignv] align
        render-ctx (.getFontRenderContext g2d)
        metrics (.getFontMetrics g2d)
        font (.getFont g2d)
        bounds (.getStringBounds font txt render-ctx)
        offset-x (calculate-horiz-offset alignh bounds)
        offset-y (calculate-vert-offset alignv bounds)]
    #_(.drawRect g2d (+ offset-x x)
      (+
        offset-y
        #_(.getDescent metrics)
        (+ y #_(.getAscent metrics))) (.getWidth bounds) (.getHeight bounds))

    (.drawString g2d ^String txt
      (int (+ x offset-x))
      (int (+ y offset-y (.getAscent metrics))))))

(defn image [^Image img x y w h]
  (let [scaled (.getScaledInstance img w h Image/SCALE_SMOOTH)]
    (.drawImage g2d scaled (int x) (int y) nil)))

(defn focused []
  (.isFocused
    (SwingUtilities/getWindowAncestor panel)))

(defn mouse-position []
  (.getMousePosition panel))

(defn mouse-x
  ([]
   (let [position (.getMousePosition panel)]
     (if position
       (.getX position)
       0)))
  ([^MouseEvent e]
   (.getX e)))

(defn mouse-y
  ([]
   (let [position (.getMousePosition panel)]
     (if position
       (.getY position)
       0)))
  ([^MouseEvent e]
   (.getY e)))

(defn key-code [^KeyEvent e]
  (.getKeyCode e))

(defn shadow-text
  ([^String s x y]
   (shadow-text s x y :left :bottom))
  ([^String s x y align-horiz]
   (shadow-text s x y align-horiz :bottom))
  ([^String s x y align-horiz align-vert]
   (color 0 196)
   (text-size 20)
   (text s (inc x) (inc y)
     {:align [(or align-horiz :left) (or align-vert :bottom)]})

   (color 255)
   (text-size 20)
   (text s x y
     {:align [(or align-horiz :left) (or align-vert :bottom)]})))

(def fonts
  {"helvetica-20pt" (Font. "Helvetica" Font/PLAIN 20)})

(defn text-font [font-name]
  (let [font (get fonts font-name (Font/decode font-name))]
    (.setFont g2d font)))



================================================
FILE: src/igoki/projector.clj
================================================
(ns igoki.projector
  (:require
    [igoki.util :as util]
    [igoki.game :as game]
    [igoki.sgf :as sgf]
    [igoki.litequil :as lq]
    [igoki.camera :as camera]
    [seesaw.core :as s])
  (:import
    (org.opencv.calib3d Calib3d)
    (org.opencv.imgproc Imgproc)
    (org.opencv.core Mat Size MatOfPoint2f TermCriteria Core Point Scalar CvType)))

(defonce proj-ctx
  (atom {}))

(defn update-projmat [ctx]
  (let [{:keys [homography board-homography sketch] :as projcontext} @proj-ctx
        {:keys [camera proj-img kifu board goban] :as context} @ctx
        {:keys [kifu-board]} kifu
        existing-corners (:corners projcontext)
        size (Size. 9 7)
        lastmove (game/find-last-move ctx)]

    (when board-homography
      (util/with-release
        [target (MatOfPoint2f.)
         projmat (Mat. (Size. (* (inc (:size goban)) camera/block-size)
                         (* (inc (:size goban)) camera/block-size)) CvType/CV_8UC3)
         newflat (Mat.)]
        (let [[[x1 y1] [x2 y2] [x3 y3] [x4 y4] :as corner-points]
              (camera/target-points (:size goban))

              sample-points (camera/sample-points corner-points (:size goban))
              target (util/vec->mat target (apply concat sample-points))]
          (Core/perspectiveTransform target target (.inv board-homography))
          (Imgproc/rectangle projmat
            (Point. 0 0)
            (Point. (+ x3 camera/block-size) (+ y3 camera/block-size))
            (Scalar. 0 0 0) -1)

          ;; Highlight differences between constructed and camera board (visual syncing)
          (when (and board kifu-board)
            (doseq [[x y o c]
                    (game/board-diff kifu-board board)]
              (let [[px py] (nth (nth sample-points y) x)]
                (Imgproc/circle projmat (Point. px py) 5 (Scalar. 0 0 255)
                  (if (= o :b) 1 -1)))))

          ;; Highlight last move
          (let [{:keys [black white]} lastmove
                m (or black white)]
            (doseq [coord m]
              (let [[x y] (sgf/convert-sgf-coord coord)
                    [px py] (when (and x y) (nth (nth sample-points y) x))]
                (when (and px py)
                  (Imgproc/circle projmat (Point. px py) 5 (Scalar. 0 255 0)
                    (if black 1 -1))))))

          (when (:show-branches kifu)
            (doseq
              [[idx {:keys [black white]}] (map-indexed vector (:branches lastmove))
               m (or black white)]
              (let [[x y :as p] (sgf/convert-sgf-coord m)
                    [px py] (nth (nth sample-points y) x)]
                #_(Core/circle projmat (Point. px py) 5 (Scalar. 255 255 255) -1)
                (Imgproc/putText projmat (str (char (+ 65 idx))) (Point. (- px 5) (+ py 5))
                  Imgproc/FONT_HERSHEY_PLAIN 0.7 (Scalar. 255 255 255) 1.5))))

          ;; Draw entire current board state
          #_(doseq [[y row] (map-indexed vector (:board @ui/ctx))
                    [x cell] (map-indexed vector row)]
             (let [[x y] (nth (nth sample-points y) x)]
               (case cell
                 :b (Core/circle projmat (Point. x y) 5 (Scalar. 255 255 255) 1)
                 :w (Core/circle projmat (Point. x y) 5 (Scalar. 255 255 255) -1)
                 nil)))


          (Imgproc/warpPerspective projmat newflat (.inv board-homography) (Size. (lq/width sketch) (lq/height sketch)))
          (swap! proj-ctx assoc :proj-img (util/mat-to-pimage newflat (:bufimg proj-img)))

          #_(doseq [[x y] (util/mat->seq target)]
              (lq/color 0 0 255)
              (lq/stroke-weight 20)
              (lq/point x y)
              (lq/stroke-weight 0))))
      #_(Core/circle (:raw camera) (Point. 540 265) 20 (Scalar. 255 0 0)))))


(defn draw-checkerboard [{:keys [board]}]
  (doseq [[x y w h] board]
    (lq/rect x y w h)))

(defn checkerboard [x y width height xblocks yblocks]
  (let [bw (/ width xblocks)
        bh (/ height yblocks)]
    {:setup {:x x :y y :width width :height height :xblocks xblocks :yblocks yblocks}
     :size (Size. (dec xblocks) (dec yblocks))
     :board
     (for [cellx (range xblocks) celly (range yblocks)
           :when (= (mod (+ cellx (* celly (inc xblocks))) 2) 1)]
       (if (= (mod celly 2) 0)
         [(+ x (* cellx bw)) (+ y (* celly bh)) (dec bw) (dec (+ bh (/ bh 10)))]
         [(+ x (* cellx bw)) (+ y (* celly bh) (/ bh 10)) (dec bw) (dec (- bh (/ bh 10)))]))
     :points
     (for [celly (range 1 yblocks) cellx (range 1 xblocks)]
       (if (= (mod celly 2) 0)
         [(+ x (* cellx bw)) (+ y (* celly bh))]
         [(+ x (* cellx bw)) (+ y (* celly bh) (/ bh 10))]))}))

(defn fix-checker-orientation [corners-mat]
  (let [corners (util/mat->seq corners-mat)
        p1 (nth corners 0)
        p2 (nth corners 9)
        p3 (nth corners 18)]
    (if (> (util/line-length [p1 p2]) (util/line-length [p2 p3]))
      (util/vec->mat corners-mat (reverse corners))
      corners-mat)))

(defn look-for-checkerboard [proj-ctx camera checker]
  (util/with-release [gray (Mat.)]
    (let [corners (MatOfPoint2f.)
          crit (TermCriteria. (bit-or TermCriteria/EPS TermCriteria/MAX_ITER) 30 0.1)]
      (Imgproc/cvtColor (:raw camera) gray Imgproc/COLOR_BGR2GRAY)
      (let [found
            (Calib3d/findChessboardCorners gray (:size checker) corners
              (+ Calib3d/CALIB_CB_ADAPTIVE_THRESH
                Calib3d/CALIB_CB_NORMALIZE_IMAGE
                Calib3d/CALIB_CB_ASYMMETRIC_GRID
                Calib3d/CALIB_CB_EXHAUSTIVE))]
        (.println System/out (str "Checking: " found))
        (when found
          (println "Found corners.")
          #_(Imgproc/cornerSubPix gray corners (Size. 3 3) (Size. -1 -1) crit)
          (fix-checker-orientation corners)
          (swap! proj-ctx assoc :corners corners))))))

(defn update-homography [proj-ctx existing-corners checker]
  (util/with-release
    [target (MatOfPoint2f.)]
    (let [target (util/vec->mat target (:points checker))
          homography
          (Calib3d/findHomography ^MatOfPoint2f existing-corners ^MatOfPoint2f target
            Calib3d/FM_RANSAC 3.0)]
      (when homography
        (println "Homography updated")
        (swap! proj-ctx assoc :homography homography)))))

(defn update-board-homography [ctx proj-ctx homography]
  (util/with-release
    [projector-space (MatOfPoint2f.)
     board-space (MatOfPoint2f.)]
    (let [goban (:goban @ctx)
          projector-space (util/vec->mat projector-space (:points goban))
          _ (Core/perspectiveTransform projector-space projector-space homography)
          board-space (util/vec->mat board-space (camera/target-points (:size goban)))
          board-homography
          (Calib3d/findHomography ^MatOfPoint2f projector-space ^MatOfPoint2f board-space
            Calib3d/FM_RANSAC 3.0)]

      (doseq [[x y] (util/mat->seq projector-space)]
        (lq/color 255 0 0)
        (lq/ellipse x y 10 10))

      (when board-homography
        (println "Board Homography updated")
        (swap! proj-ctx assoc :board-homography board-homography)))))

(defn draw [proj-ctx ctx]
  (lq/frame-rate 10)
  (lq/background 0 0 0)
  (lq/rect 0 0 (lq/width) (lq/height))

  (let [[w h] [(lq/width) (lq/height)]
        [gw gh] [(/ w 2) (/ h 2)]
        checker (checkerboard (- gw (/ gw 2)) (- gh (/ gh 2)) gw gh 10 8)

        {:keys [camera goban] :as context} @ctx
        {:keys [homography board-homography calibrate? proj-img] :as projcontext} @proj-ctx
        existing-corners (:corners projcontext)
        img (:bufimg proj-img)]

    (lq/background 255 255 255)
    (lq/rect 0 0 (lq/width) (lq/height))
    (lq/background 0 0 0)

    (when calibrate?
      (draw-checkerboard checker)

      ;; Draw screen intersection points
      #_(do
        (lq/color 255 0 0)
        (doseq [[x y] (:points checker)]
          (lq/ellipse x y 5 5)))

      #_(lq/image (-> @proj-ctx :pattern) (- (/ (q/width) 2) 180) (- (/ (q/height) 2) 200) 300 400)




      ;; There's no way this should be happening in the draw call.

      (cond
        (:raw camera)
        (look-for-checkerboard proj-ctx camera checker))



      ;; Draw the found checkerboard for acceptance..
      (when (and existing-corners)
        (util/with-release [clone (.clone (:raw camera))]
          (Calib3d/drawChessboardCorners clone (:size checker) existing-corners true)
          (swap! ctx update :camera
            assoc :pimg
            (util/mat-to-pimage clone
              (-> context :camera :pimg :bufimg))))))

    (when-not calibrate?
      (cond
        (and existing-corners (not homography))
        (update-homography proj-ctx existing-corners checker)

        (and homography (= 4 (count (:points goban))) (not board-homography))
        (update-board-homography ctx proj-ctx homography)

        proj-img
        (lq/image img 0 0 (.getWidth img) (.getHeight img))))))

(defn reset-ctx []
  (reset! proj-ctx {:sketch (:sketch @proj-ctx)}))

(defn show-calibration []
  (swap! proj-ctx assoc :calibrate? true))

(defn accept-calibration [ctx]
  (swap! proj-ctx assoc :calibrate? false)
  (swap! ctx assoc-in [:projector :setting-up] false))

(defn stop-cframe [ctx close-fn]
  (let [{:keys [sketch]} @proj-ctx]
    (when (and sketch (:frame @sketch))
      (.dispose (:frame @sketch))))
  (reset! proj-ctx {})
  (swap! ctx dissoc :projector)
  (when close-fn
    (close-fn)))

(defn start-cframe [ctx close-fn]
  (stop-cframe ctx close-fn)

  (let [sketch
        (lq/sketch
          {:title "Move on board in camera view (place paper on board for contrast)"
           :draw (partial #'draw proj-ctx ctx)
           :size (or (-> @proj-ctx :sketchconfig :size) [1280 720])})]
    (swap! proj-ctx assoc :sketch sketch :calibrate? false))
  (swap! ctx assoc :projector {:setting-up true})
  (doto
    (Thread.
      ^Runnable
      (fn []
        (while (not (:stopped (:sketch @proj-ctx)))
          (try
            (update-projmat ctx)
            (catch Exception e
              (.printStackTrace e)))
          (Thread/sleep 500))
        (when close-fn
          (close-fn))))
    (.setDaemon true)
    (.start))
  #_(when (:sketch @proj-ctx)
     (doto (:sketch @proj-ctx)
       #_(.setExtendedState JFrame/MAXIMIZED_BOTH)
       #_(.setUndecorated true))))





================================================
FILE: src/igoki/scratch/scratch.clj
================================================
(ns igoki.scratch.scratch
  (:require
    [igoki.util :as util :refer [-->]])
  (:import
    (org.opencv.objdetect CascadeClassifier)
    (org.opencv.core MatOfRect Core Rect Point Scalar Mat Size MatOfPoint MatOfKeyPoint MatOfPoint2f Point3 TermCriteria MatOfPoint3 CvType MatOfPoint3f)
    (java.awt.image BufferedImage WritableRaster DataBufferByte)
    (java.awt Color Graphics KeyboardFocusManager KeyEventDispatcher Font RenderingHints)
    (java.io File)
    (javax.imageio ImageIO)
    (javax.swing JFrame JPanel)
    (org.opencv.imgproc Imgproc)
    (java.awt.event KeyEvent MouseListener MouseEvent)
    (org.opencv.imgcodecs Imgcodecs)
    (org.opencv.videoio VideoCapture Videoio)))

;; This namespace represents some of the early igoki work, mostly to detect the actual Go Board.
;; it has been deprecated in favour of simply doing manual calibration due to the complexity
;; of dealing with the fickleness of variances in Go boards, lighting conditions, camera quality,
;; etc.
;;
;; It would be neat to have automatic handling, but it's not the core objective of this project to
;; detect Go boards - instead, it's focussed on bridging the gap between the digital and physical
;; game.

;; Step 1 - Define Corners of board
;; Step 2 - Verify coordinates
;; Step 3 - Choose mode: Local Kifu, OGS Kifu

(nu.pattern.OpenCV/loadShared)

(defonce camera (atom nil))



(defonce appstate
         (atom
           {:images      [{} {} {} {} {}]
            :goban-corners [[670 145] [695 900] [1320 855] [1250 220]]
            :input :camera
            :selected    -1
            :frozen      false}))



(defn update-image-mat! [slot image title]
  (swap! appstate #(assoc-in % [:images slot] {:mat image :title title})))

(defn reset-to-index! []
  (swap! appstate assoc :selected -1))

(defn rotate-slot-left! []
  (swap! appstate (fn [i] (update i :selected #(mod (dec %) (count (:images i)))))))

(defn rotate-slot-right! []
  (swap! appstate (fn [i] (update i :selected #(mod (dec %) (count (:images i)))))))

(defn select-frame! [n]
  (swap! appstate (fn [i] (assoc i :selected (mod n (count (:images i)))))))

(defn toggle-freeze! []
  (println "Freeze toggle")
  (swap! appstate update-in [:frozen] not))

(defn save-image [^BufferedImage img]
  (ImageIO/write img "png" (File. "resources/new.png")))

(defn load-image [^String file]
  (ImageIO/read (File. file)))

(defn handle-keypress [^KeyEvent e]
  (println "Key pressed: " (.getKeyCode e) " - Shift: " (.isShiftDown e) )
  (when (= (.getID e) KeyEvent/KEY_PRESSED)
    (case (.getKeyCode e)
      67 (swap! appstate assoc :input :camera)
      82 (swap! appstate assoc :input (Imgcodecs/imread "resources/goboard.png"))
      32 (toggle-freeze!)
      27 (reset-to-index!)
      49 (select-frame! 1)
      50 (select-frame! 2)
      51 (select-frame! 3)
      52 (select-frame! 4)
      53 (select-frame! 5)
      54 (select-frame! 6)
      55 (select-frame! 7)
      56 (select-frame! 8)
      57 (select-frame! 9)
      48 (select-frame! 0)

      10 (swap! appstate assoc :accepted true)
      false)))

(defn draw-title [g title x y]
  (.setColor g (Color/BLACK))
  (.drawString g title (dec x) (dec y))
  (.setColor g (Color/WHITE))
  (.drawString g title x y))

(defn draw-index [^JFrame frame ^Graphics g {:keys [images]}]
  (let [gridsize (Math/ceil (Math/sqrt (count images)))
        gw (/ (.getWidth frame) gridsize)]
    (doseq [[c {:keys [mat title]}] (map-indexed vector images)]
      (if-let [image (if (pos? (.width mat)) (util/mat-to-buffered-image mat nil))]
        (let [ratio (if image (/ (.getHeight image) (.getWidth image)))
              x (* (mod c gridsize) gw)
              y (* (Math/floor (/ c gridsize)) ratio gw)]
          (.drawImage g image (int x) (int y) (int gw) (int (* ratio gw)) nil)
          (draw-title g (str title ", Slot: " c) (int (+ x 5)) (int (+ y 15)))
          )))))

(defn render [^JFrame frame ^Graphics g]
  (let [{:keys [images selected] :as state} @appstate
        {:keys [mat title] :as im} (get images selected)
        image (if (and im (pos? (.getWidth frame))) (util/mat-to-buffered-image mat nil))
        ratio (if image (/ (.getHeight image) (.getWidth image)))]
    (.setRenderingHints g (RenderingHints. RenderingHints/KEY_INTERPOLATION RenderingHints/VALUE_INTERPOLATION_BICUBIC))
    (if (or (= selected -1) (nil? image))
      (draw-index frame g state)
      (do
        (.drawImage g image 0 0 (.getWidth frame) (* ratio (.getWidth frame)) nil)
        (draw-title g (str title ", Slot: " selected) 5 15)))))

(defn click-mouse [^MouseEvent e]
  )

(defn window [text x y]
  (let [frame (JFrame.)]
    (.add (.getContentPane frame)
          (proxy [JPanel] []
            (paint [^Graphics g]
              (render frame g))
            ))

    (.addMouseListener
      frame
      (proxy [MouseListener] []
        (mousePressed [^MouseEvent e]
          (click-mouse e))))

    (.addKeyEventDispatcher (KeyboardFocusManager/getCurrentKeyboardFocusManager)
                            (proxy [KeyEventDispatcher] []
                              (dispatchKeyEvent [e]
                                (handle-keypress e)
                                false)))
    (doto frame
      (.setDefaultCloseOperation JFrame/DISPOSE_ON_CLOSE)
      (.setTitle text)
      (.setResizable true)
      (.setSize 800 600)
      (.setLocation x y)
      (.setVisible true))))

#_(defn highlight-faces [image]
  (let [face-detector (CascadeClassifier. (.getAbsolutePath (clojure.java.io/file "resources/lbpcascade_frontalface.xml")))
        face-detections (MatOfRect.)]
    (.detectMultiScale face-detector image face-detections)
    (doseq [^Rect r (seq (.toArray face-detections))]
      (Core/rectangle image (Point. (.-x r) (.-y r)) (Point. (+ (.-x r) (.-width r)) (+ (.-y r) (.-height r))) (Scalar. 0 255 0)))
    ))

(defn matofpoint-vec
  "Convert MatOfPoint to vector list [[x y] [x y] ...]"
  [mat]
  (for [p (.toList mat)]
    [(.-x p) (.-y p)]))


(def target-homography
  {:9x9
   (doto (MatOfPoint2f.)
     (.fromList (for [x (range 1 3) y (range 1 2)] (Point. (* 70.0 x) (* 70.0 y)))))
   :13x13
   (util/vec->mat (MatOfPoint2f.) [[70 70] [70 980] [980 980] [980 70]])
   :19x19
   (doto (MatOfPoint2f.)
     (.fromList (for [x (range 1 2) y (range 1 2)] (Point. (* 70.0 x) (* 70.0 y)))))})

(def current-transform (atom nil))

(defn count-perimeter [mat]
  (let [m (for [x (range (.rows mat))] (seq (.get mat x 0)))]
    (first
      (reduce
        (fn [[r [ax ay :as a]] [x y :as p]]
          (cond
            (nil? a) [r p]
            :else
            [(+ r (Math/sqrt (+ (* (- ax x) (- ax x)) (* (- ay y) (- ay y))))) p]))
        [0 nil] (concat [(last m)] m)))))

(defn find-goban [gray-img colour]
  #_(let [sorted (:goban-corners @appstate)
        s (map-indexed vector sorted)
        c (--> gray-img (Imgproc/Canny 100 50 3 false))]
    (update-image-mat! 5 c "Contours")
    (reduce
      (fn [[i [ax ay :as a]] [_ [x y] :as p]]
        (cond
          (nil? a) p
          :else
          (do
            (Core/line colour (Point. ax ay) (Point. x y) (Scalar. 0 0 (- 255 (* i 32))) 5)
            p)))
      nil (concat s [(first s)]))
    sorted
    )

  #_(let [contours (ArrayList.) hier (Mat.)
          c (--> gray-img (Imgproc/Canny 100 50 3 false))]
      ;; Find contours
      (Imgproc/findContours c
                            contours hier Imgproc/RETR_TREE Imgproc/CHAIN_APPROX_NONE (Point. 0 0))
      (update-image-mat! 5 c "Contours")

      ;; Find largest area 4-cornered polygon
      (let
        [[sq x _]
         (loop [[x & xs] (range 0 (.cols hier))
                [_ _ rarea :as result] nil]
           (if (nil? x)
             result
             (let [sq (MatOfPoint2f.)
                   c (nth contours x)
                   area (Imgproc/contourArea c)
                   perim (count-perimeter c)]
               (Imgproc/approxPolyDP (doto (MatOfPoint2f.) (.fromArray (.toArray c))) sq (* perim 0.02) true)
               (cond
                 (and (= 4 (.rows sq)) (> area (or rarea 0)))
                 (recur xs [sq x area])

                 :else (recur xs result)))))

         ;; Sort the corners
         sorted
         (if-not (nil? sq)
           (let [pointlist (matofpoint-vec sq)
                 closest-to-origin (first (sort-by #(apply + %) (matofpoint-vec sq)))
                 ;; Rotate corners until closest-to-origin is first
                 [p1 [_ pfy :as pf] pc [_ ply :as pl] :as rotated] (take 4 (drop-while #(not= closest-to-origin %) (concat pointlist pointlist)))]
             ;; Flip so that 'top right' point is next
             (if (< pfy ply) rotated [p1 pl pc pf])))]

        ;; Draw the actual matching contour
        (Imgproc/drawContours colour contours x
                              (Scalar. 0 0 255) 1 0 hier 1 (Point. 0 0))

        ;; Draw the "board" polygon.
        (let [s (map-indexed vector sorted)]
          (reduce
            (fn [[i [ax ay :as a]] [_ [x y] :as p]]
              (cond
                (nil? a) p
                :else
                (do
                  (Core/line colour (Point. ax ay) (Point. x y) (Scalar. 0 0 (- 255 (* i 32))) 5)
                  p)))
            nil (concat s [(first s)])))

        sorted)))


#_(defn old-process [w calibration frame]
  (do
    #_(highlight-faces frame)
    (let [fil (Mat.) m (Mat.) m2 (Mat.) edges (Mat.) hough (Mat.) hough-img (Mat.)
          corners (MatOfPoint.) corners2f (MatOfPoint2f.)
          timg (Mat.) detectimg (Mat.)

          dest
          (-->
            frame
            (Imgproc/cvtColor Imgproc/COLOR_BGR2GRAY)
            (Imgproc/bilateralFilter 5 (double 155) (double 105))
            )

          colour
          (-->
            dest
            (Imgproc/cvtColor Imgproc/COLOR_GRAY2BGR))
          [p1 pf _ pl :as goban-corners] (find-goban dest colour)
          goban-contour (util/vec->mat (MatOfPoint2f.) goban-corners)]
      (Imgproc/goodFeaturesToTrack dest corners 1000 0.03 15 (Mat.) 10 false 0.1)
      (.fromArray corners2f (.toArray corners))
      (Imgproc/cornerSubPix dest corners2f (Size. 11 11) (Size. -1 -1)
                            (TermCriteria. (bit-or TermCriteria/EPS TermCriteria/COUNT) 30 0.1))

      #_(println goban-corners)
      #_(println (filter
                   #(pos? (Imgproc/pointPolygonTest goban-corners % true))
                   (seq (.toArray corners2f))))


      (let
        [goban-points
         (->>
           (seq (.toArray corners2f))
           (filter
             #(> (Imgproc/pointPolygonTest goban-contour % true) -10))
           (map (fn [p] [(.-x p) (.-y p)])))
         _ (println (count goban-points))
         {:keys [size target]}
         (condp < (count goban-points)
           400 {:size 19 :target (:19x19 target-homography)}
           100 {:size 13 :target (:13x13 target-homography)}
           {:size 9 :target (:9x9 target-homography)}
           )
         sorted
         (sort-by
           (juxt
             (comp #(int (/ % 25)) (partial util/line-to-point-dist [p1 pf]))
             (comp #(int (/ % 25)) (partial util/line-to-point-dist [p1 pl])))
           (take (.rows target) goban-points))
         origpoints
         (doto (MatOfPoint2f.)
           (.fromList (map (fn [[x y]] (Point. x y)) goban-corners)))
         h (if (= (.rows target) (count goban-corners))
             (Calib3d/findHomography ^MatOfPoint2f origpoints ^MatOfPoint2f target Calib3d/FM_RANSAC 3.0))]


        (if h
          (reset! current-transform h))

        (when-let [h @current-transform]
          (let [transformed (Mat.) ih (Mat.) invert-transformed (Mat.)]
            (Core/perspectiveTransform corners2f transformed h)
            #_(Core/perspectiveTransform target invert-transformed ih)

            (Imgproc/warpPerspective frame timg h (.size frame))
            (Imgproc/warpPerspective frame detectimg h (.size frame))

            #_(Imgproc/erode detectimg detectimg (Imgproc/getStructuringElement Imgproc/MORPH_RECT (Size. 10 10)))
            (doseq [c (range 0 (.rows transformed))]
              (let [[x1 y1 :as p] (seq (.get transformed c 0))]
                #_(Core/putText hough-img (str c) (Point. x1 y1) Core/FONT_HERSHEY_COMPLEX 1 (Scalar. 0 255 0) 2)
                (Core/circle colour (Point. x1 y1) 2 (Scalar. 128 255 0) 2)
                ))
            (doseq [c (range 0 (.rows invert-transformed))]
              (let [[x1 y1 :as p] (seq (.get invert-transformed c 0))]
                #_(Core/putText hough-img (str c) (Point. x1 y1) Core/FONT_HERSHEY_COMPLEX 1 (Scalar. 0 255 0) 2)
                (Core/circle colour (Point. x1 y1) 2 (Scalar. 0 255 0 128) 10)
                ))
            )
          (Core/rectangle detectimg (Point. 70 70) (Point. 980 980) (Scalar. 0 255 0) 1)

          (doseq [x (range 1 (inc size))]
            (doseq [y (range 1 (inc size))]
              (let [p (Point. (- (* 75.0 x) 15) (- (* 75.0 y) 15))
                    roi (Rect. p (Size. 30 30))
                    m (Mat. detectimg roi)
                    a (Core/mean m)
                    c (int (first (seq (.-val a))))
                    text (cond (< c 30) "B" (> c 200) "W")]
                (when text
                  (Core/putText timg text p Core/FONT_HERSHEY_COMPLEX 1 (Scalar. 0 0 255) 1.5))
                (Core/rectangle detectimg p (Point. (+ (.-x p) 30) (+ (.-y p) 30)) (Scalar. 0 255 0) 1)
                (Core/circle timg (Point. (+ (.-x p) 15) (+ (.-y p) 15)) 5 a 5))))))

      #_(doseq [{[x y] :value :as p}
                (filter #(> (:strength (meta %)) 10) (kdtree-seq @stable-points))]
          (Core/circle colour (Point. x y)
                       (/ (Math/min (or (:strength (meta p)) 1) 100) 5) (Scalar. 255 25 25) 2))

      (doseq [p (seq (.toArray corners2f))]
        (Core/circle colour p 2 (Scalar. 255 0 255) 3))


      (update-image-mat! 0 frame "Source")
      (update-image-mat! 1 dest "Find points")
      (update-image-mat! 2 colour "Find points")
      (update-image-mat! 3 timg "Detected goban")
      (update-image-mat! 4 detectimg "Check for pieces")
      #_(when-not (.empty timg)
          (update-image-mat! 2 timg "Perspective Shifted")
          (update-image-mat! 3 detectimg "Detect Stones")
          )
      (.repaint w)
      true)))

(defn refresh-camera [w camera frame]
  (Thread/sleep 100)
  #_(let [{:keys [frozen calib-corners calibration input] :as state} @appstate
        read (if frozen true (if (= input :camera) (.read camera frame) false))
        frame (if (= input :camera) frame input)]
    (cond
      (not read) false
      :else
      (old-process w calibration frame)))
  (.repaint w))



(defn capture [camidx]
  (let [camera (VideoCapture. ^int camidx Videoio/CAP_ANY)
        frame (Mat.)]
    (.read camera frame)

    (cond
      (not (.isOpened camera)) (println "Error: camera not opened")
      :else
      (do
        (update-image-mat! 0 frame "Source")
        (let [w (window "Original" 0 0)]
          (swap! appstate assoc :diag-window w)
          (doto
            (Thread.
              #(loop []
                (try
                  (refresh-camera w camera frame)
                  (catch Exception e
                    (.printStackTrace e)
                    (Thread/sleep 5000)))
                (recur)))
            (.setDaemon true)
            (.start)))))
    #_(.release camera)))



;; Some work on hough circles, discarded because it gets very inaccurate on busy boards.
(comment
  (defn hough-circles [m]
    (util/with-release
      [mat (Mat.)
       bilat (Mat.)
       blurred (Mat.)
       white-mask (Mat.)
       black-mask (Mat.)
       laplacian (Mat.)
       masked (Mat.)
       circles (Mat.)
       canny (Mat.)]
      #_(doseq [r (range (count (:signature cluster)))]
          (let [[x y] [(mod r szx) (int (/ r szx))]]
            (.put ^Mat mat x y (double-array (repeat 3 (* 255.0 (double (get (:signature cluster) r))))))))

      (Imgproc/cvtColor m mat Imgproc/COLOR_HSV2BGR)
      (Imgproc/cvtColor mat mat Imgproc/COLOR_BGR2GRAY)
      (Imgproc/dilate mat blurred (Imgproc/getStructuringElement Imgproc/MORPH_ELLIPSE (Size. 7 7)))
      (Imgproc/erode blurred blurred (Imgproc/getStructuringElement Imgproc/MORPH_ELLIPSE (Size. 9 9)))
      (Imgproc/blur blurred bilat (Size. 5 5))
      #_(Imgproc/Laplacian bilat laplacian 0 1 0.8 0.1)
      #_(Core/addWeighted laplacian 10.0 bilat 0.8 10.0 bilat)

      (Core/compare bilat (Scalar. 200.0) white-mask Core/CMP_GT)
      (Imgproc/dilate white-mask white-mask (Imgproc/getStructuringElement Imgproc/MORPH_ELLIPSE (Size. 12 12)))
      (Core/compare bilat (Scalar. 80.0) black-mask Core/CMP_LT)
      #_(Imgproc/GaussianBlur blurred bilat (Size. 9 9) 20)
      #_(Imgproc/cvtColor (ui/illuminate-correct blurred) bilat Imgproc/COLOR_BGR2GRAY)
      #_(Imgproc/bilateralFilter blurred bilat 2 (double 10) (double 10))

      (Imgproc/Canny bilat canny 50 25)

      #_(q/image (util/mat-to-pimage bilat) 0 0)
      (let [min-radius 17
            max-radius 25]
        (Imgproc/HoughCircles bilat circles Imgproc/CV_HOUGH_GRADIENT 1 25 50 8 min-radius max-radius)

        (let [found (doall (map #(vec (.get circles 0 %)) (range (.cols circles))))]
          #_(println "---------------")
          {:bilat (util/mat-to-pimage bilat)
           :canny (util/mat-to-pimage canny)
           :white (doall (filter (fn [[x y]]
                                   #_(println (first (.get ^Mat white-mask y x)))
                                   (= 255 (int (first (.get ^Mat white-mask y x))))) found))
           :black (doall (filter (fn [[x y]]
                                   #_(println (first (.get ^Mat white-mask y x)))
                                   (= 255 (int (first (.get ^Mat black-mask y x))))) found))}
          #_{:white (filter (fn [[x y]] (= 1 (int (first (.get ^Mat white-mask y x))))) found)
             :black (filter (fn [[x y]] (= 1 (int (first (.get ^Mat black-mask y x))))) found)}))))
  (defn read-circle-board [samplepoints {:keys [black white] :as circles}]
    #_(for [[y row] (map-indexed vector samplepoints)
            [x [px py]] (map-indexed vector row)]
        )
    ))


;; Some prelim work on determining the corners of a board to ease or skip initial calibration.

(defn mat->lines [^Mat mat]
  (for [x (range (.cols mat))]
    (.get mat 0 x)))

(defn theta [[x1 y1 x2 y2]]
  (mod (Math/atan2 (- y1 y2) (- x1 x2)) Math/PI))

(defn avg-theta [vs]
  (/ (reduce #(+ %1 (nth %2 4)) 0 vs) (count vs)))

(defn group-lines [avg lines]
  (let [opp (mod (- avg (/ Math/PI 2)) Math/PI)
        [mn mx] (sort [(mod (- avg (/ Math/PI 2)) Math/PI) avg])]
    (group-by #(if (< mn (nth % 4) mx) avg opp) lines)))

(defn line-group [[cx cy] [x1 y1 x2 y2 t :as l]]
  (if (< (/ Math/PI 4) t (* 3 (/ Math/PI 4)))
    [(Math/round (* t 5)) (Math/round (- x2 (/ (- y2 cy) (Math/tan t)))) cy]
    [(Math/round (* t 5)) cx (Math/round (- y2 (* (- x2 cx) (Math/tan t))))]))


(defn remove-outliers [[k ls]]
  (let [avg (last (last (take (/ (count ls) 2) (sort-by #(nth % 4) ls))))]
    [avg (filter (fn [[_ _ _ _ t]] (< (Math/abs (double (- t avg))) (/ Math/PI 9))) ls)]))


(defn find-board [ctx]
  (let [{{:keys [homography shift reference]} :view
         {:keys [raw]} :camera
         {:keys [size]} :goban} @ctx

        cleaned (Mat.)
        bilat (Mat.)
        mask (Mat.)
        pts2f (MatOfPoint2f.)]
    (.copyTo raw cleaned)
    #_(Imgproc/filter2D cleaned cleaned 1  (Mat. [1 1 1 1 -8 1 1 1 1]))
    (Imgproc/cvtColor cleaned cleaned Imgproc/COLOR_BGR2GRAY)
    #_(Imgproc/equalizeHist cleaned cleaned)
    #_(Imgproc/bilateralFilter cleaned bilat 5 (double 15) (double 15))
    (Imgproc/GaussianBlur cleaned bilat (Size. 5 5) 2)
    (Imgproc/Laplacian bilat bilat -8 3 8 2)
    (Imgproc/cvtColor bilat bilat Imgproc/COLOR_GRAY2BGR)
    (Imgproc/cvtColor bilat bilat Imgproc/COLOR_BGR2HSV)
    (Core/inRange bilat (Scalar. 0 0 100) (Scalar. 180 255 255) mask)

    #_(Imgproc/Canny bilat bilat 200 50 3 true)
    (Imgproc/HoughLinesP mask pts2f 1 (/ Math/PI 360) 100 50 10)
    #_(println (util/write-mat pts2f))
    (Imgproc/cvtColor bilat bilat Imgproc/COLOR_HSV2BGR)
    #_(println (avg-theta (mat->lines pts2f)))
    #_(println (map theta (mat->lines pts2f)))
    #_(let [groups
            (->>
              (mat->lines pts2f)
              group-lines
              (map remove-outliers))])
    (let [lines (map (fn [[x1 y1 x2 y2 :as k]] [x1 y1 x2 y2 (theta k)]) (mat->lines pts2f))
          avg (avg-theta lines)]
      #_(println "=================================================")
      (swap! ctx update-in [:linedump] conj (map remove-outliers (group-lines avg lines)))
      #_(doseq [[k ls] (map remove-outliers (group-lines avg lines))
              [[_ gx gy] gls] (group-by (partial line-group [(/ (.cols bilat) 2) (/ (.rows bilat) 2)]) ls)]

        #_(println [x1 y1 x2 y2 t])
        #_(println g " -- " (count gls))
        (let [[x1 y1 x2 y2 t] (first gls)
              k (* k (/ 180 Math/PI))
              l (min (* (count gls) 30) 255)]
          (Core/line bilat (Point. x1 y1) (Point. x2 y2) (Scalar. 255 l 0) 5)
          (Core/line bilat (Point. gx gy ) (Point. x2 y2) (Scalar. 0 0 255) 2)
          #_(let [x (/ (/ (.rows bilat) 2) (Math/tan t))]
              #_(println x " = " y2 " / " (Math/tan t))
              (Core/line bilat (Point. (+ x (- x1 (/ y1 (Math/tan t)))) (/ (.rows bilat) 2)) (Point. x2 y2) (Scalar. 255 0 0) 13)
              #_(Core/line bilat (Point. xa 0) (Point. x1 y1) (Scalar. 255 0 0) 13)))))


    #_(doseq [[x1 y1 x2 y2] (partition 4 (:data (util/write-mat pts2f)))]
        (Core/line bilat (Point. x1 y1) (Point. x2 y2) (Scalar. 0 255 0) 1))
    #_(println (util/write-mat pts2f))
    #_(doseq [p (seq (.toArray pts2f))]
        (Core/line cropped p 2 (Scalar. 255 0 255) 3))
    (comment
      ;; Finding interesting points.. This doesn't lend itself too well :/
      (Imgproc/cvtColor cropped cleaned Imgproc/COLOR_BGR2GRAY)
      (Imgproc/bilateralFilter cleaned bilat 5 (double 15) (double 15))
      (Imgproc/goodFeaturesToTrack bilat pts 500 0.01 (- camera/block-size 5))
      (.fromArray pts2f (.toArray pts))
      (Imgproc/cornerSubPix bilat pts2f (Size. 11 11) (Size. -1 -1)
        (TermCriteria. (bit-or TermCriteria/EPS TermCriteria/COUNT) 30 0.1))
      (doseq [p (seq (.toArray pts2f))]
        (Core/circle cropped p 2 (Scalar. 255 0 255) 3)))
    (swap! ctx assoc-in [:goban :flat]
      (util/mat-to-pimage bilat nil))
    #_(util/write-mat pts)))

================================================
FILE: src/igoki/scratch/training.clj
================================================
(ns igoki.scratch.training
  (:require
    [clojure.java.io :as io]
    [igoki.util :as util]
    [clojure.edn :as edn])
  (:import
    (java.io File FileInputStream)
    (org.opencv.core MatOfByte MatOfPoint2f Mat Size Rect Core)
    (org.opencv.calib3d Calib3d)
    (org.opencv.imgproc Imgproc)
    (org.opencv.imgcodecs Imgcodecs)))

(defn file-id [^File img]
  (let [nm (.getName img)]
    (.substring nm 0 (- (.length nm) 4))))

(defn config-file [^File img]
  (let [pn (str (file-id img) ".edn")]
    (io/file (.getParentFile img) pn)))

(defn config-exists? [^File img]
  (.exists (config-file img)))

(defn load-image [^File f]
  (let [result (byte-array (.length f))]
    (with-open [is (FileInputStream. f)]
      (.read is result))
    result))

(defn load-raw [^File img]
  (Imgcodecs/imdecode (MatOfByte. (load-image img)) Imgcodecs/IMREAD_UNCHANGED))

(defn load-next-sample [ctx folder]
  (let [imgfile
        (->>
          (.listFiles (io/file folder))
          (filter #(.endsWith (.toLowerCase (.getName %)) ".jpg"))
          (remove config-exists?)
          first)
        _ (println imgfile)
        raw (load-raw imgfile)
        oldpimg (-> @ctx :camera :pimg)
        pimg (util/mat-to-pimage raw (:bufimg oldpimg))]
    (swap! ctx
      #(-> %
        (assoc :goban {:points [] :size 19}
               :state :goban)
        (update :camera assoc :raw raw :pimg pimg)
        (update :training assoc :file imgfile)))
    (println imgfile "loaded")))

(defn save-current-sample [ctx]
  (let [{:keys [board training view goban]} @ctx
        config
        {:file (.getName (:file training))
         :board board
         :goban goban
         :view (select-keys view [:samplesize :samplecorners :samplepoints])}]
    (spit (config-file (:file training)) (pr-str config))))

(defn target-points [block-size size]
  (let [extent (* block-size size)]
    [[block-size block-size] [extent block-size] [extent extent] [block-size extent]]))

(defn ref-size [block-size size]
  (Size. (* block-size (inc size)) (* block-size (inc size))))

(defn sample-points [corners size]
  (let [[ctl ctr cbr cbl] corners
        divide (fn [[x1 y1] [x2 y2]]
                 (let [xf (/ (- x2 x1) (dec size))
                       yf (/ (- y2 y1) (dec size))]
                   (map (fn [s] [(+ x1 (* s xf)) (+ y1 (* s yf))]) (range size))))
        leftedge (divide ctl cbl)
        rightedge (divide ctr cbr)]
    (map
      (fn [left right] (divide left right))
      leftedge rightedge)))

(defn dump-points [^File imgfile sample-size flat samplepoints board id]
  (let [rots [nil 0 1 -1]]
    (util/with-release
      [sample (Mat.)]
      (doseq [[py rows] (map-indexed vector samplepoints)]
        (doseq [[px [x y]] (map-indexed vector rows)]
          (let [r (Rect. (- x sample-size) (- y sample-size) (* sample-size 2) (* sample-size 2))
                p (get-in board [py px])]
            (doseq [rotname rots]
              (let [nm (str (.getAbsolutePath (.getParentFile imgfile)) "/samples/"
                         (if p (name p) "e") "/" id "/" px "-" py "-" (or rotname "O")".png")]
                (if rotname
                  (do
                    (Core/transpose (.submat ^Mat flat r) sample)
                    (Core/flip sample sample rotname)
                    (Imgcodecs/imwrite nm sample))
                  (Imgcodecs/imwrite nm
                    (.submat ^Mat flat r))))))))))
  samplepoints)

(defn generate-sample-points [settings ^File img]
  (util/with-release
    [target (MatOfPoint2f.)
     origpoints (MatOfPoint2f.)
     ref (Mat.)]
    (let [{{:keys [points size]} :goban :as config} (edn/read-string (slurp (config-file img)))
          raw (load-raw img)
          target (util/vec->mat target (target-points (:block-size settings) size))
          origpoints (util/vec->mat origpoints points)
          samplecorners (target-points (:block-size settings) size)
          samplepoints (sample-points samplecorners size)
          homography
          (Calib3d/findHomography ^MatOfPoint2f origpoints ^MatOfPoint2f target
            Calib3d/FM_RANSAC 3.0)]
      (Imgproc/warpPerspective raw ref homography (ref-size (:block-size settings) size))
      (dump-points img (:sample-size settings) ref samplepoints (:board config) (.getName img)))))

(defn generate-all-samples [settings folder]
  (let [f (io/file folder)
        imgs
        (->>
          (.listFiles f)
          (filter #(.endsWith (.toLowerCase (.getName %)) ".jpg"))
          (filter config-exists?))]
    (.mkdirs (File. f "samples/b"))
    (.mkdirs (File. f "samples/e"))
    (.mkdirs (File. f "samples/w"))

    (doseq [i imgs]
      (.mkdirs (File. f (str "samples/b/" (.getName i))))
      (.mkdirs (File. f (str "samples/e/" (.getName i))))
      (.mkdirs (File. f (str "samples/w/" (.getName i))))
      (println "Processing : " (.getAbsolutePath i))
      (generate-sample-points settings i))))

(comment
  (ui/stop-read-loop igoki.core/ctx)

  (load-next-sample igoki.core/ctx "resources/samples/testing")
  (let [c (swap! igoki.core/ctx camera/read-board)] :done)
  (save-current-sample igoki.core/ctx)
  (generate-all-samples {:block-size 10 :sample-size 5} "resources/samples/training"))

================================================
FILE: src/igoki/sgf.clj
================================================
(ns igoki.sgf
  (:require
    [igoki.util :as util]
    [clojure.set :as set]))

;; According to http://www.red-bean.com/sgf/properties.html
(def property-lookup
  {
   ;; Moves
   "B"  :black
   "KO" :ko
   "MN" :move-number
   "W"  :white

   ;; Setup
   "AB" :add-black
   "AE" :add-erase
   "AW" :add-white
   "PL" :player-start

   ;; Annotation
   "C"  :comment
   "DM" :position-even
   "GB" :position-bias-black
   "GW" :position-bias-white
   "HO" :position-hotspot
   "N"  :name
   "UC" :position-unclear
   "V"  :value-to-white                                     ; Negative is good for black

   ;; Move annotation
   "BM" :move-bad
   "DO" :move-doubtful
   "IT" :move-interesting
   "TE" :move-tesuji

   ;; Markup
   "AR" :arrow                                              ; 'from:to' in value
   "CR" :circle
   "DD" :grayout
   "LB" :label                                              ; 'point:label'
   "LN" :line                                               ; 'from:to'
   "MA" :mark                                               ; with X
   "SL" :selected
   "SQ" :square
   "TR" :triangle
   "TB" :territory-black
   "TW" :territory-white

   ;; Root info
   "AP" :application
   "CA" :charset
   "FF" :file-format
   "GM" :gametype                                           ; 1=Go
   "ST" :variation-show-type
   "SZ" :size

   ;; Game info
   "AN" :annotator
   "BR" :black-rank
   "BT" :black-team
   "CP" :copyright
   "DT" :date
   "EV" :event
   "GN" :game-name
   "GC" :game-comment
   "ON" :opening
   "OT" :overtime-method
   "PB" :black-name
   "PC" :place
   "PW" :white-name
   "RE" :result                                             ; [WB][+]([RTF](esign|ime|orfeit)?)?, 0, Draw, Void, '?'
   "RO" :round
   "RU" :rules
   "SO" :source
   "US" :user
   "WR" :white-rank
   "WT" :white-team
   "HA" :handicap
   "KM" :komi

   ;; Timing
   "BL" :black-time-left
   "OB" :black-moves-left
   "OW" :white-moves-left
   "WL" :white-time-left

   ;; Miscellaneous
   "FG" :print-figure
   "PM" :print-move-numbers
   "VW" :view}                                              ; show only listed points or reset if empty
  )

(def handicap-placement
  {2 ["pd" "dp"]
   3 ["pd" "dp" "pp"]
   4 ["pd" "dp" "pp" "dd"]
   5 ["pd" "dp" "pp" "dd" "jj"]
   6 ["pd" "dp" "pp" "dd" "dj" "pj"]
   7 ["pd" "dp" "pp" "dd" "jj" "dj" "pj"]
   8 ["pd" "dp" "pp" "dd" "dj" "pj" "jd" "jp"]
   9 ["pd" "dp" "pp" "dd" "dj" "pj" "jd" "jp" "jj"]})

(def reverse-property-lookup
  (into {} (map (fn [[k v]] [v k]) property-lookup)))

(defn convert-coord [x y]
  (if (or (neg? x) (neg? y))
    ""
    (str (char (+ 97 x)) (char (+ 97 y)))))

(defn convert-sgf-coord [[x y :as s]]
  (when (and x y)
    [(- (int x) 97) (- (int y) 97)]))

(defn inpath [branch-path]
  (concat [:branches] (mapcat (fn [i] [i :branches]) (mapcat identity branch-path))))

(defn current-branch-node-list [path rootnode]
  (reductions
    (fn [node p]
      (get (:branches node) p))
    rootnode (mapcat identity path)))

(defn accumulate-action [{:keys [action text node] :as state}]
  (let [a (get property-lookup action action)]
    (-> state
        (update-in [:node a] #(conj (or % []) text))
        (assoc :mode :collected))))

(defn collect-text [state c]
  (update state :text str c))

(defn collect-action [state c]
  (if (= (:mode state) :collected)
    (assoc (dissoc state :mode) :action (str c))
    (update state :action str c)))

(defn find-existing-branch [branches {:keys [black white]}]
  (->>
    (map-indexed vector branches)
    (filter (fn [[_ n]] (or (and black (= (:black n) black))
                            (and white (= (:white n) white)))))
    first))

(defn collect-node [game node branch-path & [branch?]]
  (let [branches (get-in game (inpath branch-path))
        [idx branch] (find-existing-branch branches node)]
    (cond
      (empty? node)
      [game (if branch? (conj branch-path []) branch-path)]

      idx
      [(util/iupdate-in game (conj (inpath branch-path) idx) merge node)
       (cond->
         branch-path true (update (dec (count branch-path)) conj idx)
         branch? (conj []))]

      :else
      [(util/iupdate-in game (inpath branch-path) (fnil conj []) node)
       (let [newpoint (count (get-in game (inpath branch-path)))]
         (cond->
           branch-path
           true (update (dec (count branch-path)) conj newpoint)
           branch? (conj [])))])))

(defn read-sgf [sgf-string]
  (let [new-node {:branches []}
        initial-state {:mode nil :action "" :node nil}]
    (loop [[game branch-path :as g] [new-node []]
           state initial-state
           [c & o] sgf-string]
      (cond
        (nil? c)
        ;; TODO: This picks the first branch in an sgf as the root, might need to show a list
        ;; of games instead if the SGF actually has multiple root branches.
        (first (:branches game))

        (and (= c \\) (= (:mode state) :collect-text))
        (recur g state o) ;; Escape character in text.

        (= c \])
        (recur g (accumulate-action state) o)

        (= (:mode state) :collect-text)
        (recur g (collect-text state c) o)

        (= c \[)
        (recur g (assoc state :mode :collect-text :text "") o)

        (Character/isUpperCase ^char c)
        (recur g (collect-action state c) o)

        (= c \;)
        (do
          (recur (collect-node game (:node state) branch-path) initial-state o))

        (= c \()
        (recur (collect-node game (:node state) branch-path true) initial-state o)

        (= c \))
        (let [[g np] (collect-node game (:node state) branch-path)]
          (recur [g (vec (butlast np))] initial-state o))
        :else
        (recur g state o)
        ))))

(defn node-to-sgf [node]
  (let [nodestr
        (apply str
               (mapcat
                 (fn [[k v]]
                   (str
                     (get reverse-property-lookup k k)
                     "[" (apply str (interpose "][" v)) "]"))
                 (dissoc node :branches)))]
    (str
      ";" nodestr
      (cond
        (nil? (:branches node)) ""
        (> (count (:branches node)) 1) (str "(" (apply str (interpose ")(" (map node-to-sgf (:branches node)))) ")")
        :else (node-to-sgf (first (:branches node)))))))

(defn sgf [root]
  (str "(" (node-to-sgf root) ")"))


;; == Board construction ==
(defmulti step-action
  (fn [b k v]
    (cond
      (#{:annotator :black-rank :black-team :copyright :date :event :game-name :game-comment
         :opening :overtime-method :black-name :place :white-name :result :round :rules :source
         :user :white-rank :white-team :handicap :komi :application :charset :file-format :gametype
         :variation-show-type :comment :name :value-to-white} k)
      :annotate-game
      (#{:circle :mark :selected :square :triangle :territory-black :territory-white} k)
      :markup
      (#{:arrow :line} k)
      :line
      (#{:move-bad :move-doubtful :move-interesting :move-tesuji} k)
      :movequality
      (#{:position-even :position-bias-black :position-bias-white :position-hotspot :position-unclear} k)
      :positionquality
      :else k)))

(defmethod step-action :default [b k v]
  #_(println "Unhandled property: " k)
  b)

;; Annotations and metadata guck
(defn rectangle-point-list [v]
  (let [[[fx fy :as fp] tp] (.split v ":")
        [tx ty] (or tp fp)]
    (for [x (range (int fx) (inc (int tx)))
          y (range (int fy) (inc (int ty)))]
      (str (char x) (char y)))))

(defmethod step-action :annotate-game [b k v]
  (assoc b k v))

(defmethod step-action :move-number [b k v]
  (assoc b :move-offset (Integer/parseInt v)))

(defmethod step-action :markup [b k v]
  (update b :annotations
          concat (map (fn [sv] {:type k :point sv}) (rectangle-point-list v))))

(defmethod step-action :line [b k v]
  (let [[f t] (seq (.split (str v) ":"))]
    (update b :annotations conj {:type k :from f :to t})))

(defmethod step-action :label [b k v]
  (let [[p l] (seq (.split (str v) ":"))]
    (update b :annotations conj {:type k :point p :text l})))

(defmethod step-action :movequality [b k v]
  (assoc b :movequality [k v]))

(defmethod step-action :positionquality [b k v]
  (assoc b :positionquality [k v]))

(defmethod step-action :size [b k v]
  (let [[width height] (map #(Integer/parseInt %) (seq (.split (str v) ":")))
        height (or height width)]
    (assoc b :size [width height])))

(defmethod step-action :player-start [b k v]
  (let [v (if (= v "W") :white :black)]
    (assoc b
      :player-start v
      :player-turn v)))

;; Onto actual interesting board related stuff.
(defn set-color [color b k v]
  ;; Does not consider board size, simply adds it since the board size might be changed.
  (reduce #(assoc-in %1 [:board %2 :stone] color) b (rectangle-point-list v)))

(defmethod step-action :add-black [b k v]
  (set-color :black b k v))

(defmethod step-action :add-white [b k v]
  (set-color :white b k v))

(defmethod step-action :add-erase [b k v]
  (set-color nil b k v))

(defn inside-point? [{[width height] :size :as board} p]
  (let [a (int \a)
        [x y] (map int p)]
    (and
      (= (count p) 2)
      (>= x a) (< x (+ a width))
      (>= y a) (< y (+ a height)))))

(defn neighbour-points
  "Return a set of neighbour coords for a given point on a board, taking edges and corners into account"
  [board p]
  (when p
    (let [[x y] (map int p)]
      (->>
        [[(dec x) y] [(inc x) y] [x (dec y)] [x (inc y)]]
        (filter (partial inside-point? board))
        (map (fn [[x y]] (str (char x) (char y))))
        set))))

(defn find-group [board point]
  (let [color (get-in board [:board point :stone])]
    (loop [[p & po] [point]
           group #{}]
      (let [neighbours (set/difference (neighbour-points board p) group)]
        (cond
          (nil? p) group
          (= color (get-in board [:board p :stone]))
          (recur (concat po neighbours) (conj group p))
          :else
          (recur po group))))))

(defn count-liberties [board group]
  (let [neighbours (set/difference (set (mapcat (partial neighbour-points board) group)) group)
        liberties (filter nil? (map #(get-in board [:board % :stone]) neighbours))]
    (count liberties)))

(defn group-alive? [board group]
  (pos? (count-liberties board group)))

(defn remove-captured-group [board group]
  (let [color (get-in board [:board (first group) :stone])]
    (->
      (reduce #(assoc-in %1 [:board %2 :stone] nil) board group)
      (update-in [:captures (if (= color :white) :black :white)] (fnil + 0) (count group)))))

(defn check-capture-around [board color point]
  (let [opp-color (if (= color :white) :black :white)
        neighbours (neighbour-points board point)
        opp-points (filter #(= opp-color (get-in board [:board % :stone])) neighbours)
        captured-groups (remove (partial group-alive? board) (set (map (partial find-group board) opp-points)))]
    (reduce remove-captured-group board captured-groups)))

(defn check-suicide [board point]
  (let [group (find-group board point)]
    (if (group-alive? board group)
      board
      (remove-captured-group board group))))

(defn place-stone [board color point]
  ;; If the stone is 'placed' outside the board (either '' or 'tt') then count as a 'pass'
  (if (inside-point? board point)
    (-> board
        (assoc-in [:board point :stone] color)
        (assoc-in [:board point :movenumber] (:movenumber board))
        (check-capture-around color point)
        (check-suicide point))
    (assoc board :player-passed color)))

(defn move [board color point]
  (-> board
      (assoc :player-turn (if (= color :white) :black :white))
      (place-stone color point)
      (update :movenumber (fnil inc 0))))

(defmethod step-action :black [b k v]
  (move b :black v))

(defmethod step-action :white [b k v]
  (move b :white v))

(defmethod step-action :branches [b k v]
  b)

;; Tying it all together.

(defn step-node [board node]
  (reduce
    (fn [b [k v]]
      (reduce #(step-action %1 k %2) b v))
    (dissoc
      board
      :player-passed
      :name
      :comment
      :movequality
      :positionquality
      :annotations
      :value-to-white)
    node))

(defn construct-board [rootnode path]
  (let [nodelist (current-branch-node-list path rootnode)]
    (reduce step-node {:size [19 19] :player-turn :black :movenumber 0 :moveoffset 0} nodelist)))

================================================
FILE: src/igoki/simulated.clj
================================================
(ns igoki.simulated
  (:require
    [igoki.litequil :as lq]
    [igoki.util :as util]
    [igoki.ui.util :as ui.util]
    [seesaw.core :as s]
    [clojure.java.io :as io])
  (:import
    (java.io File)
    (org.opencv.core Mat Point Scalar MatOfPoint MatOfByte)
    (de.schlichtherle.truezip.fs FsEntryNotFoundException)
    (java.awt.event MouseEvent)
    (org.opencv.imgproc Imgproc)
    (org.opencv.imgcodecs Imgcodecs)
    (javax.swing JComboBox)))

;; This view simulates a camera for testing igoki's behaviour without having a board and camera handy
(defonce simctx (atom {:sketchconfig {:framerate 5 :size [640 480]}}))

(defn blank-board [size]
  (vec
    (for [y (range size)]
      (vec (for [x (range size)] nil)))))

(defn stone-point [[mx my] grid-start cell-size]
  [(int (/ (+ (- mx grid-start) (/ cell-size 2)) cell-size))
   (int (/ (+ (- my grid-start) (/ cell-size 2)) cell-size))])

(defn grid-spec [m]
  (let [size (or (-> @simctx :sim :size) 19)
        cellsize (max (/ (.rows m) (+ size 2)) 25)
        grid-start (+ cellsize (/ cellsize 2))]
    [cellsize grid-start]))

(defn reset-board [ctx size]
  (swap! ctx update :sim assoc :size size :board (blank-board size) :next :b :mode :alt))

(defn stone-colors [c]
  (if (= c :w)
    [(Scalar. 255 255 255) (Scalar. 28 28 28)]
    [(Scalar. 28 28 28) (Scalar. 0 0 0)]))

(defn draw-stone [m x y c cellsize]
  (when c
    (let [[incolor bcolor] (stone-colors c)]
      (Imgproc/circle m (Point. x y) (/ cellsize 4) incolor (/ cellsize 2))
      (Imgproc/circle m (Point. x y) (/ cellsize 2) bcolor 2))))

(defn draw-board [^Mat m]
  (try
    (when (and m (pos? (.rows m)))
      #_(.setTo m (Scalar. 92 179 220))
      (let [{{:keys [size board next]} :sim} @simctx
            [cellsize grid-start] (grid-spec m)
            mpos (lq/mouse-position)
            [mx my]
            (if mpos
              [(* (/ (float (.getX mpos)) (lq/width)) (.width m))
               (* (/ (float (.getY mpos)) (lq/height)) (.height m))]
              [2000 2000])]

        (doseq [x (range size)]
          (let [coord (+ grid-start (* x cellsize))
                extent (+ grid-start (* cellsize (dec size)))]

            (Imgproc/line m (Point. coord grid-start) (Point. coord extent) (Scalar. 0 0 0))
            (Imgproc/line m (Point. grid-start coord) (Point. extent coord) (Scalar. 0 0 0))))


        (doseq [[x y] (util/star-points size)]
          (Imgproc/circle m (Point. (+ grid-start (* x cellsize))
                                 (+ grid-start (* y cellsize))) 2 (Scalar. 0 0 0) 2))

        (doseq [[y rows] (map-indexed vector board)
                [x v] (map-indexed vector rows)]
          (when v
            (draw-stone m (+ grid-start (* cellsize x)) (+ grid-start (* cellsize y)) v cellsize)))

        (let [[x y] (stone-point [mx my] grid-start cellsize)]
          (Imgproc/circle m (Point. (+ grid-start (* x cellsize))
                                 (+ grid-start (* y cellsize))) (/ cellsize 2) (Scalar. 0 0 255) 1))

        (draw-stone m mx my (-> @simctx :sim :next) cellsize)

        (util/with-release [pts (MatOfPoint.)]
          (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]]))
          (Imgproc/fillPoly m [pts] (Scalar. 96 90 29)))))

    (catch Exception e
      (.printStackTrace e)))
  m)

(defn simulate []
  (let [context @simctx]
    (cond
      (= (:mode context) :replay)
      (when (-> context :camera :raw)
        (swap! simctx
          update
          :camera assoc
          :raw (-> context :camera :raw)
          :pimg
          (util/mat-to-pimage (-> context :camera :raw)
            (-> context :camera :pimg :bufimg))))

      :else
      (when (-> context :sim :background)
        (let [m (.clone (-> context :sim :background))]
          (draw-board m)
          (swap! simctx
            update :camera assoc
            :raw m
            :pimg
            (util/mat-to-pimage m
              (-> context :camera :pimg :bufimg))))))))


(defn next-stone [{:keys [next mode]}]
  (case mode
    :black :b
    :white :w
    :erase nil
    (if (= next :w) :b :w)))

(defn mouse-pressed [ctx ^MouseEvent e]
  (swap! ctx
    (fn [{{:keys [raw]} :camera :keys [sim] :as c}]
      (let [[cs gs] (grid-spec raw)
            mpos (lq/mouse-position)
            [px py]
            (stone-point
              [(* (/ (float (.getX mpos)) (lq/width)) (.width raw))
               (* (/ (float (.getY mpos)) (lq/height)) (.height raw))] gs cs)
            current (get-in sim [:board py px] :outside)]
        (println "[cs gs]" [cs gs])
        (println "[px py]" [px py])
        (println "current" current)
        (cond
          (= current :outside) c
          :else
          (-> c
              (assoc-in [:sim :board py px] (:next sim))
              (assoc-in [:sim :next] (next-stone sim))))))))

(defn alternate-mode [{:keys [next] :as sim}]
  (assoc sim :mode :alt :next (if (= next :w) :b :w)))

(defn set-mode [sim c]
  (assoc sim :mode c :next (case c :white :w :black :b nil)))

(defn step-file-index [ctx nextfn]
  (try
    (let [{{:keys [file index]} :replay
           {:keys [pimg]} :camera} @ctx
          nextindex (nextfn index)
          image (util/zip-read-file file (str nextindex ".jpg"))
          raw (Imgcodecs/imdecode (MatOfByte. image) Imgcodecs/IMREAD_UNCHANGED)
          pimg (util/mat-to-pimage raw (:bufimg pimg))]
      (swap!
        ctx
        (fn [c]
          (-> c
              (update :camera assoc :raw raw :pimg pimg)
              (update :replay assoc :index nextindex)))))
    (catch FsEntryNotFoundException e (.printStackTrace e))))

(defn load-zip [ctx file]
  (println "Loading:" file)
  (swap! ctx assoc :mode :replay :replay {:file file :index 0})
  (step-file-index ctx identity))

(defn load-img [ctx file]
  (swap! ctx assoc :mode :replay :replay {:file file :index 0})
  (step-file-index ctx identity))

(defn set-board-size [ctx size]
  (swap! ctx assoc-in [:sim :size] size))

(defn key-pressed [simctx e]
  ;; TODO: Add these are buttons on the panel.
  (case (lq/key-code e)
    ;; Left
    37
    ;; Right
    39 (step-file-index simctx inc)
    ;; L
    76 (ui.util/load-dialog #(load-zip simctx (.getAbsolutePath %)) (str (System/getProperty "user.dir") "/capture"))
    (println "Unhandled key-down: " (lq/key-code e))))



(defn setup [ctx]
  (lq/smooth)
  (lq/frame-rate 20)
  (lq/background 200))

(defn paint [ctx]
  (simulate)
  (let [{{:keys [^Mat raw pimg]} :camera
         {:keys [frame index]} :replay
         :keys [stopped mode]} @ctx
        [cellsize grid-start] (if raw (grid-spec raw) [])
        tx (- (lq/width) 180)]
    (lq/background 128 64 78)
    (lq/rect 0 0 (lq/width) (lq/height))
    (cond
      stopped
      (lq/shadow-text "Select 'simulation' camera..." 20 35)

      (nil? pimg)
      (lq/shadow-text "Image not built yet, please wait..." 10 25)

      (= mode :replay)
      (do
        (lq/image (:bufimg pimg) 0 0 (lq/width) (lq/height))
        (lq/shadow-text (str "Frame " index) 10 25))

      :else
      (lq/image (:bufimg pimg) 0 0 (lq/width) (lq/height)))))

(defn start-simulation [ctx]
  (try
    ;; This tmp song and dance because Imgcodes takes a filename string :'/
    (let [tmp (File/createTempFile "background" "jpg")]
      (io/copy (io/input-stream (io/resource "wooden-background.jpg")) tmp)

      (swap! simctx
        (fn [s]
          (->
            s
            (assoc :stopped false)
            (update :sim assoc :background (Imgcodecs/imread (.getAbsolutePath tmp)))))))
    (catch Exception e
      (.printStackTrace e)))

  (reset-board simctx 19)

  (doto
    (Thread.
      #(when-not (-> @simctx :stopped)
        (let [{{:keys [raw pimg]} :camera} @simctx]
          (swap! ctx update :camera assoc :raw raw :pimg pimg)
          (Thread/sleep (or (-> @ctx :camera :read-delay) 500))
          (recur))))
    (.setDaemon true)
    (.start)))

(defn set-board-mode [mode]
  (case mode
    "Alternating"
    (swap! simctx update :sim alternate-mode)

    "Black"
    (swap! simctx update :sim set-mode :black)

    "White"
    (swap! simctx update :sim set-mode :white)

    "Clear"
    (swap! simctx update :sim set-mode :erase)))

(defn button-panel [simctx]
  (let [mode (:mode @simctx)

        panel
        (s/flow-panel
          :hgap 15
          :items
          (cond
            (= mode :replay)
            [(s/button
               :id :sim-back
               :text "Back to Simulation")

             [20 :by 10]
             (s/button
               :id :sim-zip-left
               :text "<"
               :listen
               [:action
                (fn [e]
                  (step-file-index simctx dec))])

             [10 :by 10]
             (s/button
               :id :sim-zip-right
               :text ">"
               :listen
               [:action
                (fn [e]
                  (step-file-index simctx inc))])]
            :else
            ["Size: "
             (s/combobox
               :listen
               [:action
                (fn [e]
                  (let [sel (.getSelectedIndex ^JComboBox (.getSource e))]
                    (set-board-size simctx (nth [9 13 19] sel))))]
               :model ["9x9" "13x13" "19x19"]
               :selected-index 2)
             [20 :by 10]
             "Place Mode: "
             (s/combobox
               :listen
               [:action
                (fn [e]
                  (set-board-mode (s/value (.getSource e))))]
               :model ["Alternating" "Black" "White" "Clear"])

             [20 :by 10]
             (s/button
               :text "Reset"
               :listen
               [:action
                (fn [e]
                  (when
                    (s/confirm "Are you sure?" :confirm-type :yes-no)
                    (reset-board simctx (-> @simctx :sim :size))))])
             [40 :by 10]
             (s/button
               :id :sim-load-zip
               :text "Load Captured ZIP")]))]

    (cond
      (= mode :replay)
      (s/listen (s/select panel [:#sim-back])
        :action
        (fn [e]
          (swap! simctx
            (fn [c]
              (->
                c
                (dissoc :replay)
                (assoc :mode :alt))))
          (s/replace! (.getParent panel) panel (button-panel simctx))))

      :else
      (s/listen (s/select panel [:#sim-load-zip])
        :action
        (fn [e]
          (ui.util/load-dialog
            (fn [f]
              (load-zip simctx (.getAbsolutePath f))
              (s/replace! (.getParent panel) panel (button-panel simctx)))
            (str (System/getProperty "user.dir") "/capture")))))
    panel))

(defn simulation-panel [ctx]
  (let [sketch
        (lq/sketch-panel
          {:draw (partial #'paint simctx)
           :setup (partial #'setup simctx)
           :mouse-pressed (partial #'mouse-pressed simctx)
           :key-pressed (partial #'key-pressed simctx)})

        buttons (button-panel simctx)
        ]
    (swap! simctx assoc :sketch sketch)
    (s/border-panel
      :center (:panel sketch)
      :south buttons)))

(defn stop []
  (swap! simctx assoc :stopped true))


================================================
FILE: src/igoki/sound/announce.clj
================================================
(ns igoki.sound.announce
  (:require
    [clojure.string :as str]
    [igoki.sgf :as sgf]
    [clojure.java.io :as io]
    [igoki.sound.sound :as snd])
  (:import (java.util.concurrent ThreadPoolExecutor TimeUnit LinkedBlockingQueue)))

;; Japanese words generated via google translate
;; Enlglish words generated via https://www.naturalreaders.com/online/

(comment
  ;; Words..
  "
  1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
  Ichi, Ni, San, Yon, Go, Roku, Nana, Hachi, Kyu, Ju,
  Ju-ichi, Ju-ni, Ju-san, Ju-yon, Ju-go, Ju-roku, Ju-nana, Ju-hachi, Ju-kyu
  Agohimo, Akisankaku, Atari, Atekomi, Boshi, Botsugi,
  Daidaigeima, Dan, Dango, Degiri, Fukure, Geta, Goken-biraki,
  Gote, Gote no sente, Gyaku komi, Hamete, Hana zuke, Hane,
  Hane-dashi, Hane-komi, Hara-zuke, Hasami, Hasami tsuke,
  Hazama tobi, Hekomi, Hiraki, Horikomi, Ikken tobi,
  Joseki, Kakari, Kaketsugi, Kannon-biraki, Kuruma no ato-oshi,
  Katatsuki, Kiri, Komi, Kosumi, Kosumi-dashi, Kosumi-tsuke,
  Kyu, Moku, Mokai komoku, Kenka Komoku, Nageru, Hanekaeshi,
  Nidan-bane, Niken biraki, Niken tobi, Nigiri, Nirensei,
  Nobi, Ogeima, Onadare, Owari, Ponnuki, Ryojimari, Ryoatari,
  Sagari, Sangen biraki, Sanrensei, Sente, Sente no gote,
  Shico, Shico-atari, Shimari, Kuro, Shiro, Shodan, Suberi,
  Susaoki, Susogakari, Tagai sen, Taisha, Takefu, Te, Teai, Tenuki, Tesuji, Tobi,
  Tetchu, Tsuke, Tsuke-koshi, Tsuki, Tsume-biraki, Warikomi,
  Wariuchi, Komoku, Hoshi, Sansan, Mokuhazushi, Takamoku, Oomokuhazushi,
  Ootakamoku, Gonogo, Tengen, Hoshishita, Hoshiwaki, NiNoIchi,
  Hidariue, migiue, hidarishita, migishita, keima kakari,
  ikken takagakari, ogeima takagakari, niken takagakari"
  )

(def sound-executor
  (ThreadPoolExecutor. 1 1 60 TimeUnit/MINUTES (LinkedBlockingQueue. 24)))

(defn announce [lang parts]
  (println "Announce: " parts)
  (.submit sound-executor
    (fn []
      (doseq [p (remove #(or (nil? %) (str/blank? %)) parts)]
        (case p
          "," (Thread/sleep 250)
          "-" (Thread/sleep 500)
          (snd/sound (str "public/sounds/" (name (or lang :en)) "/" p ".wav")))))))

(def soundmapping
  {:en
   {:players {:white "white" :black "black"}
    :coords
    {:join nil
     :x
     (->>
       (range 1 20)
       (map
         (fn [i]
           (let [c (char (+ 64 i (if (> i 8) 1 0)))]
             [i (str c)])))
       (into {}))

     :y
     (->>
       (range 1 20)
       (map
         (fn [i] [i (str "post" i)]))
       (into {}))}}
   :jp
   {:players {:white "Shiro" :black "Kuro"}
    :coords
    {:join "no"
     :x
     {1 "Ichi" 2 "Ni" 3 "San" 4 "Yon" 5 "Go" 6 "Roku" 7 "Nana" 8 "Hachi" 9 "Kyu" 10 "Ju"
      11 "Ju-ichi" 12 "Ju-ni" 13 "Ju-san" 14 "Ju-yon" 15 "Ju-go" 16 "Ju-roku"
      17 "Ju-nana" 18 "Ju-hachi" 19 "Ju-kyu"}

     :y
     {1 "Ichi" 2 "Ni" 3 "San" 4 "Yon" 5 "Go" 6 "Roku" 7 "Nana" 8 "Hachi" 9 "Kyu" 10 "Ju"
      11 "Ju-ichi" 12 "Ju-ni" 13 "Ju-san" 14 "Ju-yon" 15 "Ju-go" 16 "Ju-roku"
      17 "Ju-nana" 18 "Ju-hachi" 19 "Ju-kyu"}}}})

(def named-points
  {[3 3]   ["Hidariue" "Sansan"]
   [3 16]  ["Hidarishita" "Sansan"]
   [16 3]  ["Migiue" "Sansan"]
   [16 16] ["Migishita" "Sansan"]
   [10 10] ["Tengen"]})

(def opening-point
  {[3 3] "Sansan"
   [4 4] "Hoshi"
   [3 4] "Komoku"
   [3 5] "Mokuhazushi"
   [4 5] "Takamoku"
   [3 6] "Oomokuhazushi"
   [4 6] "Ootakamoku"
   [5 5] "Gonogo"
   [3 9] "Hoshishita"
   [3 10] "Hoshiwaki"
   [1 2] "NiNoIchi"})

(defn normalize [[x y]]
  (sort
    [(- 10 (Math/abs (int (- x 10))))
     (- 10 (Math/abs (int (- y 10))))]))

(defn board-area [[x y]]
  (if (or (= x 10) (= y 10))
    nil
    (if (< x 10)
      (if (< y 10)
        "Hidariue"
        "Hidarishita")
      (if (< y 10)
        "Migiue"
        "Migishita"))))

(defn lookup-sound [language path]
  (get-in soundmapping (concat [language] path)))

(defn comment-move [ctx node board]
  (let [{:keys [player language] :or {language :en}} (:announce @ctx)
        {:keys [black white]} node
        moves (or black white)
        position (first moves)
        [x y :as p] (map inc (sgf/convert-sgf-coord position))
        #_#_named (named-points p)
        #_#_opening [(board-area p) (opening-point (normalize p))]]

    (when
      (and
        ;; There shouldn't be black _and_ white moves to announce, else we'll just bombard
        (not (and black white))

        ;; There should also only be one move to announce, else, again, we'll bombard.
        (= 1 (count moves))

        ;; And the user should have requested which players to announce, specifically.
        (or
          (and white (:white player))
          (and black (:black player))))

      (announce
        language
        (concat
          (when (> (count player) 1)
            [(lookup-sound language [:players (if black :black :white)])
             ","])
          [(lookup-sound language [:coords :x x])
           (lookup-sound language [:coords :join])
           (lookup-sound language [:coords :y y])] #_(or named opening))))))

(defn set-announce-player [ctx player]
  (swap! ctx assoc-in [:announce :player]
    (case player
      :black #{:black}
      :white #{:white}
      :both #{:black :white}
      #{})))

(defn set-announce-language [ctx langkey]
  (swap! ctx assoc-in [:announce :language] langkey))



================================================
FILE: src/igoki/sound/sound.clj
================================================
(ns igoki.sound.sound
  (:import
    (javax.sound.sampled AudioSystem LineListener LineEvent LineEvent$Type)
    (java.util.concurrent CountDownLatch)))

(def get-clip
  (memoize
    (fn [file]
      #_(println "Loading:" file)
      (let [ais  (AudioSystem/getAudioInputStream (ClassLoader/getSystemResource file))
            clip (AudioSystem/getClip)]
        (.open clip ais)
        clip))))

(defn sound [file]
  (try
    (let [clip (get-clip file)
          latch (CountDownLatch. 1)
          listener
          (proxy [LineListener] []
            (update [^LineEvent e]
              (when (= (.getType e) LineEvent$Type/STOP)
                (.countDown latch))))]
      (.addLineListener clip listener)
      (.setFramePosition clip 0)
      (.start clip)
      (.await latch)
      (.removeLineListener clip listener))
    (catch Exception e)))

(def sounds
  {:click  "public/sounds/click.wav"
   :undo   "public/sounds/back.wav"
   :submit "public/sounds/submit.wav"})

(defn play-sound [soundkey]
  (if-let [s (get sounds soundkey)]
    (doto (Thread. #(sound s))
      (.setDaemon true)
      (.start))))


================================================
FILE: src/igoki/ui/calibration.clj
================================================
(ns igoki.ui.calibration
  (:require
    [seesaw.core :as s]
    [igoki.camera :as camera]
    [igoki.litequil :as lq]
    [igoki.util :as util]
    [igoki.projector :as projector]
    [clojure.java.io :as io]
    [igoki.ui.util :as ui.util])
  (:import
    (javax.swing JComboBox BorderFactory)
    (java.awt Cursor Insets)))

(defn calibration-options [ctx]
  (s/flow-panel
    :items
    ["Size: "
     (s/combobox
       :listen
       [:action
        (fn [e]
          (let [sel (.getSelectedIndex ^JComboBox (.getSource e))]
            (camera/set-board-size ctx (nth [9 13 19] sel))))]
       :model ["9x9" "13x13" "19x19"]
       :selected-index 2)
     [20 :by 10]
     "Camera: "
     (s/combobox
       :listen
       [:action
        (fn [e]
          (if (.getParent (.getSource e))
            (.grabFocus (.getParent (.getSource e))))
          (doto
            (Thread.
              #(camera/select-camera ctx (- (.getSelectedIndex ^JComboBox (.getSource e)) 2)))
            (.setDaemon true)
            (.start)))]
       :model
       (concat
         ["Off"
          "Simulated"]
         (for [x (range 5)]
           (str "Camera " (inc x))))
       :selected-index 0)
     [20 :by 10]

     [20 :by 10]
     (s/button
       :id :kofi-button
       :icon (io/resource "kofi.png")
       :text "Support me on Ko-fi"
       :focusable? false
       :listen
       [:action
        (fn [e]
          (ui.util/open "https://ko-fi.com/cmdrdats"))])]))

(defn construct [ctx]
  (lq/smooth)
  (lq/frame-rate 5)
  (lq/background 200))

(defn convert-point [bufimg [px py]]
  [(/ (* px (lq/width)) (.getWidth bufimg))
   (/ (* py (lq/height)) (.getHeight bufimg))])

(def pn ["A1" "T1" "T19" "A19"])

(defn draw [ctx]
  (lq/background 128 64 78)
  (lq/rect 0 0 (lq/width) (lq/height))

  (let [c (camera/camera-image ctx)]
    (cond
      (nil? c)
      (lq/shadow-text "Could not acquire image?" 10 25)

      :else
      (let [{{:keys [size edges points lines flat flat-view? camerapoints]} :goban
             board :board} @ctx

            points (map (partial convert-point c) points)
            edges (map #(map (partial convert-point c) %) edges)]
        (lq/image c 0 0 (lq/width) (lq/height))
        (lq/shadow-text "Please select the corners of the board" 10 25)


        (lq/color 255 255 255 128)
        (lq/stroke-weight 0.5)
        (when (and camerapoints board size)
          (doseq [[idx p] (map-indexed vector camerapoints)
                  :let [[px py] (convert-point c p)
                        stone (get-in board [(int (/ idx size)) (mod idx size)])]
                  :when stone]
            (if (= stone :b)
              (do (lq/background 0 0 0) (lq/color 255 255 255))
              (do (lq/background 255 255 255) (lq/color 0 0 0)))
            (lq/ellipse px py 10 10)))



        (lq/color 255 255 255 96)
        (lq/stroke-weight 1)
        (when lines
          (doseq [[p1 p2] lines]
            (lq/line (convert-point c p1) (convert-point c p2)))
          (lq/shadow-text
            (str size "x" size)
            (/ (reduce + (map first points)) 4)
            (/ (reduce + (map second points)) 4)
            :center :bottom))

        (lq/color 78 64 255 128)
        (lq/stroke-weight 2)
        (doseq [[p1 p2] edges]
          (lq/line p1 p2))


        (doseq [[p [x y]] (map-indexed vector points)]
          (lq/text (get pn p) x (- y 5)
            {:align [:center :bottom]})
          (lq/ellipse x y 2 2))
        (when (and flat flat-view?)
          (lq/image (:bufimg flat) 0 0 (lq/width) (lq/height)))))))


(defn mouse-dragged [ctx e]
  (when-let [size (camera/camera-size ctx)]
    (let [[cx cy] size
          p
          [(/ (* (lq/mouse-x) cx) (lq/width))
           (/ (* (lq/mouse-y) cy) (lq/height))]

          points (get-in @ctx [:goban :points])
          points (util/update-closest-point points p)]
      (camera/update-corners ctx points))))

(defn mouse-pressed [ctx e]
  (when-let [size (camera/camera-size ctx)]
    (let [[cx cy] size
          p
          [(/ (* (lq/mouse-x) cx) (lq/width))
           (/ (* (lq/mouse-y) cy) (lq/height))]

          points (get-in @ctx [:goban :points])
          points
          (if (> (count points) 3)
            (util/update-closest-point points p)
            (vec (conj points p)))]
      (camera/update-corners ctx points))))


;; TODO: This isn't even working - this needs to all become UI elements or just straight
;; out dropped.
(defn key-typed [ctx e]
  (case (lq/key-code e)
    32 (swap! ctx update-in [:goban :flat-view?] (fnil not false))
    67 (camera/cycle-corners ctx)
    (println "Unhandled key-down: " (lq/key-code e))))

(defn calibration-panel [ctx]
  (let [panel
        (:panel
          (lq/sketch-panel
            {:setup (partial #'construct ctx)
             :draw (partial #'draw ctx)
             :mouse-dragged (partial #'mouse-dragged ctx)
             :mouse-pressed (partial #'mouse-pressed ctx)
             :key-typed (partial #'key-typed ctx)}))]
    (.setCursor panel (Cursor/getPredefinedCursor Cursor/CROSSHAIR_CURSOR))
    (s/border-panel
      :minimum-size [10 :by 10]
      :id :calibration-panel
      :south (calibration-options ctx)
      :center panel)))

================================================
FILE: src/igoki/ui/game.clj
================================================
(ns igoki.ui.game
  (:require
    [igoki.util :as util]
    [igoki.litequil :as lq]
    [igoki.sgf :as sgf]
    [igoki.ui.util :as ui.util]
    [igoki.game :as game]
    [seesaw.core :as s]
    [seesaw.color :as sc]
    [igoki.sound.announce :as announce]
    [seesaw.mig :as sm])
  (:import
    (java.io File)))


(defn export-sgf [ctx]
  (ui.util/save-dialog
    (:current-file @ctx)
    #(spit % (game/convert-sgf ctx))))

(defn load-sgf [ctx]
  (ui.util/load-dialog
    (fn [^File f]
      (println "Opening sgf: " (.getAbsolutePath f))
      (game/load-sgf ctx f))))


(def move-colours
  {0 {:white [0 0 0] :black [255 255 255]}
   1 {:white [255 64 64] :black [255 96 96]}
   2 {:white [0 150 0] :black [64 255 64]}
   3 {:white [32 32 255] :black [128 128 255]}
   4 {:white [255 255 0] :black [255 255 0]}
   5 {:white [0 255 255] :black [0 255 255]}
   6 {:white [255 0 255] :black [255 0 255]}})

(defn draw [ctx]
  (lq/stroke-weight 1)
  (lq/color 0)
  (lq/background 255 255 255)
  (lq/rect 0 0 (lq/width) (lq/height))

  ;; Draw the board
  (let [{{:keys [submit kifu-board constructed movenumber] :as game} :kifu
         {:keys [pimg flattened-pimage]} :camera
         board :board
         {:keys [size ] :or {size 19}} :goban} @ctx
        cellsize (/ (lq/height) (+ size 2))
        grid-start (+ cellsize (/ cellsize 2))
        board-size (* cellsize (dec size))
        extent (+ grid-start board-size)
        tx (+ (lq/height) (/ cellsize 2))
        visiblepath (take (or movenumber 0) (mapcat identity (:current-branch-path game)))
        actionlist (sgf/current-branch-node-list [visiblepath] (:moves game))
        lastmove (last actionlist)
        canvas-size (max 250 (min (lq/width) (lq/height)))]




    (when flattened-pimage
      (lq/image (:bufimg flattened-pimage)
        (- grid-start cellsize) (- grid-start cellsize)
        (+ board-size (* cellsize 2)) (+ board-size (* cellsize 2))))

    (lq/color 220 179 92 150)
    (lq/fillrect 0 0 canvas-size canvas-size)


    (lq/stroke-weight 0.8)
    (lq/color 0 196)
    (lq/background 0)

    ;; Draw the grid
    (lq/text-font "helvetica-20pt")

    (doseq [x (range size)]
      (let [coord (+ grid-start (* x cellsize))
            letter (char (+ 65 x (if (> x 7) 1 0)))]
        (lq/text (str letter) coord (- grid-start (/ cellsize 2))
          {:align [:center :bottom]})
        (lq/text (str letter) coord (+ extent (/ cellsize 2))
          {:align [:center :top]})

        (lq/text (str (inc x))
          (- grid-start (/ cellsize 2)) coord
          {:align [:right :center]})
        (lq/text (str (inc x)) (+ extent (/ cellsize 2)) coord
          {:align [:left :center]})

        (lq/line coord grid-start coord extent)
        (lq/line grid-start coord extent coord)))

    ;; Draw star points
    (doseq [[x y] (util/star-points size)]
      (lq/stroke-weight 1)
      (lq/color 0 32)
      (lq/background 0)
      (lq/ellipse
        (+ grid-start (* x cellsize))
        (+ grid-start (* y cellsize)) 6 6))

    ;; Draw camera board (shadow)
    (doseq [[y row] (map-indexed vector board)
            [x d] (map-indexed vector row)]
      (when d
        (lq/stroke-weight 1)
        (lq/color 0 32)
        (lq/background (if (= d :w) 255 0) 32)
        (lq/ellipse
          (+ grid-start (* x cellsize))
          (+ grid-start (* y cellsize))
          (- cellsize 3) (- cellsize 3))))

    (lq/text-size 12)

    ;; Draw the constructed sgf board stones
    (doseq [[pt {:keys [stone] mn :movenumber}] (:board constructed)]
      (let [[x y :as p] (sgf/convert-sgf-coord pt)]
        (when (and p stone)
          (lq/stroke-weight 0.5)
          (lq/color 0)
          (lq/background (if (= stone :white) 255 0))
          (lq/ellipse (+ grid-start (* x cellsize))
            (+ grid-start (* y cellsize)) (- cellsize 2) (- cellsize 2))

          (lq/background (if (= stone :white) 0 255)))

        (when (and (not stone) mn)
          (lq/stroke-weight 0)
          (lq/color 220 179 92)
          (lq/background 220 179 92)
          (lq/ellipse (+ grid-start (* x cellsize))
            (+ grid-start (* y cellsize)) 20 20)

          (lq/background 0))

        (when (and mn (< (- movenumber mn) 40))
          (let [movediff (- movenumber mn)
                movenum (mod (inc mn) 100)
                movecol (get-in move-colours [(int (/ mn 100)) (or stone :black)] [0 0 0])
                movecol
                (if (> movediff 20)
                  (conj movecol (- 255 (* 255 (/ (- movediff 20) 20))))
                  movecol)]
            (apply lq/color movecol)

            (lq/text-size 12)
            (lq/text (str movenum) (+ grid-start (* x cellsize)) (- (+ grid-start (* y cellsize)) 1)
              {:align [:center :center]})))))

    ;; TODO: This should go out to its own panel.
    (when (:comment lastmove)
      (lq/color 0)
      (lq/text-size 12)
      (lq/text (first (:comment lastmove)) tx 240 (- (lq/width) tx) (lq/height)
        {:align [:left :top]}))


    ;; Draw labels
    (doseq [label (:label lastmove)]
      (let [[pt text] (.split label ":" 2)
            [x y :as p] (sgf/convert-sgf-coord pt)
            stone (nth (nth kifu-board y) x)]

        (cond
          (= stone :w)
          (do
            (lq/background 255)
            (lq/color 255))

          (= stone :b)
          (do
            (lq/background 0)
            (lq/color 0))

          :else
          (do
            (lq/background 220 179 92)
            (lq/color 220 179 92)))

        (lq/stroke-weight 0)
        (lq/ellipse (+ grid-start (* x cellsize))
          (+ grid-start (* y cellsize)) (/ cellsize 1.5) (/ cellsize 1.5))
        (lq/background (if (= stone :b) 255 0))

        (lq/color 0)
        (lq/text
          text
          (+ grid-start (* x cellsize))
          (- (+ grid-start (* y cellsize)) 1)
          {:align [:center :center]})))

    ;; Draw annotated triangles.
    (doseq [pt (:triangle lastmove)]
      (let [[x y :as p] (sgf/convert-sgf-coord pt)
            stone (nth (nth kifu-board y) x)]

        (lq/stroke-weight 0)
        (apply lq/background (cond (= stone :b) [0] (= stone :w) [255] :else [220 179 92]))
        (lq/ellipse (+ grid-start (* x cellsize))
          (+ grid-start (* y cellsize)) (/ cellsize 1.1) (/ cellsize 1.1))

        (lq/stroke-weight 2)
        (lq/color (if (= stone :b) 255 0))
        (lq/triangle
          (+ grid-start (* x cellsize)) (- (+ grid-start (* y cellsize)) 6)
          (- (+ grid-start (* x cellsize)) 6) (+ (+ grid-start (* y cellsize)) 4.5)
          (+ (+ grid-start (* x cellsize)) 6) (+ (+ grid-start (* y cellsize)) 4.5))))

    ;; If in the process of submitting, mark that stone.
    (when submit
      #_(let [[x y _ d] (:move submit)]
          (lq/stroke-weight 1)
          (lq/stroke 0 128)
          (lq/background (if (= d :w) 255 0) 128)
          (lq/ellipse
            (+ grid-start (* x cellsize))
            (+ grid-start (* y cellsize))
            (- cellsize 3) (- cellsize 3))
          (lq/background (if (= d :w) 0 255))
          (lq/text "?" (+ grid-start (* xcellsize)) (+ grid-start (* y cellsize))
            {:align [:center :center]})))

    ;; Mark the last move
    (when lastmove
      (let [{:keys [black white]} lastmove]
        (doseq [m (or black white)]
          (let [[x y :as p] (sgf/convert-sgf-coord m)]
            (when p
              (lq/color (if white 0 255))
              (lq/stroke-weight 3)
              (lq/background 0 0)
              (lq/ellipse (+ grid-start (* x cellsize))
                (+ grid-start (* y cellsize)) (/ cellsize 2) (/ cellsize 2))))))

      ;; Mark next branches
      (when (:show-branches game)
        (doseq [[idx {:keys [black white]}] (map-indexed vector (:branches lastmove))
                m (or black white)]
          (let [[x y :as p] (sgf/convert-sgf-coord m)]
            (when p
              (if (zero? idx)
                (lq/color (if white 255 0))
                (apply lq/color (if white [255 0 0] [0 0 255])))
              (lq/stroke-weight 3)
              (lq/background 0 0)
              (lq/ellipse (+ grid-start (* x cellsize))
                (+ grid-start (* y cellsize)) (/ cellsize 2) (/ cellsize 2))
              (lq/background 0)

              (when (pos? idx)
                (lq/text-size 9)
                (lq/text
                  (str idx)
                  (- (+ grid-start (* x cellsize)) 9)
                  (- (+ grid-start (* y cellsize)) 9))))))))

    ;; Highlight differences between constructed and camera board (visual syncing)
    (when (and board kifu-board)
      (doseq [[x y _ _]
              (game/board-diff kifu-board board)]
        (lq/stroke-weight 3)
        (lq/color 255 0 0)
        (lq/background 0 0)
        (lq/ellipse (+ grid-start (* x cellsize))
          (+ grid-start (* y cellsize)) (- cellsize 3) (- cellsize 3))))))


(defn game-panel [ctx]
  (let [panel
        (:panel
          (lq/sketch-panel
            {:draw (partial #'draw ctx)}))

        container
        (s/border-panel
          :minimum-size [10 :by 10]
          :south
          (sm/mig-panel
            :constraints ["center"]
            :items
            [[(s/label :text "" :id :record-status) "spanx, wrap"]
             [(s/toggle :text "Debug ZIP" :id :record-debug
                :selected? false
                :listen
                [:action
                 (fn [e]
                   (swap! ctx assoc :debug-capture (s/value (.getSource e))))])
              ""]
             [(s/button :text "<"
                :listen
                [:action (fn [e] (game/move-backward ctx))])
              ""]
             [(s/button :text ">"
                :listen
                [:action (fn [e] (game/move-forward ctx))])
              ""]
             [[20 :by 10] ""]
             [(s/button :text "Pass"
                :listen
                [:action (fn [e] (game/pass ctx))])
              ""]
             [[20 :by 10] ""]
             [(s/toggle :text "Show Branches"
                :listen
                [:action
                 (fn [e]
                   (game/toggle-branches ctx (s/value (.getSource e))))])
              "wrap"]
             [[20 :by 10] "grow"]
             [(s/label :text "Announce ") ""]
             [(s/combobox
                :listen
                [:action
                 (fn [e]
                   (announce/set-announce-player ctx
                     (case (s/value (.getSource e))
                       "Black" :black
                       "White" :white
                       "Both" :both
                       nil)))]
                :model ["None" "Black" "White" "Both"])
              ""]
             [(s/label :text " in ") ""]
             [(s/combobox
                :listen
                [:action
                 (fn [e]
                   (announce/set-announce-language ctx
                     (case (s/value (.getSource e))
                       "English" :en
                       "Japanese" :jp)))]
                :model ["English" "Japanese"])
              ""]
             [[20 :by 10] ""]
             [(s/label :text "" :id :game-status) ""]
             ])
          :center panel)]

    (util/add-watch-path ctx :kifu
      [:kifu]
      (fn [k r o {:keys [movenumber constructed] :as game}]
        (s/config! (s/select container [:#record-status]) :text
          (str "Img:" (:camidx game) " at " (:filename game)))
        (s/config! (s/select container [:#game-status]) :text
          (str "Move " (inc (or movenumber 0)) ", " (if (= (:player-turn constructed) :black) "Black" "White") " to play"))))

    container))

================================================
FILE: src/igoki/ui/main.clj
================================================
(ns igoki.ui.main
  (:require
    [seesaw.core :as s]
    [igoki.ui.game :as ui.game]
    [igoki.ui.calibration :as calibration]
    [igoki.ui.ogs :as ogs]
    [igoki.ui.tree :as tree]
    [igoki.ui.robot :as robot]
    [igoki.game :as game]
    [igoki.simulated :as sim]
    [igoki.camera :as camera]
    [igoki.ui.util :as ui.util]
    [igoki.ui.projector :as ui.projector])
  (:import (javax.swing JFrame)))

(s/native!)

(defn ogs-panel [ctx]
  (s/tabbed-panel
    :minimum-size [10 :by 10]
    :placement :bottom
    :overflow :scroll
    :tabs
    [{:title "OGS"
      :tip "Online-go.com integration"
      :content (ogs/ogs-panel ctx)}
     {:title "Manual"
      :tip "Manual screen integration"
      :content (robot/robot-panel ctx)}
     {:title "Projector"
      :type "Projector setup/settings"
      :content (ui.projector/projector-panel ctx)}
     {:title "Screen"
      :tip "Simulation (dev tools)"
      :content
      (sim/simulation-panel ctx)}]))

(defn tree-panel [ctx]
  (s/tabbed-panel
    :minimum-size [10 :by 10]
    :placement :bottom
    :overflow :scroll
    :tabs
    [{:title "Tree"
      :tip "SGF Move tree"
      :content
      (tree/tree-panel ctx)}
     #_{:title "Log"
      :tip "Output log (dev tools)"
      :content
      (logging/log-panel ctx)
      }]))

(defn primary-splits [ctx]
  (let [cl
        (s/top-bottom-split
          (calibration/calibration-panel ctx)
          (ogs-panel ctx)
          :border 0
          :resize-weight 0.5
          :divider-location 0.5)

        gt
        (s/top-bottom-split
          (ui.game/game-panel ctx)
          (tree-panel ctx)
          :border 0
          :resize-weight 0.5
          :divider-location 0.5)]
    (s/left-right-split
      cl gt
      :resize-weight 0.5
      :divider-location 0.5)))



(defn main-menu [ctx]
  (s/menubar
    :items
    [(s/menu :text "File"
       :items
       [(s/action
          :mnemonic \n
          :name "New SGF..."
          :key "menu N"
          :handler
          (fn [e]
            (when
              (s/confirm "Reset to new SGF recording, are you sure?"
                :title "New SGF"
                :type :warning
                :option-type :yes-no)
              (game/reset-kifu ctx))))
        (s/action
          :mnemonic \o
          :name "Open SGF"
          :key "menu O"
          :handler
          (fn [e]
            (ui.game/load-sgf ctx)))

        (s/action
          :mnemonic \s
          :name "Save SGF"
          :key "menu S"
          :handler
          (fn [e]
            (ui.game/export-sgf ctx)))


        :separator
        (s/action
          :mnemonic \x
          :name "Exit"
          :handler
          (fn [e]
            (when
              (s/confirm "Exiting, are you sure?"
                :title "Exit"
                :type :warning
                :option-type :yes-no)
              (camera/stop-read-loop ctx)
              (System/exit 0))))])

     (s/menu :text "Help"
       :items
       [(s/action
          :name "Website"
          :handler
          (fn [e]
            (ui.util/open "https://github.com/cmdrdats/igoki")))
        (s/action
          :name "Update"
          :handler
          (fn [e]
            (ui.util/open "https://github.com/CmdrDats/igoki/releases")))

        :separator
        (str "Version: " (System/getProperty "igoki.version"))])
     ]))

(defn frame-content [ctx]
  (s/border-panel
    :center (primary-splits ctx)))

(defonce app-frame (atom nil))
(defn main-frame [ctx]
  (let [frame
        (s/frame
          :icon "igoki48.png"
          :title "igoki"
          :size [1024 :by 768]
          :menubar (main-menu ctx)
          :on-close :exit)]
    (.setExtendedState frame JFrame/MAXIMIZED_BOTH)
    (-> frame s/show!)
    (s/config! frame :content (frame-content ctx))
    (reset! app-frame frame))
  #_(open
    {:title "igoki"
     :body
     [:button "Push me"]}))

(defn refresh [ctx]
  (s/config! @app-frame :menubar
    (main-menu ctx)
    :content (frame-content ctx)))

================================================
FILE: src/igoki/ui/ogs.clj
================================================
(ns igoki.ui.ogs
  (:require
    [seesaw.core :as s]
    [seesaw.mig :as sm]
    [seesaw.color :as sc]
    [igoki.integration.ogs :as ogs]
    [igoki.ui.util :as ui.util]
    [igoki.util :as util])
  (:import
    (java.awt.event KeyEvent)
    (java.awt Graphics2D)
    (java.awt.geom Ellipse2D$Double)
    (javax.swing DefaultListCellRenderer)))

(declare game-list-panel)
(defn game-info-panel [ctx]
  (let [{:keys [game_name players json]} (get-in @ctx [:ogs :game])
        {:keys [black white]} players

        panel
        (sm/mig-panel
          :constraints ["center"]
          :items
          [["Game Details" "wrap, span, center, gapbottom 20"]
           ["Game Name:" "align label"]
           [(str game_name) "wrap"]
           ["Black: " "align label"]
           [(ogs/str-player black) "wrap"]
           ["White: " "align label"]
           [(ogs/str-player white) "wrap"]])

        container
        (s/border-panel
          :south
          (s/flow-panel
            :align :center
            :items
            [(s/button :text "Disconnect" :id :ogs-game-disconnect)])

          :center
          (s/scrollable panel :vscroll :always))]

    (s/listen (s/select container [:#ogs-game-disconnect])
      :action
      (fn [e]
        (ogs/disconnect-record ctx)
        (s/replace!
          (.getParent container) container (game-list-panel ctx))))

    container))


(defn paint-game-panel [ctx game c ^Graphics2D g]
  (.setColor g (sc/color "#dcb35c"))
  (let [[w h] [(.getWidth c) (.getHeight c)]
        board-width (:width game)
        ;; TODO: Need to do a few fixes for non-square boards....
        board-height (:height game)
        cellsize (/ h (+ board-width 2))
        grid-start (+ cellsize (/ cellsize 2))
        board-size (* cellsize (dec board-width))
        extent (+ grid-start board-size)
        tx (+ h (/ cellsize 2))

        constructed (:constructed game)
        kifu-board (get-in constructed [:kifu :kifu-board])
        ]
    (.fillRect g 0 0 w h)

    (.setColor g (sc/color :black))
    (doseq [x (range board-width)]
      (let [coord (+ grid-start (* x cellsize))]
        (.drawLine g coord grid-start coord extent)
        (.drawLine g grid-start coord extent coord)))

    ;; Draw star points
    (doseq [[x y] (util/star-points board-width)]
      (let [e (Ellipse2D$Double. (+ grid-start (- (* x cellsize) 1.5))
                (+ grid-start (- (* cellsize y) 1.5)) 4 4)]
        (.fill g e)))


    (doseq [y (range board-height)]
      (doseq [x (range board-width)]
        (let [stone (nth (nth kifu-board y) x)]
          (when stone
            (let [e (Ellipse2D$Double. (+ grid-start (- (* x cellsize) 4))
                      (+ grid-start (- (* cellsize y) 4)) 8 8)]
              (.setColor g (sc/color (if (= stone :w) :white :black)))
              (.fill g e)
              (when (= stone :w)
                (.setColor g (sc/color :black))
                (.draw g e)))))))))

(defn game-panel [ctx game selected?]
  (let [black (:black game)
        white (:white game)]
    (sm/mig-panel
      :background (if selected? :steelblue nil)
      :constraints [""]
      :items
      [[(s/canvas
          :size [200 :by 200]
          :paint (partial #'paint-game-panel ctx game)) "left, spany"]
       [(ogs/str-player black) "wrap, gapleft 10"]
       [(ogs/str-player white) "wrap, gapleft 10"]])))

(declare ogs-login-panel)
(defn game-list-panel [ctx]
  (let [setup-model
        #(map
           (fn [g]
            (assoc g :constructed
              (ogs/initialize-game {} (:json g))))
           (get-in @ctx [:ogs :overview :active_games]))

        gamelist
        (s/listbox
          :id :ogs-game-list
          :model (setup-model)
          :renderer
          (proxy [DefaultListCellRenderer] []
            (getListCellRendererComponent [component value index selected? foxus?]
              (game-panel ctx value selected?))))

        status-label (s/label :text "" :id :ogs-connect-status)
        container
        (s/border-panel
          :south
          (s/flow-panel
            :align :center
            :items
            [(s/button :text "Disconnect" :id :ogs-disconnect)
             [40 :by 10]
             (s/button :text "Refresh" :id :ogs-refresh)
             (s/button :text "Connect to selected" :id :ogs-connect-selected)
             status-label])
          :center
          (s/scrollable gamelist
            :vscroll :always))]

    (s/listen
      (s/select container [:#ogs-connect-selected])
      :action
      (fn [e]
        (let [game (s/value gamelist)
              ogs (:ogs @ctx)

              {:keys [success msg]}
              (ogs/connect-record ctx (:socket ogs)
                (str (:id game)) (:auth ogs))]
          (if success
            (s/replace! (.getParent container) container (game-info-panel ctx))
            (s/config! status-label :text msg)))))

    (s/listen
      (s/select container [:#ogs-refresh])
      :action
      (fn [e]
        (ogs/refresh-games ctx)
        (s/config!
          (s/select container [:#ogs-game-list])
          :model (setup-model))))

    (s/listen
      (s/select container [:#ogs-disconnect])
      :action
      (fn [e]
        (ogs/disconnect ctx)
        (s/replace!
          (.getParent container) container (ogs-login-panel ctx))))
    container))

(defn ogs-login-panel [ctx]
  (let [settings (ogs/load-settings)
        login-panel
        (sm/mig-panel
          :constraints ["center" "" ""]
          :items
          [["Online Go Bot Credentials" "span, center, gapbottom 15"]
           ["Generate API details:" "align label"]
           [(s/button :text "Open Browser" :id :open) "wrap"]
           ["Client ID: " "align label"]
           [(s/text :id :client-id :columns 32) "wrap"]
           ["Client Secret: " "align label"]
           [(s/password :id :client-secret :columns 24) "wrap"]
           ["Client Type: " "align label"]
           ["Confidential" "wrap"]
           ["Authorization Grant Type: " "align label"]
           ["Password" "wrap"]
           ["" "span, center, gapbottom 15"]
           ["User login credentials" "span, center, gapbottom 15"]
           ["Username: " "align label"]
           [(s/text :id :username :columns 24) "wrap"]
           ["Password: " "align label"]
           [(s/password :id :password :columns 20) "wrap"]
           ["" "align label"]
           [(s/checkbox :id :remember :text "Remember password") "wrap"]
           [(s/label :id :progress :text "Progress")]
           [(s/button :text "Connect" :id :save) "tag ok, span, split 3, sizegroup bttn, gaptop 15"]])


        login
        (fn []
          (doto
            (Thread.
              #(let [result
                     (ogs/connect ctx (s/value login-panel)
                       (fn [e]
                         (s/config! (s/select login-panel [:#progress])
                           :text (str e))))]
                 (s/config! (s/select login-panel [:#progress])
                   :text
                   (if (:success result)
                     (str "Connected.")
                     (:message result)))
                 (s/replace! (.getParent login-panel) login-panel (game-list-panel ctx))))

            (.setDaemon true)
            (.start)))]
    (s/value! login-panel settings)

    ;; Form listening for submit.
    (s/listen
      (s/select login-panel [:#open])
      :action
      (fn [e]
        (ui.util/open "https://online-go.com/oauth2/applications/")))

    (s/listen
      (concat
        (s/select login-panel [:JPasswordField])
        (s/select login-panel [:JTextField]))
      :key-released
      (fn [e]
        (when (= KeyEvent/VK_ENTER (.getKeyCode e))
          (login))))

    (s/listen
      (s/select login-panel [:#save])
      :action (fn [e] (login)))
    login-panel))



(defn ogs-panel [ctx]
  (let [login-panel (ogs-login-panel ctx)
        panel (s/border-panel :id :ogs-panel :center login-panel)]
    panel))

================================================
FILE: src/igoki/ui/projector.clj
================================================
(ns igoki.ui.projector
  (:require [seesaw.core :as s]
            [igoki.projector :as projector]
            [seesaw.mig :as sm]))

(defn refresh-button-states [ctx container]
  (let [{:keys [sketch calibrate?] :as s} @projector/proj-ctx
        _ (println "proj-ctx" s)
        {:keys [frame] :as f} (when sketch @sketch)
        _ (println "sketch" f)
        _ (println "frame" frame)
        {:keys [robot]} @ctx
        states
        [[:#projector-open-button (not frame)]
         [:#projector-calibration-grid (and frame (not calibrate?))]
         [:#projector-calibration-accept (and frame calibrate?)]
         [:#projector-close-button frame]]]

    (println "STATES" states)
    (doseq [[id state] states]
      (println "Setting " (s/select container [id])
        "to " state)
      ((if state s/show! s/hide!)
       (s/select container [id])))))

(defn projector-panel [ctx]
  (let [container (s/border-panel)
        refresh
        #(refresh-button-states ctx container)]
    (s/config! container :center
      (s/border-panel
        :north
        (sm/mig-panel
          :constraints ["center"]
          :items
          [[(s/button
              :id :projector-open-button
              :text "Projector Window"
              :listen
              [:action
               (fn [e]
                 (projector/start-cframe ctx refresh)
                 (refresh))]) "wrap"]
           [(s/button
              :id :projector-calibration-grid
              :text "Calibration Grid"
              :listen
              [:action
               (fn [e]
                 (projector/show-calibration)
                 (refresh))]) "wrap"]

           [(s/button
              :id :projector-calibration-accept
              :text "Accept Calibration"
              :listen
              [:action
               (fn [e]
                 (projector/accept-calibration ctx)
                 (refresh))]) "wrap"]

           [(s/button
              :id :projector-close-button
              :text "Close capture frame"
              :listen
              [:action
               (fn [e]
                 (projector/stop-cframe ctx refresh))]) "wrap"]])

        #_#_:center
            (game-setup-panel ctx container)

        #_#_:south
        (s/flow-panel
          :items
          [(s/button
             :id :robot-start-capture
             :text "Start Recording"
             :visible? false
             :listen
             [:action
              (fn [e] (robot-start-capture ctx container))])

           (s/button
             :id :robot-pause-capture
             :text "Pause Recording"
             :visible? false
             :listen
             [:action
              (fn [e] (robot-pause-capture ctx container))])

           (s/button
             :id :robot-unpause-capture
             :text "Unpause Recording"
             :visible? false
             :listen
             [:action
              (fn [e] (robot-unpause-capture ctx container))])

           (s/button
             :id :robot-stop-capture
             :text "Stop Recording"
             :visible? false
             :listen
             [:action
              (fn [e] (robot-stop-capture ctx container))])])
        ))
    (refresh)
    container))

================================================
FILE: src/igoki/ui/robot.clj
================================================
(ns igoki.ui.robot
  (:require
    [seesaw.core :as s]
    [seesaw.color :as sc]
    [igoki.litequil :as lq]
    [igoki.util :as util]
    [igoki.integration.robot :as i.robot]
    [igoki.camera :as camera]
    [seesaw.mig :as sm])
  (:import
    (java.awt.event MouseEvent)
    (javax.swing JFrame)
    (java.awt Rectangle Cursor Graphics2D BasicStroke)
    (java.awt.image BufferedImage)
    (org.nd4j.linalg.exception ND4JIllegalStateException)))


(defn get-mouse-op [ctx x y sx sy]
  (let [started? (get-in @ctx [:robot :started])]
    (cond
      started? [:none :none]

      :else
      [(cond
         (< x 25) :west
         (> x (- sx 25)) :east
         :else :move)

       (cond
         (< y 25) :north
         (> y (- sy 25)) :south
         :else :move)])))

(defn apply-xop [^Rectangle bounds [xop yop] mdx]
  ;; Yes, creating duplicate objects. Mutation sucks. A hill I'll die on.
  (let [result (Rectangle. bounds)]
    (case xop
      :west
      (.setBounds result
        (+ (.getX bounds) mdx) (.getY bounds)
        (- (.getWidth bounds) mdx) (.getHeight bounds))
      :east
      (.setSize result
        (+ (.getWidth bounds) mdx) (.getHeight bounds))

      ;; Only move if the other is _also_ move, else it's non-standard behaviour
      :move
      (when (= yop :move)
        (.setLocation result
          (+ (.getX bounds) mdx) (.getY bounds)))

      nil)
    result))

(defn apply-yop [^Rectangle bounds [xop yop] mdy]
  ;; Yes, creating duplicate objects. Mutation sucks. A hill I'll die on.
  (let [result (Rectangle. bounds)]
    (case yop
      :north
      (.setBounds result
        (.getX bounds) (+ (.getY bounds) mdy)
        (.getWidth bounds) (- (.getHeight bounds) mdy))
      :south
      (.setSize result
        (.getWidth bounds) (+ (.getHeight bounds) mdy))

      ;; Only move if the other is _also_ move, else it's non-standard behaviour
      :move
      (when (= xop :move)
        (.setLocation result
          (.getX bounds) (+ (.getY bounds) mdy)))

      nil)
    result))

(defn setup-resize-bounds [ctx ^JFrame frame]
  (let [state (atom {})]
    (s/listen frame
      :mouse-moved
      (fn [^MouseEvent e]
        (let [size (.getSize frame)
              [xop yop] (get-mouse-op ctx (.getX e) (.getY e) (.getWidth size) (.getHeight size))
              cursor
              (Cursor/getPredefinedCursor
                (cond
                  (and (= yop :north) (= xop :east)) Cursor/NE_RESIZE_CURSOR
                  (and (= yop :north) (= xop :west)) Cursor/NW_RESIZE_CURSOR
                  (and (= yop :south) (= xop :east)) Cursor/SE_RESIZE_CURSOR
                  (and (= yop :south) (= xop :west)) Cursor/SW_RESIZE_CURSOR
     
Download .txt
gitextract_2g4iif2j/

├── .github/
│   └── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── doc/
│   └── intro.md
├── launch4j.xml
├── project.clj
├── raw/
│   ├── sgf/
│   │   ├── 147.sgf
│   │   └── 1988-06-20.sgf
│   └── splash.xcf
├── resources/
│   ├── convnet.cnet
│   ├── log4j.properties
│   ├── logging.properties
│   ├── mycertfile.pem
│   ├── ogs.truststore
│   └── supersimple.cnet
└── src/
    └── igoki/
        ├── camera.clj
        ├── core.clj
        ├── game.clj
        ├── inferrence.clj
        ├── integration/
        │   ├── ogs.clj
        │   └── robot.clj
        ├── litequil.clj
        ├── projector.clj
        ├── scratch/
        │   ├── scratch.clj
        │   └── training.clj
        ├── sgf.clj
        ├── simulated.clj
        ├── sound/
        │   ├── announce.clj
        │   └── sound.clj
        ├── ui/
        │   ├── calibration.clj
        │   ├── game.clj
        │   ├── main.clj
        │   ├── ogs.clj
        │   ├── projector.clj
        │   ├── robot.clj
        │   ├── tree.clj
        │   └── util.clj
        ├── util/
        │   └── crypto.clj
        └── util.clj
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (229K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 63,
    "preview": "# These are supported funding model platforms\n\nko_fi: cmdrdats\n"
  },
  {
    "path": ".gitignore",
    "chars": 190,
    "preview": "/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*.lo"
  },
  {
    "path": "LICENSE",
    "chars": 11218,
    "preview": "THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC\nLICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION"
  },
  {
    "path": "README.md",
    "chars": 12320,
    "preview": "# igoki\n\nBridge the gap between playing Go on a physical board and digitally.\n\nReasons for wanting to play on a physical"
  },
  {
    "path": "doc/intro.md",
    "chars": 106,
    "preview": "# Introduction to badukpro\n\nTODO: write [great documentation](http://jacobian.org/writing/what-to-write/)\n"
  },
  {
    "path": "launch4j.xml",
    "chars": 864,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<launch4jConfig>\n  <dontWrapJar>true</dontWrapJar>\n  <headerType>console</headerT"
  },
  {
    "path": "project.clj",
    "chars": 1059,
    "preview": "(defproject igoki \"0.8.0\"\n  :description \"Igoki, physical Go board/OGS interface\"\n  :url \"http://github.com/CmdrDats/igo"
  },
  {
    "path": "raw/sgf/147.sgf",
    "chars": 828,
    "preview": "(;\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]"
  },
  {
    "path": "raw/sgf/1988-06-20.sgf",
    "chars": 1538,
    "preview": "(;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[19"
  },
  {
    "path": "resources/log4j.properties",
    "chars": 406,
    "preview": "# Active appenders and base log level\nlog4j.rootLogger=ERROR, console\n\n# Console appender\nlog4j.appender.console=org.apa"
  },
  {
    "path": "resources/logging.properties",
    "chars": 47,
    "preview": " handers = org.slf4j.bridge.SLF4JBridgeHandler\n"
  },
  {
    "path": "resources/mycertfile.pem",
    "chars": 2744,
    "preview": "-----BEGIN CERTIFICATE-----\nMIIHvDCCBqSgAwIBAgIHB5t1bc/BmTANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE\nBhMCSUwxFjAUBgNVBAoTDVN0YXJ"
  },
  {
    "path": "src/igoki/camera.clj",
    "chars": 11832,
    "preview": "(ns igoki.camera\n  (:require\n    [igoki.util :as util]\n    [igoki.simulated :as sim]\n    [clojure.java.io :as io])\n  (:i"
  },
  {
    "path": "src/igoki/core.clj",
    "chars": 666,
    "preview": "(ns igoki.core\n  (:require\n    [igoki.ui.main :as ui.main]\n    [igoki.camera :as camera]\n    [igoki.game :as game]\n    ["
  },
  {
    "path": "src/igoki/game.clj",
    "chars": 9838,
    "preview": "(ns igoki.game\n  (:require\n    [igoki.util :as util]\n    [igoki.sgf :as sgf]\n    [igoki.inferrence :as inferrence]\n    ["
  },
  {
    "path": "src/igoki/inferrence.clj",
    "chars": 3649,
    "preview": "(ns igoki.inferrence\n  (:require\n    [igoki.util :as util]\n    [igoki.sgf :as sgf]))\n\n(defn simple-board-view [{:keys [b"
  },
  {
    "path": "src/igoki/integration/ogs.clj",
    "chars": 15255,
    "preview": "(ns igoki.integration.ogs\n  (:require\n    [clj-http.client :as client]\n    [clojure.tools.logging :as log]\n    [clojure."
  },
  {
    "path": "src/igoki/integration/robot.clj",
    "chars": 10146,
    "preview": "(ns igoki.integration.robot\n  (:require\n    [seesaw.core :as s]\n    [igoki.camera :as camera]\n    [igoki.integration.ogs"
  },
  {
    "path": "src/igoki/litequil.clj",
    "chars": 9966,
    "preview": "(ns igoki.litequil\n  (:require [seesaw.core :as s])\n  (:import\n    (javax.swing JPanel SwingUtilities JFrame)\n    (java."
  },
  {
    "path": "src/igoki/projector.clj",
    "chars": 10308,
    "preview": "(ns igoki.projector\n  (:require\n    [igoki.util :as util]\n    [igoki.game :as game]\n    [igoki.sgf :as sgf]\n    [igoki.l"
  },
  {
    "path": "src/igoki/scratch/scratch.clj",
    "chars": 22635,
    "preview": "(ns igoki.scratch.scratch\n  (:require\n    [igoki.util :as util :refer [-->]])\n  (:import\n    (org.opencv.objdetect Casca"
  },
  {
    "path": "src/igoki/scratch/training.clj",
    "chars": 5236,
    "preview": "(ns igoki.scratch.training\n  (:require\n    [clojure.java.io :as io]\n    [igoki.util :as util]\n    [clojure.edn :as edn])"
  },
  {
    "path": "src/igoki/sgf.clj",
    "chars": 12393,
    "preview": "(ns igoki.sgf\n  (:require\n    [igoki.util :as util]\n    [clojure.set :as set]))\n\n;; According to http://www.red-bean.com"
  },
  {
    "path": "src/igoki/simulated.clj",
    "chars": 11261,
    "preview": "(ns igoki.simulated\n  (:require\n    [igoki.litequil :as lq]\n    [igoki.util :as util]\n    [igoki.ui.util :as ui.util]\n  "
  },
  {
    "path": "src/igoki/sound/announce.clj",
    "chars": 5270,
    "preview": "(ns igoki.sound.announce\n  (:require\n    [clojure.string :as str]\n    [igoki.sgf :as sgf]\n    [clojure.java.io :as io]\n "
  },
  {
    "path": "src/igoki/sound/sound.clj",
    "chars": 1123,
    "preview": "(ns igoki.sound.sound\n  (:import\n    (javax.sound.sampled AudioSystem LineListener LineEvent LineEvent$Type)\n    (java.u"
  },
  {
    "path": "src/igoki/ui/calibration.clj",
    "chars": 5250,
    "preview": "(ns igoki.ui.calibration\n  (:require\n    [seesaw.core :as s]\n    [igoki.camera :as camera]\n    [igoki.litequil :as lq]\n "
  },
  {
    "path": "src/igoki/ui/game.clj",
    "chars": 11701,
    "preview": "(ns igoki.ui.game\n  (:require\n    [igoki.util :as util]\n    [igoki.litequil :as lq]\n    [igoki.sgf :as sgf]\n    [igoki.u"
  },
  {
    "path": "src/igoki/ui/main.clj",
    "chars": 4031,
    "preview": "(ns igoki.ui.main\n  (:require\n    [seesaw.core :as s]\n    [igoki.ui.game :as ui.game]\n    [igoki.ui.calibration :as cali"
  },
  {
    "path": "src/igoki/ui/ogs.clj",
    "chars": 8003,
    "preview": "(ns igoki.ui.ogs\n  (:require\n    [seesaw.core :as s]\n    [seesaw.mig :as sm]\n    [seesaw.color :as sc]\n    [igoki.integr"
  },
  {
    "path": "src/igoki/ui/projector.clj",
    "chars": 3257,
    "preview": "(ns igoki.ui.projector\n  (:require [seesaw.core :as s]\n            [igoki.projector :as projector]\n            [seesaw.m"
  },
  {
    "path": "src/igoki/ui/robot.clj",
    "chars": 13603,
    "preview": "(ns igoki.ui.robot\n  (:require\n    [seesaw.core :as s]\n    [seesaw.color :as sc]\n    [igoki.litequil :as lq]\n    [igoki."
  },
  {
    "path": "src/igoki/ui/tree.clj",
    "chars": 3098,
    "preview": "(ns igoki.ui.tree\n  (:require\n    [seesaw.core :as s]\n    [seesaw.border :as sb]\n    [clojure.pprint :as ppr]\n    [cloju"
  },
  {
    "path": "src/igoki/ui/util.clj",
    "chars": 1956,
    "preview": "(ns igoki.ui.util\n  (:require\n    [clojure.java.io :as io]\n    [seesaw.core :as s])\n  (:import\n    (javax.swing SwingUti"
  },
  {
    "path": "src/igoki/util/crypto.clj",
    "chars": 1293,
    "preview": "(ns igoki.util.crypto\n  (:import\n    [javax.crypto Cipher]\n    [javax.crypto.spec SecretKeySpec]\n    [java.security Mess"
  },
  {
    "path": "src/igoki/util.clj",
    "chars": 6412,
    "preview": "(ns igoki.util\n  (:require\n    [clojure.java.io :as io])\n  (:import\n    (org.opencv.core Mat Size CvType Point MatOfPoin"
  }
]

// ... and 4 more files (download for full content)

About this extraction

This page contains the full source code of the CmdrDats/igoki GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (214.4 KB), approximately 64.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!