diff --git a/.gitignore b/.gitignore index 86188e699..79e484680 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ leelaz src/main/resources/META-INF target leelaz_opencl_tuning +lizzie.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a6043895..7174719a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Active developmoent is on the next branch! Not master. Please clone next when contributing! +# Active development is on the next branch! Not master. Please clone next when contributing! TODO: add more specific contributing guidelines. In general... diff --git a/doc/plugin.md b/doc/plugin.md deleted file mode 100644 index e5053390c..000000000 --- a/doc/plugin.md +++ /dev/null @@ -1,115 +0,0 @@ -# Developing plug-ins for Lizzie - - -The Jar file for a plugin should look like this: - -./plugin -./plugin/Plugin.class -./META-INF -./META-INF/MANIFEST.MF -./...Your other classes - -### Start first step - -Create a "HelloWorld" directory and create a "plugin" directory in it, and create a new "Plugin.java" file in the "plugin" directory. - -And write the following - -```java -package plugin; - -import java.io.IOException; - -import IPlugin; - -/** - * Plugin - */ -public class Plugin implements IPlugin { - public static String name = "Hello World"; - public static String version = "0.0.0"; - - @Override - public String getName() { - return name; - } - - @Override - public String getVersion() { - return version; - } -} -``` - -This is the most basic framework of a plugin where ``` name ``` and ``` version ``` are used to generate a hash of this plugin. - -Let's edit Plugin.java to have a message box pop up when we press 'H'. - -```java - @Override - public void onKeyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_H) { - JOptionPane.showConfirmDialog(null, "Hello World!"); - } - } -``` - -And add the import statement at the beginning of the file - -```java -import java.swt.KeyEvent; -import javax.swing.JOptionPane; -``` - -Copy lizzie.jar to "HelloWorld" directory, and execute the command: - -``` -javac -classpath lizzie.jar ./plugin/Plugin.java -jar -cvf HelloWorld.jar ./plugin/Plugin.class -``` - -Copy the generated "HelloWorld.jar" to the "plugin" directory under the Lizzie directory. - -Start the lizzie and press 'H'! - -### More - -The plugin can freely call Lizzie's own classes and interfaces, so it can do a lot of functions. - -In the future, plugins may be able to enable and disable certain functions, so it is possible to do very complete functions with plug-ins. - -### Methods for plug-ins - -In ``` Lizzie.config ```: - -``` public boolean showBranch ``` - -``` public boolean showBestMoves ``` - -``` public void toggleShowBranch() ``` - -``` public void toggleShowBestMoves() ``` - -### Callback functions - -Basic callback function - -``` public abstract String getName() ``` Should be override, return plug-in 's name. -``` public abstract String getVersion() ``` Should be override, return plug-in 's version. - -``` public void onInit() throws IOException ``` Called when the plugin is loaded, the plugin is loaded after loading the configuration file. - -``` public void onMousePressed(MouseEvent e) ``` -``` public void onMouseReleased(MouseEvent e) ``` -``` public void onMouseMoved(MouseEvent e) ``` -``` public void onKeyPressed(KeyEvent e) ``` -``` public void onKeyReleased(KeyEvent e) ``` Like its name, it is called in the corresponding event. - -``` public boolean onDraw(Graphics2D g) ``` Called when drawing, the return value represents whether to submit the drawing result. - -``` public void onShutdown() throws IOException ``` Called when the program is shut down. - - -Special callback function - -``` public void onSgfLoaded() ``` diff --git a/doc/theme.md b/doc/theme.md deleted file mode 100644 index 20f006348..000000000 --- a/doc/theme.md +++ /dev/null @@ -1,19 +0,0 @@ -# Theme -The theme's function is to make Lizzie's display richer, so simply change the assets file can not do things. - -The way to load a theme is to copy the theme's Jar file to the theme directory, and then set the theme's class name in the configuration file. - -Any class that implements the ITheme interface can be used as a theme class. - -Use ```javac -classpath ...``` to ensure that the theme's source files can be imported into the ITheme class. - -A theme class needs to implement the following methods: -```java -public Image getBlackStone(int[] position) throws IOException; - -public Image getWhiteStone(int[] position) throws IOException; - - public Image getBoard() throws IOException; - - public Image getBackground() throws IOException; -``` diff --git a/pom.xml b/pom.xml index 6f02aa141..699e4d2f2 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,9 @@ 1.8 1.8 + + -Xlint:all + @@ -52,6 +55,15 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 2.9 + + + false + + @@ -63,5 +75,19 @@ 20180130 + + + junit + junit + 4.11 + test + + + + + com.jhlabs + filters + 2.0.235 + diff --git a/src/main/java/com/jhlabs/image/AbstractBufferedImageOp.java b/src/main/java/com/jhlabs/image/AbstractBufferedImageOp.java deleted file mode 100644 index dc1257fb3..000000000 --- a/src/main/java/com/jhlabs/image/AbstractBufferedImageOp.java +++ /dev/null @@ -1,59 +0,0 @@ -/* -** Copyright 2005 Huxtable.com. All rights reserved. -*/ - -package com.jhlabs.image; - -import java.awt.*; -import java.awt.geom.*; -import java.awt.image.*; - -/** - * A convenience class which implements those methods of BufferedImageOp which are rarely changed. - */ -public abstract class AbstractBufferedImageOp implements BufferedImageOp { - - public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) { - if ( dstCM == null ) - dstCM = src.getColorModel(); - return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dstCM.isAlphaPremultiplied(), null); - } - - public Rectangle2D getBounds2D( BufferedImage src ) { - return new Rectangle(0, 0, src.getWidth(), src.getHeight()); - } - - public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) { - if ( dstPt == null ) - dstPt = new Point2D.Double(); - dstPt.setLocation( srcPt.getX(), srcPt.getY() ); - return dstPt; - } - - public RenderingHints getRenderingHints() { - return null; - } - - /** - * A convenience method for getting ARGB pixels from an image. This tries to avoid the performance - * penalty of BufferedImage.getRGB unmanaging the image. - */ - public int[] getRGB( BufferedImage image, int x, int y, int width, int height, int[] pixels ) { - int type = image.getType(); - if ( type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB ) - return (int [])image.getRaster().getDataElements( x, y, width, height, pixels ); - return image.getRGB( x, y, width, height, pixels, 0, width ); - } - - /** - * A convenience method for setting ARGB pixels in an image. This tries to avoid the performance - * penalty of BufferedImage.setRGB unmanaging the image. - */ - public void setRGB( BufferedImage image, int x, int y, int width, int height, int[] pixels ) { - int type = image.getType(); - if ( type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB ) - image.getRaster().setDataElements( x, y, width, height, pixels ); - else - image.setRGB( x, y, width, height, pixels, 0, width ); - } -} \ No newline at end of file diff --git a/src/main/java/com/jhlabs/image/ConvolveFilter.java b/src/main/java/com/jhlabs/image/ConvolveFilter.java deleted file mode 100644 index 46a947dad..000000000 --- a/src/main/java/com/jhlabs/image/ConvolveFilter.java +++ /dev/null @@ -1,285 +0,0 @@ -/* -** Copyright 2005 Huxtable.com. All rights reserved. -*/ - -package com.jhlabs.image; - -import java.awt.*; -import java.awt.image.*; -import java.awt.geom.*; - -/** - * A filter which applies a convolution kernel to an image. - * @author Jerry Huxtable - */ -public class ConvolveFilter extends AbstractBufferedImageOp { - - static final long serialVersionUID = 2239251672685254626L; - - public static int ZERO_EDGES = 0; - public static int CLAMP_EDGES = 1; - public static int WRAP_EDGES = 2; - - protected Kernel kernel = null; - public boolean alpha = true; - private int edgeAction = CLAMP_EDGES; - - /** - * Construct a filter with a null kernel. This is only useful if you're going to change the kernel later on. - */ - public ConvolveFilter() { - this(new float[9]); - } - - /** - * Construct a filter with the given 3x3 kernel. - * @param matrix an array of 9 floats containing the kernel - */ - public ConvolveFilter(float[] matrix) { - this(new Kernel(3, 3, matrix)); - } - - /** - * Construct a filter with the given kernel. - * @param rows the number of rows in the kernel - * @param cols the number of columns in the kernel - * @param matrix an array of rows*cols floats containing the kernel - */ - public ConvolveFilter(int rows, int cols, float[] matrix) { - this(new Kernel(cols, rows, matrix)); - } - - /** - * Construct a filter with the given 3x3 kernel. - * @param matrix an array of 9 floats containing the kernel - */ - public ConvolveFilter(Kernel kernel) { - this.kernel = kernel; - } - - public void setKernel(Kernel kernel) { - this.kernel = kernel; - } - - public Kernel getKernel() { - return kernel; - } - - public void setEdgeAction(int edgeAction) { - this.edgeAction = edgeAction; - } - - public int getEdgeAction() { - return edgeAction; - } - - public BufferedImage filter( BufferedImage src, BufferedImage dst ) { - int width = src.getWidth(); - int height = src.getHeight(); - - if ( dst == null ) - dst = createCompatibleDestImage( src, null ); - - int[] inPixels = new int[width*height]; - int[] outPixels = new int[width*height]; - getRGB( src, 0, 0, width, height, inPixels ); - - convolve(kernel, inPixels, outPixels, width, height, alpha, edgeAction); - - setRGB( dst, 0, 0, width, height, outPixels ); - return dst; - } - - public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) { - if ( dstCM == null ) - dstCM = src.getColorModel(); - return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dstCM.isAlphaPremultiplied(), null); - } - - public Rectangle2D getBounds2D( BufferedImage src ) { - return new Rectangle(0, 0, src.getWidth(), src.getHeight()); - } - - public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) { - if ( dstPt == null ) - dstPt = new Point2D.Double(); - dstPt.setLocation( srcPt.getX(), srcPt.getY() ); - return dstPt; - } - - public RenderingHints getRenderingHints() { - return null; - } - - public static void convolve(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, int edgeAction) { - convolve(kernel, inPixels, outPixels, width, height, true, edgeAction); - } - - public static void convolve(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { - if (kernel.getHeight() == 1) - convolveH(kernel, inPixels, outPixels, width, height, alpha, edgeAction); - else if (kernel.getWidth() == 1) - convolveV(kernel, inPixels, outPixels, width, height, alpha, edgeAction); - else - convolveHV(kernel, inPixels, outPixels, width, height, alpha, edgeAction); - } - - /** - * Convolve with a 2D kernel - */ - public static void convolveHV(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { - int index = 0; - float[] matrix = kernel.getKernelData( null ); - int rows = kernel.getHeight(); - int cols = kernel.getWidth(); - int rows2 = rows/2; - int cols2 = cols/2; - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - float r = 0, g = 0, b = 0, a = 0; - - for (int row = -rows2; row <= rows2; row++) { - int iy = y+row; - int ioffset; - if (0 <= iy && iy < height) - ioffset = iy*width; - else if ( edgeAction == CLAMP_EDGES ) - ioffset = y*width; - else if ( edgeAction == WRAP_EDGES ) - ioffset = ((iy+height) % height) * width; - else - continue; - int moffset = cols*(row+rows2)+cols2; - for (int col = -cols2; col <= cols2; col++) { - float f = matrix[moffset+col]; - - if (f != 0) { - int ix = x+col; - if (!(0 <= ix && ix < width)) { - if ( edgeAction == CLAMP_EDGES ) - ix = x; - else if ( edgeAction == WRAP_EDGES ) - ix = (x+width) % width; - else - continue; - } - int rgb = inPixels[ioffset+ix]; - a += f * ((rgb >> 24) & 0xff); - r += f * ((rgb >> 16) & 0xff); - g += f * ((rgb >> 8) & 0xff); - b += f * (rgb & 0xff); - } - } - } - int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff; - int ir = PixelUtils.clamp((int)(r+0.5)); - int ig = PixelUtils.clamp((int)(g+0.5)); - int ib = PixelUtils.clamp((int)(b+0.5)); - outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib; - } - } - } - - /** - * Convolve with a kernel consisting of one row - */ - public static void convolveH(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { - int index = 0; - float[] matrix = kernel.getKernelData( null ); - int cols = kernel.getWidth(); - int cols2 = cols/2; - - for (int y = 0; y < height; y++) { - int ioffset = y*width; - for (int x = 0; x < width; x++) { - float r = 0, g = 0, b = 0, a = 0; - int moffset = cols2; - for (int col = -cols2; col <= cols2; col++) { - float f = matrix[moffset+col]; - - if (f != 0) { - int ix = x+col; - if ( ix < 0 ) { - if ( edgeAction == CLAMP_EDGES ) - ix = 0; - else if ( edgeAction == WRAP_EDGES ) - ix = (x+width) % width; - } else if ( ix >= width) { - if ( edgeAction == CLAMP_EDGES ) - ix = width-1; - else if ( edgeAction == WRAP_EDGES ) - ix = (x+width) % width; - } - int rgb = inPixels[ioffset+ix]; - a += f * ((rgb >> 24) & 0xff); - r += f * ((rgb >> 16) & 0xff); - g += f * ((rgb >> 8) & 0xff); - b += f * (rgb & 0xff); - } - } - int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff; - int ir = PixelUtils.clamp((int)(r+0.5)); - int ig = PixelUtils.clamp((int)(g+0.5)); - int ib = PixelUtils.clamp((int)(b+0.5)); - outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib; - } - } - } - - /** - * Convolve with a kernel consisting of one column - */ - public static void convolveV(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { - int index = 0; - float[] matrix = kernel.getKernelData( null ); - int rows = kernel.getHeight(); - int rows2 = rows/2; - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - float r = 0, g = 0, b = 0, a = 0; - - for (int row = -rows2; row <= rows2; row++) { - int iy = y+row; - int ioffset; - if ( iy < 0 ) { - if ( edgeAction == CLAMP_EDGES ) - ioffset = 0; - else if ( edgeAction == WRAP_EDGES ) - ioffset = ((y+height) % height)*width; - else - ioffset = iy*width; - } else if ( iy >= height) { - if ( edgeAction == CLAMP_EDGES ) - ioffset = (height-1)*width; - else if ( edgeAction == WRAP_EDGES ) - ioffset = ((y+height) % height)*width; - else - ioffset = iy*width; - } else - ioffset = iy*width; - - float f = matrix[row+rows2]; - - if (f != 0) { - int rgb = inPixels[ioffset+x]; - a += f * ((rgb >> 24) & 0xff); - r += f * ((rgb >> 16) & 0xff); - g += f * ((rgb >> 8) & 0xff); - b += f * (rgb & 0xff); - } - } - int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff; - int ir = PixelUtils.clamp((int)(r+0.5)); - int ig = PixelUtils.clamp((int)(g+0.5)); - int ib = PixelUtils.clamp((int)(b+0.5)); - outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib; - } - } - } - - public String toString() { - return "Blur/Convolve..."; - } -} \ No newline at end of file diff --git a/src/main/java/com/jhlabs/image/GaussianFilter.java b/src/main/java/com/jhlabs/image/GaussianFilter.java deleted file mode 100644 index fb04e6f99..000000000 --- a/src/main/java/com/jhlabs/image/GaussianFilter.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.jhlabs.image; -import java.awt.image.*; - -/** - * A filter which applies Gaussian blur to an image. This is a subclass of ConvolveFilter - * which simply creates a kernel with a Gaussian distribution for blurring. - * @author Jerry Huxtable - */ -public class GaussianFilter extends ConvolveFilter { - - static final long serialVersionUID = 5377089073023183684L; - - private float radius; - private Kernel kernel; - - /** - * Construct a Gaussian filter - */ - public GaussianFilter() { - this(2); - } - - /** - * Construct a Gaussian filter - * @param radius blur radius in pixels - */ - public GaussianFilter(float radius) { - setRadius(radius); - } - - /** - * Set the radius of the kernel, and hence the amount of blur. The bigger the radius, the longer this filter will take. - * @param radius the radius of the blur in pixels. - */ - public void setRadius(float radius) { - this.radius = radius; - kernel = makeKernel(radius); - } - - /** - * Get the radius of the kernel. - * @return the radius - */ - public float getRadius() { - return radius; - } - - public BufferedImage filter( BufferedImage src, BufferedImage dst ) { - int width = src.getWidth(); - int height = src.getHeight(); - - if ( dst == null ) - dst = createCompatibleDestImage( src, null ); - - int[] inPixels = new int[width*height]; - int[] outPixels = new int[width*height]; - src.getRGB( 0, 0, width, height, inPixels, 0, width ); - - convolveAndTranspose(kernel, inPixels, outPixels, width, height, alpha, CLAMP_EDGES); - convolveAndTranspose(kernel, outPixels, inPixels, height, width, alpha, CLAMP_EDGES); - - dst.setRGB( 0, 0, width, height, inPixels, 0, width ); - return dst; - } - - public static void convolveAndTranspose(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { - float[] matrix = kernel.getKernelData( null ); - int cols = kernel.getWidth(); - int cols2 = cols/2; - - for (int y = 0; y < height; y++) { - int index = y; - int ioffset = y*width; - for (int x = 0; x < width; x++) { - float r = 0, g = 0, b = 0, a = 0; - int moffset = cols2; - for (int col = -cols2; col <= cols2; col++) { - float f = matrix[moffset+col]; - - if (f != 0) { - int ix = x+col; - if ( ix < 0 ) { - if ( edgeAction == CLAMP_EDGES ) - ix = 0; - else if ( edgeAction == WRAP_EDGES ) - ix = (x+width) % width; - } else if ( ix >= width) { - if ( edgeAction == CLAMP_EDGES ) - ix = width-1; - else if ( edgeAction == WRAP_EDGES ) - ix = (x+width) % width; - } - int rgb = inPixels[ioffset+ix]; - a += f * ((rgb >> 24) & 0xff); - r += f * ((rgb >> 16) & 0xff); - g += f * ((rgb >> 8) & 0xff); - b += f * (rgb & 0xff); - } - } - int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff; - int ir = PixelUtils.clamp((int)(r+0.5)); - int ig = PixelUtils.clamp((int)(g+0.5)); - int ib = PixelUtils.clamp((int)(b+0.5)); - outPixels[index] = (ia << 24) | (ir << 16) | (ig << 8) | ib; - index += height; - } - } - } - - /** - * Make a Gaussian blur kernel. - */ - public static Kernel makeKernel(float radius) { - int r = (int)Math.ceil(radius); - int rows = r*2+1; - float[] matrix = new float[rows]; - float sigma = radius/3; - float sigma22 = 2*sigma*sigma; - float sigmaPi2 = 2*ImageMath.PI*sigma; - float sqrtSigmaPi2 = (float)Math.sqrt(sigmaPi2); - float radius2 = radius*radius; - float total = 0; - int index = 0; - for (int row = -r; row <= r; row++) { - float distance = row*row; - if (distance > radius2) - matrix[index] = 0; - else - matrix[index] = (float)Math.exp(-(distance)/sigma22) / sqrtSigmaPi2; - total += matrix[index]; - index++; - } - for (int i = 0; i < rows; i++) - matrix[i] /= total; - - return new Kernel(rows, 1, matrix); - } - - public String toString() { - return "Blur/Gaussian Blur..."; - } -} \ No newline at end of file diff --git a/src/main/java/com/jhlabs/image/ImageMath.java b/src/main/java/com/jhlabs/image/ImageMath.java deleted file mode 100644 index 1e95374d0..000000000 --- a/src/main/java/com/jhlabs/image/ImageMath.java +++ /dev/null @@ -1,602 +0,0 @@ -/* -** Copyright 2005 Huxtable.com. All rights reserved. -*/ - -package com.jhlabs.image; - -/** - * A class containing static math methods useful for image processing. - */ -public class ImageMath { - - public final static float PI = (float)Math.PI; - public final static float HALF_PI = (float)Math.PI/2.0f; - public final static float QUARTER_PI = (float)Math.PI/4.0f; - public final static float TWO_PI = (float)Math.PI*2.0f; - - /** - * Apply a bias to a number in the unit interval, moving numbers towards 0 or 1 - * according to the bias parameter. - * @param a the number to bias - * @param b the bias parameter. 0.5 means no change, smaller values bias towards 0, larger towards 1. - * @return the output value - */ - public static float bias(float a, float b) { -// return (float)Math.pow(a, Math.log(b) / Math.log(0.5)); - return a/((1.0f/b-2)*(1.0f-a)+1); - } - - /** - * A variant of the gamma function. - * @param a the number to apply gain to - * @param b the gain parameter. 0.5 means no change, smaller values reduce gain, larger values increase gain. - * @return the output value - */ - public static float gain(float a, float b) { -/* - float p = (float)Math.log(1.0 - b) / (float)Math.log(0.5); - - if (a < .001) - return 0.0f; - else if (a > .999) - return 1.0f; - if (a < 0.5) - return (float)Math.pow(2 * a, p) / 2; - else - return 1.0f - (float)Math.pow(2 * (1. - a), p) / 2; -*/ - float c = (1.0f/b-2.0f) * (1.0f-2.0f*a); - if (a < 0.5) - return a/(c+1.0f); - else - return (c-a)/(c-1.0f); - } - - /** - * The step function. Returns 0 below a threshold, 1 above. - * @param a the threshold position - * @param x the input parameter - * @return the output value - 0 or 1 - */ - public static float step(float a, float x) { - return (x < a) ? 0.0f : 1.0f; - } - - /** - * The pulse function. Returns 1 between two thresholds, 0 outside. - * @param a the lower threshold position - * @param b the upper threshold position - * @param x the input parameter - * @return the output value - 0 or 1 - */ - public static float pulse(float a, float b, float x) { - return (x < a || x >= b) ? 0.0f : 1.0f; - } - - /** - * A smoothed pulse function. A cubic function is used to smooth the step between two thresholds. - * @param a1 the lower threshold position for the start of the pulse - * @param a2 the upper threshold position for the start of the pulse - * @param b1 the lower threshold position for the end of the pulse - * @param b2 the upper threshold position for the end of the pulse - * @param x the input parameter - * @return the output value - */ - public static float smoothPulse(float a1, float a2, float b1, float b2, float x) { - if (x < a1 || x >= b2) - return 0; - if (x >= a2) { - if (x < b1) - return 1.0f; - x = (x - b1) / (b2 - b1); - return 1.0f - (x*x * (3.0f - 2.0f*x)); - } - x = (x - a1) / (a2 - a1); - return x*x * (3.0f - 2.0f*x); - } - - /** - * A smoothed step function. A cubic function is used to smooth the step between two thresholds. - * @param a the lower threshold position - * @param b the upper threshold position - * @param x the input parameter - * @return the output value - */ - public static float smoothStep(float a, float b, float x) { - if (x < a) - return 0; - if (x >= b) - return 1; - x = (x - a) / (b - a); - return x*x * (3 - 2*x); - } - - /** - * A "circle up" function. Returns y on a unit circle given 1-x. Useful for forming bevels. - * @param x the input parameter in the range 0..1 - * @return the output value - */ - public static float circleUp(float x) { - x = 1-x; - return (float)Math.sqrt(1-x*x); - } - - /** - * A "circle down" function. Returns 1-y on a unit circle given x. Useful for forming bevels. - * @param x the input parameter in the range 0..1 - * @return the output value - */ - public static float circleDown(float x) { - return 1.0f-(float)Math.sqrt(1-x*x); - } - - /** - * Clamp a value to an interval. - * @param a the lower clamp threshold - * @param b the upper clamp threshold - * @param x the input parameter - * @return the clamped value - */ - public static float clamp(float x, float a, float b) { - return (x < a) ? a : (x > b) ? b : x; - } - - /** - * Clamp a value to an interval. - * @param a the lower clamp threshold - * @param b the upper clamp threshold - * @param x the input parameter - * @return the clamped value - */ - public static int clamp(int x, int a, int b) { - return (x < a) ? a : (x > b) ? b : x; - } - - /** - * Return a mod b. This differs from the % operator with respect to negative numbers. - * @param a the dividend - * @param b the divisor - * @return a mod b - */ - public static double mod(double a, double b) { - int n = (int)(a/b); - - a -= n*b; - if (a < 0) - return a + b; - return a; - } - - /** - * Return a mod b. This differs from the % operator with respect to negative numbers. - * @param a the dividend - * @param b the divisor - * @return a mod b - */ - public static float mod(float a, float b) { - int n = (int)(a/b); - - a -= n*b; - if (a < 0) - return a + b; - return a; - } - - /** - * Return a mod b. This differs from the % operator with respect to negative numbers. - * @param a the dividend - * @param b the divisor - * @return a mod b - */ - public static int mod(int a, int b) { - int n = a/b; - - a -= n*b; - if (a < 0) - return a + b; - return a; - } - - /** - * The triangle function. Returns a repeating triangle shape in the range 0..1 with wavelength 1.0 - * @param x the input parameter - * @return the output value - */ - public static float triangle(float x) { - float r = mod(x, 1.0f); - return 2.0f*(r < 0.5 ? r : 1-r); - } - - /** - * Linear interpolation. - * @param t the interpolation parameter - * @param a the lower interpolation range - * @param b the upper interpolation range - * @return the interpolated value - */ - public static float lerp(float t, float a, float b) { - return a + t * (b - a); - } - - /** - * Linear interpolation. - * @param t the interpolation parameter - * @param a the lower interpolation range - * @param b the upper interpolation range - * @return the interpolated value - */ - public static int lerp(float t, int a, int b) { - return (int)(a + t * (b - a)); - } - - /** - * Linear interpolation of ARGB values. - * @param t the interpolation parameter - * @param rgb1 the lower interpolation range - * @param rgb2 the upper interpolation range - * @return the interpolated value - */ - public static int mixColors(float t, int rgb1, int rgb2) { - int a1 = (rgb1 >> 24) & 0xff; - int r1 = (rgb1 >> 16) & 0xff; - int g1 = (rgb1 >> 8) & 0xff; - int b1 = rgb1 & 0xff; - int a2 = (rgb2 >> 24) & 0xff; - int r2 = (rgb2 >> 16) & 0xff; - int g2 = (rgb2 >> 8) & 0xff; - int b2 = rgb2 & 0xff; - a1 = lerp(t, a1, a2); - r1 = lerp(t, r1, r2); - g1 = lerp(t, g1, g2); - b1 = lerp(t, b1, b2); - return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1; - } - - /** - * Bilinear interpolation of ARGB values. - * @param x the X interpolation parameter 0..1 - * @param y the y interpolation parameter 0..1 - * @param rgb array of four ARGB values in the order NW, NE, SW, SE - * @return the interpolated value - */ - public static int bilinearInterpolate(float x, float y, int[] p) { - float m0, m1; - int a0 = (p[0] >> 24) & 0xff; - int r0 = (p[0] >> 16) & 0xff; - int g0 = (p[0] >> 8) & 0xff; - int b0 = p[0] & 0xff; - int a1 = (p[1] >> 24) & 0xff; - int r1 = (p[1] >> 16) & 0xff; - int g1 = (p[1] >> 8) & 0xff; - int b1 = p[1] & 0xff; - int a2 = (p[2] >> 24) & 0xff; - int r2 = (p[2] >> 16) & 0xff; - int g2 = (p[2] >> 8) & 0xff; - int b2 = p[2] & 0xff; - int a3 = (p[3] >> 24) & 0xff; - int r3 = (p[3] >> 16) & 0xff; - int g3 = (p[3] >> 8) & 0xff; - int b3 = p[3] & 0xff; - - float cx = 1.0f-x; - float cy = 1.0f-y; - - m0 = cx * a0 + x * a1; - m1 = cx * a2 + x * a3; - int a = (int)(cy * m0 + y * m1); - - m0 = cx * r0 + x * r1; - m1 = cx * r2 + x * r3; - int r = (int)(cy * m0 + y * m1); - - m0 = cx * g0 + x * g1; - m1 = cx * g2 + x * g3; - int g = (int)(cy * m0 + y * m1); - - m0 = cx * b0 + x * b1; - m1 = cx * b2 + x * b3; - int b = (int)(cy * m0 + y * m1); - - return (a << 24) | (r << 16) | (g << 8) | b; - } - - /** - * Return the NTSC gray level of an RGB value. - * @param rgb1 the input pixel - * @return the gray level (0-255) - */ - public static int brightnessNTSC(int rgb) { - int r = (rgb >> 16) & 0xff; - int g = (rgb >> 8) & 0xff; - int b = rgb & 0xff; - return (int)(r*0.299f + g*0.587f + b*0.114f); - } - - // Catmull-Rom splines - private final static float m00 = -0.5f; - private final static float m01 = 1.5f; - private final static float m02 = -1.5f; - private final static float m03 = 0.5f; - private final static float m10 = 1.0f; - private final static float m11 = -2.5f; - private final static float m12 = 2.0f; - private final static float m13 = -0.5f; - private final static float m20 = -0.5f; - private final static float m21 = 0.0f; - private final static float m22 = 0.5f; - private final static float m23 = 0.0f; - private final static float m30 = 0.0f; - private final static float m31 = 1.0f; - private final static float m32 = 0.0f; - private final static float m33 = 0.0f; - - /** - * Compute a Catmull-Rom spline. - * @param x the input parameter - * @param numKnots the number of knots in the spline - * @param knots the array of knots - * @return the spline value - */ - public static float spline(float x, int numKnots, float[] knots) { - int span; - int numSpans = numKnots - 3; - float k0, k1, k2, k3; - float c0, c1, c2, c3; - - if (numSpans < 1) - throw new IllegalArgumentException("Too few knots in spline"); - - x = clamp(x, 0, 1) * numSpans; - span = (int)x; - if (span > numKnots-4) - span = numKnots-4; - x -= span; - - k0 = knots[span]; - k1 = knots[span+1]; - k2 = knots[span+2]; - k3 = knots[span+3]; - - c3 = m00*k0 + m01*k1 + m02*k2 + m03*k3; - c2 = m10*k0 + m11*k1 + m12*k2 + m13*k3; - c1 = m20*k0 + m21*k1 + m22*k2 + m23*k3; - c0 = m30*k0 + m31*k1 + m32*k2 + m33*k3; - - return ((c3*x + c2)*x + c1)*x + c0; - } - - /** - * Compute a Catmull-Rom spline, but with variable knot spacing. - * @param x the input parameter - * @param numKnots the number of knots in the spline - * @param xknots the array of knot x values - * @param yknots the array of knot y values - * @return the spline value - */ - public static float spline(float x, int numKnots, int[] xknots, int[] yknots) { - int span; - int numSpans = numKnots - 3; - float k0, k1, k2, k3; - float c0, c1, c2, c3; - - if (numSpans < 1) - throw new IllegalArgumentException("Too few knots in spline"); - - for (span = 0; span < numSpans; span++) - if (xknots[span+1] > x) - break; - if (span > numKnots-3) - span = numKnots-3; - float t = (float)(x-xknots[span]) / (xknots[span+1]-xknots[span]); - span--; - if (span < 0) { - span = 0; - t = 0; - } - - k0 = yknots[span]; - k1 = yknots[span+1]; - k2 = yknots[span+2]; - k3 = yknots[span+3]; - - c3 = m00*k0 + m01*k1 + m02*k2 + m03*k3; - c2 = m10*k0 + m11*k1 + m12*k2 + m13*k3; - c1 = m20*k0 + m21*k1 + m22*k2 + m23*k3; - c0 = m30*k0 + m31*k1 + m32*k2 + m33*k3; - - return ((c3*t + c2)*t + c1)*t + c0; - } - - /** - * Compute a Catmull-Rom spline for RGB values. - * @param x the input parameter - * @param numKnots the number of knots in the spline - * @param knots the array of knots - * @return the spline value - */ - public static int colorSpline(float x, int numKnots, int[] knots) { - int span; - int numSpans = numKnots - 3; - float k0, k1, k2, k3; - float c0, c1, c2, c3; - - if (numSpans < 1) - throw new IllegalArgumentException("Too few knots in spline"); - - x = clamp(x, 0, 1) * numSpans; - span = (int)x; - if (span > numKnots-4) - span = numKnots-4; - x -= span; - - int v = 0; - for (int i = 0; i < 4; i++) { - int shift = i * 8; - - k0 = (knots[span] >> shift) & 0xff; - k1 = (knots[span+1] >> shift) & 0xff; - k2 = (knots[span+2] >> shift) & 0xff; - k3 = (knots[span+3] >> shift) & 0xff; - - c3 = m00*k0 + m01*k1 + m02*k2 + m03*k3; - c2 = m10*k0 + m11*k1 + m12*k2 + m13*k3; - c1 = m20*k0 + m21*k1 + m22*k2 + m23*k3; - c0 = m30*k0 + m31*k1 + m32*k2 + m33*k3; - int n = (int)(((c3*x + c2)*x + c1)*x + c0); - if (n < 0) - n = 0; - else if (n > 255) - n = 255; - v |= n << shift; - } - - return v; - } - - /** - * Compute a Catmull-Rom spline for RGB values, but with variable knot spacing. - * @param x the input parameter - * @param numKnots the number of knots in the spline - * @param xknots the array of knot x values - * @param yknots the array of knot y values - * @return the spline value - */ - public static int colorSpline(int x, int numKnots, int[] xknots, int[] yknots) { - int span; - int numSpans = numKnots - 3; - float k0, k1, k2, k3; - float c0, c1, c2, c3; - - if (numSpans < 1) - throw new IllegalArgumentException("Too few knots in spline"); - - for (span = 0; span < numSpans; span++) - if (xknots[span+1] > x) - break; - if (span > numKnots-3) - span = numKnots-3; - float t = (float)(x-xknots[span]) / (xknots[span+1]-xknots[span]); - span--; - if (span < 0) { - span = 0; - t = 0; - } - - int v = 0; - for (int i = 0; i < 4; i++) { - int shift = i * 8; - - k0 = (yknots[span] >> shift) & 0xff; - k1 = (yknots[span+1] >> shift) & 0xff; - k2 = (yknots[span+2] >> shift) & 0xff; - k3 = (yknots[span+3] >> shift) & 0xff; - - c3 = m00*k0 + m01*k1 + m02*k2 + m03*k3; - c2 = m10*k0 + m11*k1 + m12*k2 + m13*k3; - c1 = m20*k0 + m21*k1 + m22*k2 + m23*k3; - c0 = m30*k0 + m31*k1 + m32*k2 + m33*k3; - int n = (int)(((c3*t + c2)*t + c1)*t + c0); - if (n < 0) - n = 0; - else if (n > 255) - n = 255; - v |= n << shift; - } - - return v; - } - - /** - * An implementation of Fant's resampling algorithm. - * @param source the source pixels - * @param dest the destination pixels - * @param length the length of the scanline to resample - * @param offset the start offset into the arrays - * @param stride the offset between pixels in consecutive rows - * @param out an array of output positions for each pixel - */ - public static void resample(int[] source, int[] dest, int length, int offset, int stride, float[] out) { - int i, j; - float intensity; - float sizfac; - float inSegment; - float outSegment; - int a, r, g, b, nextA, nextR, nextG, nextB; - float aSum, rSum, gSum, bSum; - float[] in; - int srcIndex = offset; - int destIndex = offset; - int lastIndex = source.length; - int rgb; - - in = new float[length+1]; - i = 0; - for (j = 0; j < length; j++) { - while (out[i+1] < j) - i++; - in[j] = i + (float) (j - out[i]) / (out[i + 1] - out[i]); - } - in[length] = length; - - inSegment = 1.0f; - outSegment = in[1]; - sizfac = outSegment; - aSum = rSum = gSum = bSum = 0.0f; - rgb = source[srcIndex]; - a = (rgb >> 24) & 0xff; - r = (rgb >> 16) & 0xff; - g = (rgb >> 8) & 0xff; - b = rgb & 0xff; - srcIndex += stride; - rgb = source[srcIndex]; - nextA = (rgb >> 24) & 0xff; - nextR = (rgb >> 16) & 0xff; - nextG = (rgb >> 8) & 0xff; - nextB = rgb & 0xff; - srcIndex += stride; - i = 1; - - while (i < length) { - float aIntensity = inSegment * a + (1.0f - inSegment) * nextA; - float rIntensity = inSegment * r + (1.0f - inSegment) * nextR; - float gIntensity = inSegment * g + (1.0f - inSegment) * nextG; - float bIntensity = inSegment * b + (1.0f - inSegment) * nextB; - if (inSegment < outSegment) { - aSum += (aIntensity * inSegment); - rSum += (rIntensity * inSegment); - gSum += (gIntensity * inSegment); - bSum += (bIntensity * inSegment); - outSegment -= inSegment; - inSegment = 1.0f; - a = nextA; - r = nextR; - g = nextG; - b = nextB; - if (srcIndex < lastIndex) - rgb = source[srcIndex]; - nextA = (rgb >> 24) & 0xff; - nextR = (rgb >> 16) & 0xff; - nextG = (rgb >> 8) & 0xff; - nextB = rgb & 0xff; - srcIndex += stride; - } else { - aSum += (aIntensity * outSegment); - rSum += (rIntensity * outSegment); - gSum += (gIntensity * outSegment); - bSum += (bIntensity * outSegment); - dest[destIndex] = - ((int)Math.min(aSum/sizfac, 255) << 24) | - ((int)Math.min(rSum/sizfac, 255) << 16) | - ((int)Math.min(gSum/sizfac, 255) << 8) | - (int)Math.min(bSum/sizfac, 255); - destIndex += stride; - rSum = gSum = bSum = 0.0f; - inSegment -= outSegment; - outSegment = in[i+1] - in[i]; - sizfac = outSegment; - i++; - } - } - } - -} \ No newline at end of file diff --git a/src/main/java/com/jhlabs/image/PixelUtils.java b/src/main/java/com/jhlabs/image/PixelUtils.java deleted file mode 100644 index daf6f9d98..000000000 --- a/src/main/java/com/jhlabs/image/PixelUtils.java +++ /dev/null @@ -1,211 +0,0 @@ -/* -** Copyright 2005 Huxtable.com. All rights reserved. -*/ - -package com.jhlabs.image; - -import java.util.*; -import java.awt.Color; - -/** - * Some more useful math functions for image processing. - * These are becoming obsolete as we move to Java2D. Use MiscComposite instead. - */ -public class PixelUtils { - - public final static int REPLACE = 0; - public final static int NORMAL = 1; - public final static int MIN = 2; - public final static int MAX = 3; - public final static int ADD = 4; - public final static int SUBTRACT = 5; - public final static int DIFFERENCE = 6; - public final static int MULTIPLY = 7; - public final static int HUE = 8; - public final static int SATURATION = 9; - public final static int VALUE = 10; - public final static int COLOR = 11; - public final static int SCREEN = 12; - public final static int AVERAGE = 13; - public final static int OVERLAY = 14; - public final static int CLEAR = 15; - public final static int EXCHANGE = 16; - public final static int DISSOLVE = 17; - public final static int DST_IN = 18; - public final static int ALPHA = 19; - public final static int ALPHA_TO_GRAY = 20; - - private static Random randomGenerator = new Random(); - - /** - * Clamp a value to the range 0..255 - */ - public static int clamp(int c) { - if (c < 0) - return 0; - if (c > 255) - return 255; - return c; - } - - public static int interpolate(int v1, int v2, float f) { - return clamp((int)(v1+f*(v2-v1))); - } - - public static int brightness(int rgb) { - int r = (rgb >> 16) & 0xff; - int g = (rgb >> 8) & 0xff; - int b = rgb & 0xff; - return (r+g+b)/3; - } - - public static boolean nearColors(int rgb1, int rgb2, int tolerance) { - int r1 = (rgb1 >> 16) & 0xff; - int g1 = (rgb1 >> 8) & 0xff; - int b1 = rgb1 & 0xff; - int r2 = (rgb2 >> 16) & 0xff; - int g2 = (rgb2 >> 8) & 0xff; - int b2 = rgb2 & 0xff; - return Math.abs(r1-r2) <= tolerance && Math.abs(g1-g2) <= tolerance && Math.abs(b1-b2) <= tolerance; - } - - private final static float hsb1[] = new float[3];//FIXME-not thread safe - private final static float hsb2[] = new float[3];//FIXME-not thread safe - - // Return rgb1 painted onto rgb2 - public static int combinePixels(int rgb1, int rgb2, int op) { - return combinePixels(rgb1, rgb2, op, 0xff); - } - - public static int combinePixels(int rgb1, int rgb2, int op, int extraAlpha, int channelMask) { - return (rgb2 & ~channelMask) | combinePixels(rgb1 & channelMask, rgb2, op, extraAlpha); - } - - public static int combinePixels(int rgb1, int rgb2, int op, int extraAlpha) { - if (op == REPLACE) - return rgb1; - int a1 = (rgb1 >> 24) & 0xff; - int r1 = (rgb1 >> 16) & 0xff; - int g1 = (rgb1 >> 8) & 0xff; - int b1 = rgb1 & 0xff; - int a2 = (rgb2 >> 24) & 0xff; - int r2 = (rgb2 >> 16) & 0xff; - int g2 = (rgb2 >> 8) & 0xff; - int b2 = rgb2 & 0xff; - - switch (op) { - case NORMAL: - break; - case MIN: - r1 = Math.min(r1, r2); - g1 = Math.min(g1, g2); - b1 = Math.min(b1, b2); - break; - case MAX: - r1 = Math.max(r1, r2); - g1 = Math.max(g1, g2); - b1 = Math.max(b1, b2); - break; - case ADD: - r1 = clamp(r1+r2); - g1 = clamp(g1+g2); - b1 = clamp(b1+b2); - break; - case SUBTRACT: - r1 = clamp(r2-r1); - g1 = clamp(g2-g1); - b1 = clamp(b2-b1); - break; - case DIFFERENCE: - r1 = clamp(Math.abs(r1-r2)); - g1 = clamp(Math.abs(g1-g2)); - b1 = clamp(Math.abs(b1-b2)); - break; - case MULTIPLY: - r1 = clamp(r1*r2/255); - g1 = clamp(g1*g2/255); - b1 = clamp(b1*b2/255); - break; - case DISSOLVE: - if ((randomGenerator.nextInt() & 0xff) <= a1) { - r1 = r2; - g1 = g2; - b1 = b2; - } - break; - case AVERAGE: - r1 = (r1+r2)/2; - g1 = (g1+g2)/2; - b1 = (b1+b2)/2; - break; - case HUE: - case SATURATION: - case VALUE: - case COLOR: - Color.RGBtoHSB(r1, g1, b1, hsb1); - Color.RGBtoHSB(r2, g2, b2, hsb2); - switch (op) { - case HUE: - hsb2[0] = hsb1[0]; - break; - case SATURATION: - hsb2[1] = hsb1[1]; - break; - case VALUE: - hsb2[2] = hsb1[2]; - break; - case COLOR: - hsb2[0] = hsb1[0]; - hsb2[1] = hsb1[1]; - break; - } - rgb1 = Color.HSBtoRGB(hsb2[0], hsb2[1], hsb2[2]); - r1 = (rgb1 >> 16) & 0xff; - g1 = (rgb1 >> 8) & 0xff; - b1 = rgb1 & 0xff; - break; - case SCREEN: - r1 = 255 - ((255 - r1) * (255 - r2)) / 255; - g1 = 255 - ((255 - g1) * (255 - g2)) / 255; - b1 = 255 - ((255 - b1) * (255 - b2)) / 255; - break; - case OVERLAY: - int m, s; - s = 255 - ((255 - r1) * (255 - r2)) / 255; - m = r1 * r2 / 255; - r1 = (s * r1 + m * (255 - r1)) / 255; - s = 255 - ((255 - g1) * (255 - g2)) / 255; - m = g1 * g2 / 255; - g1 = (s * g1 + m * (255 - g1)) / 255; - s = 255 - ((255 - b1) * (255 - b2)) / 255; - m = b1 * b2 / 255; - b1 = (s * b1 + m * (255 - b1)) / 255; - break; - case CLEAR: - r1 = g1 = b1 = 0xff; - break; - case DST_IN: - r1 = clamp((r2*a1)/255); - g1 = clamp((g2*a1)/255); - b1 = clamp((b2*a1)/255); - a1 = clamp((a2*a1)/255); - return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1; - case ALPHA: - a1 = a1*a2/255; - return (a1 << 24) | (r2 << 16) | (g2 << 8) | b2; - case ALPHA_TO_GRAY: - int na = 255-a1; - return (a1 << 24) | (na << 16) | (na << 8) | na; - } - if (extraAlpha != 0xff || a1 != 0xff) { - a1 = a1*extraAlpha/255; - int a3 = (255-a1)*a2/255; - r1 = clamp((r1*a1+r2*a3)/255); - g1 = clamp((g1*a1+g2*a3)/255); - b1 = clamp((b1*a1+b2*a3)/255); - a1 = clamp(a1+a3); - } - return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1; - } - -} \ No newline at end of file diff --git a/src/main/java/featurecat/benchmark/Stopwatch.java b/src/main/java/featurecat/benchmark/Stopwatch.java index d7349e627..354b333a7 100644 --- a/src/main/java/featurecat/benchmark/Stopwatch.java +++ b/src/main/java/featurecat/benchmark/Stopwatch.java @@ -2,58 +2,55 @@ import java.util.LinkedHashMap; -/** - * Simple stopwatch profiler to benchmark how long code takes to run - */ +/** Simple stopwatch profiler to benchmark how long code takes to run */ public class Stopwatch { - private LinkedHashMap times; - private long startTime; - private long lastTime; + private LinkedHashMap times; + private long startTime; + private long lastTime; - /** - * Begins timing from the moment this object is created. - */ - public Stopwatch() { - times = new LinkedHashMap<>(); - startTime = System.nanoTime(); - lastTime = startTime; - } + /** Begins timing from the moment this object is created. */ + public Stopwatch() { + times = new LinkedHashMap<>(); + startTime = System.nanoTime(); + lastTime = startTime; + } - /** - * Mark down the current time that it took $marker$ to run. - * @param marker a tag to describe what section of code is being profiled - */ - public void lap(String marker) { - long currentTime = System.nanoTime(); - times.put(marker, currentTime - lastTime); - lastTime = currentTime; - } + /** + * Mark down the current time that it took $marker$ to run. + * + * @param marker a tag to describe what section of code is being profiled + */ + public void lap(String marker) { + long currentTime = System.nanoTime(); + times.put(marker, currentTime - lastTime); + lastTime = currentTime; + } - /** - * Print the recorded profiler statistics - */ - public void print() { - System.out.println("\n======== profiler ========"); - long totalTime = lastTime - startTime; - for (String marker : times.keySet()) { - System.out.printf("%5.1f%% %s\n", 100.0 * times.get(marker) / totalTime, marker); - } - System.out.println("in " + totalTime / 1_000_000.0 + " ms"); + /** Print the recorded profiler statistics */ + public void print() { + System.out.println("\n======== profiler ========"); + long totalTime = lastTime - startTime; + for (String marker : times.keySet()) { + System.out.printf("%5.1f%% %s\n", 100.0 * times.get(marker) / totalTime, marker); } + System.out.println("in " + totalTime / 1_000_000.0 + " ms"); + } - public void printTimePerAction(int numActionsExecuted) { - System.out.println("\n======== profiler ========"); - long totalTime = System.nanoTime() - startTime; - System.out.println((totalTime / 1_000_000.0 / numActionsExecuted) + " ms per action"); - System.out.println(numActionsExecuted + " total actions executed in " + totalTime / 1_000_000_000.0 + " s"); - } + public void printTimePerAction(int numActionsExecuted) { + System.out.println("\n======== profiler ========"); + long totalTime = System.nanoTime() - startTime; + System.out.println((totalTime / 1_000_000.0 / numActionsExecuted) + " ms per action"); + System.out.println( + numActionsExecuted + " total actions executed in " + totalTime / 1_000_000_000.0 + " s"); + } - /** - * Reset the Stopwatch so it can be used again. Begins timing from the moment this method is executed. - */ - public void reset() { - times = new LinkedHashMap<>(); - startTime = System.nanoTime(); - lastTime = startTime; - } + /** + * Reset the Stopwatch so it can be used again. Begins timing from the moment this method is + * executed. + */ + public void reset() { + times = new LinkedHashMap<>(); + startTime = System.nanoTime(); + lastTime = startTime; + } } diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index 97d3e0789..169880605 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -1,276 +1,327 @@ package featurecat.lizzie; -import org.json.*; - import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; +import java.util.List; +import org.json.*; public class Config { - public boolean showMoveNumber = false; - public boolean showWinrate = true; - public boolean showVariationGraph = true; - public boolean showRawBoard = false; - public boolean handicapInsteadOfWinrate = false; - - public boolean showBranch = true; - public boolean showBestMoves = true; - public boolean showNextMoves = true; - public boolean showSubBoard = true; - public boolean largeSubBoard = false; - - public JSONObject config; - public JSONObject leelazConfig; - public JSONObject uiConfig; - public JSONObject persisted; - - private String configFilename = "config.txt"; - private String persistFilename = "persist"; - - private JSONObject loadAndMergeConfig(JSONObject defaultCfg, String fileName) throws IOException { - File file = new File(fileName); - if (!file.canRead()) { - System.err.printf("Creating config file %s\n", fileName); - try { - writeConfig(defaultCfg, file); - } catch (JSONException e) { - e.printStackTrace(); - System.exit(1); - } - } - - FileInputStream fp = new FileInputStream(file); - - JSONObject mergedcfg = null; - boolean modified = false; - try { - mergedcfg = new JSONObject(new JSONTokener(fp)); - modified = merge_defaults(mergedcfg, defaultCfg); - } catch (JSONException e) { - mergedcfg = null; - e.printStackTrace(); - } - - fp.close(); - - // Validate and correct settings - if (validateAndCorrectSettings(mergedcfg)) { - modified = true; - } - - if (modified) { - writeConfig(mergedcfg, file); - } - return mergedcfg; - - + public boolean showMoveNumber = false; + public boolean showWinrate = true; + public boolean showVariationGraph = true; + public boolean showComment = false; + public int commentFontSize = 0; + public boolean showRawBoard = false; + public boolean showCaptured = true; + public boolean handicapInsteadOfWinrate = false; + public boolean showDynamicKomi = true; + + public boolean showStatus = true; + public boolean showBranch = true; + public boolean showBestMoves = true; + public boolean showNextMoves = true; + public boolean showSubBoard = true; + public boolean largeSubBoard = false; + public boolean startMaximized = true; + + public JSONObject config; + public JSONObject leelazConfig; + public JSONObject uiConfig; + public JSONObject persisted; + + private String configFilename = "config.txt"; + private String persistFilename = "persist"; + + private JSONObject loadAndMergeConfig( + JSONObject defaultCfg, String fileName, boolean needValidation) throws IOException { + File file = new File(fileName); + if (!file.canRead()) { + System.err.printf("Creating config file %s\n", fileName); + try { + writeConfig(defaultCfg, file); + } catch (JSONException e) { + e.printStackTrace(); + System.exit(1); + } } - /** - * Check settings to ensure its consistency, especially for those whose types are not boolean. - * If any inconsistency is found, try to correct it or to report it. - *
- * For example, we only support 9x9, 13x13 or 19x19(default) sized boards. If the configured board size - * is not in the list above, we should correct it. - * - * @param config The config json object to check - * @return if any correction has been made. - */ - private boolean validateAndCorrectSettings(JSONObject config) { - // We don't care persisted settings. They are managed automatically. - if (config == null || !config.has("ui")) { - return false; - } - - boolean madeCorrections = false; - - // Check ui configs - JSONObject ui = config.getJSONObject("ui"); - - // Check board-size. We support only 9x9, 13x13 or 19x19 - int boardSize = ui.optInt("board-size", 19); - if (boardSize != 19 && boardSize != 13 && boardSize != 9) { - // Correct it to default 19x19 - ui.put("board-size", 19); - madeCorrections = true; - } - - // Check engine configs - JSONObject leelaz = config.getJSONObject("leelaz"); - // Check if the engine program exists. - String enginePath = leelaz.optString("engine-program", "./leelaz"); - if (!Files.exists(Paths.get(enginePath)) && !Files.exists(Paths.get(enginePath + ".exe" /* For windows */))) { - // FIXME: I don't know how to handle it properly.. Possibly showing a warning dialog may be a good idea? - leelaz.put("engine-program", "./leelaz"); - madeCorrections = true; - } - - // Similar checks for startup directory. It should exist and should be a directory. - String engineStartLocation = leelaz.optString("engine-start-location", "."); - if (!(Files.exists(Paths.get(engineStartLocation)) && Files.isDirectory(Paths.get(engineStartLocation)))) { - leelaz.put("engine-start-location", "."); - madeCorrections = true; - } + FileInputStream fp = new FileInputStream(file); - return madeCorrections; + JSONObject mergedcfg = null; + boolean modified = false; + try { + mergedcfg = new JSONObject(new JSONTokener(fp)); + modified = merge_defaults(mergedcfg, defaultCfg); + } catch (JSONException e) { + mergedcfg = null; + e.printStackTrace(); } - public Config() throws IOException { - JSONObject defaultConfig = createDefaultConfig(); - JSONObject persistConfig = createPersistConfig(); - - // Main properties - this.config = loadAndMergeConfig(defaultConfig, configFilename); - // Persisted properties - this.persisted = loadAndMergeConfig(persistConfig, persistFilename); - - leelazConfig = config.getJSONObject("leelaz"); - uiConfig = config.getJSONObject("ui"); - - showMoveNumber = uiConfig.getBoolean("show-move-number"); - showBranch = uiConfig.getBoolean("show-leelaz-variation"); - showWinrate = uiConfig.getBoolean("show-winrate"); - showVariationGraph = uiConfig.getBoolean("show-variation-graph"); - showBestMoves = uiConfig.getBoolean("show-best-moves"); - showNextMoves = uiConfig.getBoolean("show-next-moves"); - showSubBoard = uiConfig.getBoolean("show-subboard"); - largeSubBoard = uiConfig.getBoolean("large-subboard"); - handicapInsteadOfWinrate = uiConfig.getBoolean("handicap-instead-of-winrate"); - } + fp.close(); - // Modifies config by adding in values from default_config that are missing. - // Returns whether it added anything. - public boolean merge_defaults(JSONObject config, JSONObject defaults_config) { - boolean modified = false; - Iterator keys = defaults_config.keys(); - while (keys.hasNext()) { - String key = keys.next(); - Object new_val = defaults_config.get(key); - if (new_val instanceof JSONObject) { - if (!config.has(key)) { - config.put(key, new JSONObject()); - modified = true; - } - Object old_val = config.get(key); - modified |= merge_defaults((JSONObject) old_val, (JSONObject) new_val); - } else { - if (!config.has(key)) { - config.put(key, new_val); - modified = true; - } - } - } - return modified; + // Validate and correct settings + if (needValidation && validateAndCorrectSettings(mergedcfg)) { + modified = true; } - public void toggleShowMoveNumber() { - this.showMoveNumber = !this.showMoveNumber; - } - public void toggleShowBranch() { - this.showBranch = !this.showBranch; - } - public void toggleShowWinrate() { - this.showWinrate = !this.showWinrate; - } - public void toggleShowVariationGraph() { - this.showVariationGraph = !this.showVariationGraph; - } - public void toggleShowBestMoves() { - this.showBestMoves = !this.showBestMoves; + if (modified) { + writeConfig(mergedcfg, file); } - public void toggleShowNextMoves() { - this.showNextMoves = !this.showNextMoves; - } - public void toggleHandicapInsteadOfWinrate() { - this.handicapInsteadOfWinrate = !this.handicapInsteadOfWinrate; - } - public void toggleLargeSubBoard() { - this.largeSubBoard = !this.largeSubBoard; - } - public boolean showLargeSubBoard() { - return showSubBoard && largeSubBoard; + return mergedcfg; + } + + /** + * Check settings to ensure its consistency, especially for those whose types are not + * boolean. If any inconsistency is found, try to correct it or to report it.
+ * For example, we only support 9x9, 13x13 or 19x19(default) sized boards. If the configured board + * size is not in the list above, we should correct it. + * + * @param config The config json object to check + * @return if any correction has been made. + */ + private boolean validateAndCorrectSettings(JSONObject config) { + if (config == null) { + return false; } + boolean madeCorrections = false; + // Check ui configs + JSONObject ui = config.getJSONObject("ui"); - - private JSONObject createDefaultConfig() { - JSONObject config = new JSONObject(); - - // About engine parameter - JSONObject leelaz = new JSONObject(); - leelaz.put("network-file", "network.gz"); - leelaz.put("engine-command", "./leelaz --gtp --lagbuffer 0 --weights %network-file --threads 2"); - leelaz.put("engine-start-location", "."); - leelaz.put("max-analyze-time-minutes", 5); - leelaz.put("max-game-thinking-time-seconds", 2); - leelaz.put("print-comms", false); - leelaz.put("analyze-update-interval-centisec", 10); - leelaz.put("automatically-download-latest-network", false); - - config.put("leelaz", leelaz); - - // About User Interface display - JSONObject ui = new JSONObject(); - - ui.put("board-color", new JSONArray("[217, 152, 77]")); - ui.put("theme", "DefaultTheme"); - ui.put("shadows-enabled", true); - ui.put("fancy-stones", true); - ui.put("fancy-board", true); - ui.put("shadow-size", 100); - ui.put("show-move-number", false); - ui.put("show-leelaz-variation", true); - ui.put("show-winrate", true); - ui.put("show-variation-graph", true); - ui.put("show-best-moves", true); - ui.put("show-next-moves", true); - ui.put("show-subboard", true); - ui.put("large-subboard", false); - ui.put("win-rate-always-black", false); - ui.put("confirm-exit", false); - ui.put("handicap-instead-of-winrate",false); - ui.put("board-size", 19); - - config.put("ui", ui); - return config; + // Check board-size. We support only 9x9, 13x13 or 19x19 + int boardSize = ui.optInt("board-size", 19); + if (boardSize != 19 && boardSize != 13 && boardSize != 9) { + // Correct it to default 19x19 + ui.put("board-size", 19); + madeCorrections = true; } - private JSONObject createPersistConfig() { - JSONObject config = new JSONObject(); - - // About engine parameter - JSONObject filesys = new JSONObject(); - filesys.put("last-folder", ""); - - config.put("filesystem", filesys); - - // About User Interface display - //JSONObject ui = new JSONObject(); - - //ui.put("window-height", 657); - //ui.put("window-width", 687); - - //config.put("ui", ui); - return config; + // Check engine configs + JSONObject leelaz = config.getJSONObject("leelaz"); + // Checks for startup directory. It should exist and should be a directory. + String engineStartLocation = getBestDefaultLeelazPath(); + if (!(Files.exists(Paths.get(engineStartLocation)) + && Files.isDirectory(Paths.get(engineStartLocation)))) { + leelaz.put("engine-start-location", "."); + madeCorrections = true; } - private void writeConfig(JSONObject config, File file) throws IOException, JSONException { - file.createNewFile(); - - FileOutputStream fp = new FileOutputStream(file); - OutputStreamWriter writer = new OutputStreamWriter(fp); - - writer.write(config.toString(2)); - - writer.close(); - fp.close(); + return madeCorrections; + } + + public Config() throws IOException { + JSONObject defaultConfig = createDefaultConfig(); + JSONObject persistConfig = createPersistConfig(); + + // Main properties + this.config = loadAndMergeConfig(defaultConfig, configFilename, true); + // Persisted properties + this.persisted = loadAndMergeConfig(persistConfig, persistFilename, false); + + leelazConfig = config.getJSONObject("leelaz"); + uiConfig = config.getJSONObject("ui"); + + showMoveNumber = uiConfig.getBoolean("show-move-number"); + showStatus = uiConfig.getBoolean("show-status"); + showBranch = uiConfig.getBoolean("show-leelaz-variation"); + showWinrate = uiConfig.getBoolean("show-winrate"); + showVariationGraph = uiConfig.getBoolean("show-variation-graph"); + showComment = uiConfig.optBoolean("show-comment", false); + commentFontSize = uiConfig.optInt("comment-font-size", 0); + showCaptured = uiConfig.getBoolean("show-captured"); + showBestMoves = uiConfig.getBoolean("show-best-moves"); + showNextMoves = uiConfig.getBoolean("show-next-moves"); + showSubBoard = uiConfig.getBoolean("show-subboard"); + largeSubBoard = uiConfig.getBoolean("large-subboard"); + handicapInsteadOfWinrate = uiConfig.getBoolean("handicap-instead-of-winrate"); + startMaximized = uiConfig.getBoolean("window-maximized"); + showDynamicKomi = uiConfig.getBoolean("show-dynamic-komi"); + } + + // Modifies config by adding in values from default_config that are missing. + // Returns whether it added anything. + public boolean merge_defaults(JSONObject config, JSONObject defaults_config) { + boolean modified = false; + Iterator keys = defaults_config.keys(); + while (keys.hasNext()) { + String key = keys.next(); + Object new_val = defaults_config.get(key); + if (new_val instanceof JSONObject) { + if (!config.has(key)) { + config.put(key, new JSONObject()); + modified = true; + } + Object old_val = config.get(key); + modified |= merge_defaults((JSONObject) old_val, (JSONObject) new_val); + } else { + if (!config.has(key)) { + config.put(key, new_val); + modified = true; + } + } } - - public void persist() throws IOException { - writeConfig(this.persisted, new File(persistFilename)); + return modified; + } + + public void toggleShowMoveNumber() { + this.showMoveNumber = !this.showMoveNumber; + } + + public void toggleShowBranch() { + this.showBranch = !this.showBranch; + } + + public void toggleShowWinrate() { + this.showWinrate = !this.showWinrate; + } + + public void toggleShowVariationGraph() { + this.showVariationGraph = !this.showVariationGraph; + } + + public void toggleShowComment() { + this.showComment = !this.showComment; + } + + public void toggleShowBestMoves() { + this.showBestMoves = !this.showBestMoves; + } + + public void toggleShowNextMoves() { + this.showNextMoves = !this.showNextMoves; + } + + public void toggleHandicapInsteadOfWinrate() { + this.handicapInsteadOfWinrate = !this.handicapInsteadOfWinrate; + } + + public void toggleLargeSubBoard() { + this.largeSubBoard = !this.largeSubBoard; + } + + public boolean showLargeSubBoard() { + return showSubBoard && largeSubBoard; + } + + /** + * Scans the current directory as well as the current PATH to find a reasonable default leelaz + * binary. + * + * @return A working path to a leelaz binary. If there are none on the PATH, "./leelaz" is + * returned for backwards compatibility. + */ + public static String getBestDefaultLeelazPath() { + List potentialPaths = new ArrayList<>(); + potentialPaths.add("."); + potentialPaths.addAll(Arrays.asList(System.getenv("PATH").split(":"))); + + for (String potentialPath : potentialPaths) { + for (String potentialExtension : Arrays.asList(new String[] {"", ".exe"})) { + File potentialLeelaz = new File(potentialPath, "leelaz" + potentialExtension); + if (potentialLeelaz.exists() && potentialLeelaz.canExecute()) { + return potentialLeelaz.getPath(); + } + } } + return "./leelaz"; + } + + private JSONObject createDefaultConfig() { + JSONObject config = new JSONObject(); + + // About engine parameter + JSONObject leelaz = new JSONObject(); + leelaz.put("network-file", "network.gz"); + leelaz.put( + "engine-command", + String.format( + "%s --gtp --lagbuffer 0 --weights %%network-file --threads 2", + getBestDefaultLeelazPath())); + leelaz.put("engine-start-location", "."); + leelaz.put("max-analyze-time-minutes", 5); + leelaz.put("max-game-thinking-time-seconds", 2); + leelaz.put("print-comms", false); + leelaz.put("analyze-update-interval-centisec", 10); + leelaz.put("automatically-download-latest-network", false); + + config.put("leelaz", leelaz); + + // About User Interface display + JSONObject ui = new JSONObject(); + + ui.put("board-color", new JSONArray("[217, 152, 77]")); + ui.put("shadows-enabled", true); + ui.put("fancy-stones", true); + ui.put("fancy-board", true); + ui.put("shadow-size", 100); + ui.put("show-move-number", false); + ui.put("show-status", true); + ui.put("show-leelaz-variation", true); + ui.put("show-winrate", true); + ui.put("show-variation-graph", true); + ui.put("show-captured", true); + ui.put("show-best-moves", true); + ui.put("show-next-moves", true); + ui.put("show-subboard", true); + ui.put("large-subboard", false); + ui.put("win-rate-always-black", false); + ui.put("confirm-exit", false); + ui.put("resume-previous-game", false); + ui.put("autosave-interval-seconds", -1); + ui.put("handicap-instead-of-winrate", false); + ui.put("board-size", 19); + ui.put("window-size", new JSONArray("[1024, 768]")); + ui.put("window-maximized", false); + ui.put("show-dynamic-komi", true); + ui.put("min-playout-ratio-for-stats", 0.0); + + config.put("ui", ui); + return config; + } + + private JSONObject createPersistConfig() { + JSONObject config = new JSONObject(); + + // About engine parameter + JSONObject filesys = new JSONObject(); + filesys.put("last-folder", ""); + + config.put("filesystem", filesys); + + // About User Interface display + JSONObject ui = new JSONObject(); + + // ui.put("window-height", 657); + // ui.put("window-width", 687); + // ui.put("max-alpha", 240); + + // Avoid the key "ui" because it was used to distinguish "config" and "persist" + // in old version of validateAndCorrectSettings(). + // If we use "ui" here, we will have trouble to run old lizzie. + config.put("ui-persist", ui); + return config; + } + + private void writeConfig(JSONObject config, File file) throws IOException, JSONException { + file.createNewFile(); + + FileOutputStream fp = new FileOutputStream(file); + OutputStreamWriter writer = new OutputStreamWriter(fp); + + writer.write(config.toString(2)); + + writer.close(); + fp.close(); + } + + public void persist() throws IOException { + writeConfig(this.persisted, new File(persistFilename)); + } } diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index f5c1b54cf..15a3b6de2 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -1,71 +1,132 @@ package featurecat.lizzie; -import org.json.JSONException; import featurecat.lizzie.analysis.Leelaz; -import featurecat.lizzie.plugin.PluginManager; -import featurecat.lizzie.rules.Board; import featurecat.lizzie.gui.LizzieFrame; - -import javax.swing.*; +import featurecat.lizzie.rules.Board; +import java.io.File; import java.io.IOException; +import javax.swing.*; +import org.json.JSONArray; +import org.json.JSONException; -/** - * Main class. - */ +/** Main class. */ public class Lizzie { - public static LizzieFrame frame; - public static Leelaz leelaz; - public static Board board; - public static Config config; - public static String lizzieVersion = "0.5"; - - /** - * Launches the game window, and runs the game. - */ - public static void main(String[] args) throws IOException, JSONException, ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException, IllegalAccessException, InterruptedException { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - - config = new Config(); + public static LizzieFrame frame; + public static Leelaz leelaz; + public static Board board; + public static Config config; + public static String lizzieVersion = "0.5"; + private static String[] args; - PluginManager.loadPlugins(); + /** Launches the game window, and runs the game. */ + public static void main(String[] args) throws IOException { + setLookAndFeel(); + args = args; + config = new Config(); + board = new Board(); + frame = new LizzieFrame(); + new Thread(Lizzie::run).start(); + } - board = new Board(); + public static void run() { + try { + leelaz = new Leelaz(); + if (config.handicapInsteadOfWinrate) { + leelaz.estimatePassWinrate(); + } + if (args.length == 1) { + frame.loadFile(new File(args[0])); + } else if (config.config.getJSONObject("ui").getBoolean("resume-previous-game")) { + board.resumePreviousGame(); + } + leelaz.togglePonder(); + } catch (IOException e) { + e.printStackTrace(); + System.exit(-1); + } + } - frame = new LizzieFrame(); + public static void setLookAndFeel() { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (UnsupportedLookAndFeelException e) { + e.printStackTrace(); + } + } - new Thread( () -> { - try { - leelaz = new Leelaz(); - if(config.handicapInsteadOfWinrate) { - leelaz.estimatePassWinrate(); - } - leelaz.togglePonder(); - } catch (IOException e) { - e.printStackTrace(); - } - }).start(); + public static void shutdown() { + if (board != null && config.config.getJSONObject("ui").getBoolean("confirm-exit")) { + int ret = + JOptionPane.showConfirmDialog( + null, "Do you want to save this SGF?", "Save SGF?", JOptionPane.OK_CANCEL_OPTION); + if (ret == JOptionPane.OK_OPTION) { + LizzieFrame.saveFile(); + } + } + if (board != null) { + board.autosaveToMemory(); + } - + try { + config.persist(); + } catch (IOException e) { + e.printStackTrace(); // Failed to save config } - public static void shutdown() { - PluginManager.onShutdown(); - if (board != null && config.config.getJSONObject("ui").getBoolean("confirm-exit")) { - int ret = JOptionPane.showConfirmDialog(null, "Do you want to save this SGF?", "Save SGF?", JOptionPane.OK_CANCEL_OPTION); - if (ret == JOptionPane.OK_OPTION) { - LizzieFrame.saveSgf(); - } - } + if (leelaz != null) leelaz.shutdown(); + System.exit(0); + } - try { - config.persist(); - } catch (IOException err) { - // Failed to save config - } + /** + * Switch the Engine by index number + * + * @param index engine index + */ + public static void switchEngine(int index) { - if (leelaz != null) - leelaz.shutdown(); - System.exit(0); + String commandLine = null; + if (index == 0) { + commandLine = Lizzie.config.leelazConfig.getString("engine-command"); + commandLine = + commandLine.replaceAll( + "%network-file", Lizzie.config.leelazConfig.getString("network-file")); + } else { + JSONArray commandList = Lizzie.config.leelazConfig.getJSONArray("engine-command-list"); + if (commandList != null && commandList.length() >= index) { + commandLine = commandList.getString(index - 1); + } else { + index = -1; + } + } + if (index < 0 + || commandLine == null + || commandLine.trim().isEmpty() + || index == Lizzie.leelaz.currentEngineN()) { + return; + } + + // Workaround for leelaz cannot exit when restarting + if (leelaz.isThinking) { + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.frame.isPlayingAgainstLeelaz = false; + Lizzie.leelaz.togglePonder(); // we must toggle twice for it to restart pondering + Lizzie.leelaz.isThinking = false; + } + Lizzie.leelaz.togglePonder(); } + board.saveMoveNumber(); + try { + leelaz.restartEngine(commandLine, index); + board.restoreMoveNumber(); + } catch (IOException e) { + e.printStackTrace(); + } + } } diff --git a/src/main/java/featurecat/lizzie/Util.java b/src/main/java/featurecat/lizzie/Util.java index 395dfd73a..94def40a9 100644 --- a/src/main/java/featurecat/lizzie/Util.java +++ b/src/main/java/featurecat/lizzie/Util.java @@ -1,5 +1,6 @@ package featurecat.lizzie; +import java.awt.FontMetrics; import java.io.*; import java.net.URL; import java.nio.channels.Channels; @@ -11,73 +12,99 @@ import java.util.zip.GZIPInputStream; public class Util { - /** - * @param val the value we want to clamp - * @param min the minimum value that will be returned - * @param max the maximum value that will be returned - * @return the closest number to val within the range [min, max] - */ - public static > T clamp(T val, T min, T max) { - if (val.compareTo(min) < 0) return min; - else if (val.compareTo(max) > 0) return max; - else return val; - } + /** + * @param val the value we want to clamp + * @param min the minimum value that will be returned + * @param max the maximum value that will be returned + * @return the closest number to val within the range [min, max] + */ + public static > T clamp(T val, T min, T max) { + if (val.compareTo(min) < 0) return min; + else if (val.compareTo(max) > 0) return max; + else return val; + } - /** - * @return the sha 256 checksum of decompressed contents from a GZIPed file - */ - public static String getSha256Sum(File file) { - try (InputStream inputStream = new GZIPInputStream(new FileInputStream(file))) { - MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); - DigestInputStream digestInputStream = new DigestInputStream(inputStream, messageDigest); + /** @return the sha 256 checksum of decompressed contents from a GZIPed file */ + public static String getSha256Sum(File file) { + try (InputStream inputStream = new GZIPInputStream(new FileInputStream(file))) { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + DigestInputStream digestInputStream = new DigestInputStream(inputStream, messageDigest); - // we have to read the file. additionally, using a buffer is efficient. - // 8192 was tested as the best value among 4096, 8192, 16384, and 32768. - byte[] buffer = new byte[8192]; - while (digestInputStream.read(buffer) != -1) ; + // we have to read the file. additionally, using a buffer is efficient. + // 8192 was tested as the best value among 4096, 8192, 16384, and 32768. + byte[] buffer = new byte[8192]; + while (digestInputStream.read(buffer) != -1) ; - MessageDigest digest = digestInputStream.getMessageDigest(); - digestInputStream.close(); + MessageDigest digest = digestInputStream.getMessageDigest(); + digestInputStream.close(); - byte[] sha256 = digest.digest(); - StringBuilder sb = new StringBuilder(); - for (byte b : sha256) { - sb.append(String.format("%02X", b)); - } - return sb.toString().toLowerCase(); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - } catch (EOFException e) { - // do nothing, just means we need to download a new one - } catch (IOException e) { - e.printStackTrace(); - } - return null; + byte[] sha256 = digest.digest(); + StringBuilder sb = new StringBuilder(); + for (byte b : sha256) { + sb.append(String.format("%02X", b)); + } + return sb.toString().toLowerCase(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (EOFException e) { + // do nothing, just means we need to download a new one + } catch (IOException e) { + e.printStackTrace(); } + return null; + } - /** - * @return the url's contents, downloaded as a string - */ - public static String downloadAsString(URL url) { - try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { - return br.lines().collect(Collectors.joining("\n")); - } catch (IOException e) { - e.printStackTrace(); - } + /** @return the url's contents, downloaded as a string */ + public static String downloadAsString(URL url) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { + return br.lines().collect(Collectors.joining("\n")); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } - return null; + /** Downloads the contents of the url, and saves them in a file. */ + public static void saveAsFile(URL url, File file) { + try { + ReadableByteChannel rbc = Channels.newChannel(url.openStream()); + FileOutputStream fos = new FileOutputStream(file); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + } catch (IOException e) { + e.printStackTrace(); } + } - /** - * Downloads the contents of the url, and saves them in a file. - */ - public static void saveAsFile(URL url, File file) { - try { - ReadableByteChannel rbc = Channels.newChannel(url.openStream()); - FileOutputStream fos = new FileOutputStream(file); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - } catch (IOException e) { - e.printStackTrace(); + /** + * Truncate text that is too long for the given width + * + * @param line + * @param fm + * @param fitWidth + * @return fitted + */ + public static String truncateStringByWidth(String line, FontMetrics fm, int fitWidth) { + if (line == null || line.length() == 0) { + return ""; + } + int width = fm.stringWidth(line); + if (width > fitWidth) { + int guess = line.length() * fitWidth / width; + String before = line.substring(0, guess).trim(); + width = fm.stringWidth(before); + if (width > fitWidth) { + int diff = width - fitWidth; + int i = 0; + for (; (diff > 0 && i < 5); i++) { + diff = diff - fm.stringWidth(line.substring(guess - i - 1, guess - i)); } + return line.substring(0, guess - i).trim(); + } else { + return before; + } + } else { + return line; } + } } diff --git a/src/main/java/featurecat/lizzie/analysis/Branch.java b/src/main/java/featurecat/lizzie/analysis/Branch.java index e6958254f..a0f61d1d5 100644 --- a/src/main/java/featurecat/lizzie/analysis/Branch.java +++ b/src/main/java/featurecat/lizzie/analysis/Branch.java @@ -4,33 +4,45 @@ import featurecat.lizzie.rules.BoardData; import featurecat.lizzie.rules.Stone; import featurecat.lizzie.rules.Zobrist; - import java.util.List; public class Branch { - public BoardData data; + public BoardData data; - public Branch(Board board, List variation) { - int moveNumber = 0; - int[] lastMove = board.getLastMove(); - int[] moveNumberList = new int[Board.BOARD_SIZE * Board.BOARD_SIZE]; - boolean blackToPlay = board.getData().blackToPlay; + public Branch(Board board, List variation) { + int moveNumber = 0; + int[] lastMove = board.getLastMove(); + int[] moveNumberList = new int[Board.BOARD_SIZE * Board.BOARD_SIZE]; + boolean blackToPlay = board.getData().blackToPlay; - Stone lastMoveColor = board.getData().lastMoveColor; - Stone[] stones = board.getStones().clone(); - Zobrist zobrist = board.getData().zobrist == null ? null : board.getData().zobrist.clone(); + Stone lastMoveColor = board.getData().lastMoveColor; + Stone[] stones = board.getStones().clone(); + Zobrist zobrist = board.getData().zobrist == null ? null : board.getData().zobrist.clone(); - // Dont care about winrate for branch - this.data = new BoardData(stones, lastMove, lastMoveColor, blackToPlay, - zobrist, moveNumber, moveNumberList, board.getData().blackCaptures, board.getData().whiteCaptures, 0.0, 0); + // Dont care about winrate for branch + this.data = + new BoardData( + stones, + lastMove, + lastMoveColor, + blackToPlay, + zobrist, + moveNumber, + moveNumberList, + board.getData().blackCaptures, + board.getData().whiteCaptures, + 0.0, + 0); - for (int i = 0; i < variation.size(); i++) { - int[] coord = Board.convertNameToCoordinates(variation.get(i)); - data.lastMove = coord; - data.stones[Board.getIndex(coord[0], coord[1])] = data.blackToPlay ? Stone.BLACK_GHOST : Stone.WHITE_GHOST; - data.moveNumberList[Board.getIndex(coord[0], coord[1])] = i + 1; - data.lastMoveColor = data.blackToPlay ? Stone.WHITE : Stone.BLACK; - data.blackToPlay = !data.blackToPlay; - } + for (int i = 0; i < variation.size(); i++) { + int[] coord = Board.convertNameToCoordinates(variation.get(i)); + if (coord == null) break; + data.lastMove = coord; + data.stones[Board.getIndex(coord[0], coord[1])] = + data.blackToPlay ? Stone.BLACK_GHOST : Stone.WHITE_GHOST; + data.moveNumberList[Board.getIndex(coord[0], coord[1])] = i + 1; + data.lastMoveColor = data.blackToPlay ? Stone.WHITE : Stone.BLACK; + data.blackToPlay = !data.blackToPlay; } + } } diff --git a/src/main/java/featurecat/lizzie/analysis/GameInfo.java b/src/main/java/featurecat/lizzie/analysis/GameInfo.java index 7910bc1a8..198eff393 100644 --- a/src/main/java/featurecat/lizzie/analysis/GameInfo.java +++ b/src/main/java/featurecat/lizzie/analysis/GameInfo.java @@ -1,56 +1,55 @@ package featurecat.lizzie.analysis; - import java.util.Date; public class GameInfo { - public static final String DEFAULT_NAME_HUMAN_PLAYER = "Human"; - public static final String DEFAULT_NAME_CPU_PLAYER = "Leela Zero"; - public static final double DEFAULT_KOMI = 7.5; - - private String playerBlack = ""; - private String playerWhite = ""; - private Date date = new Date(); - private double komi = DEFAULT_KOMI; - private int handicap = 0; - - public String getPlayerBlack() { - return playerBlack; - } - - public Date getDate() { - return date; - } - - public void setDate(Date date) { - this.date = date; - } - - public void setPlayerBlack(String playerBlack) { - this.playerBlack = playerBlack; - } - - public String getPlayerWhite() { - return playerWhite; - } - - public void setPlayerWhite(String playerWhite) { - this.playerWhite = playerWhite; - } - - public double getKomi() { - return komi; - } - - public void setKomi(double komi) { - this.komi = komi; - } - - public int getHandicap() { - return handicap; - } - - public void setHandicap(int handicap) { - this.handicap = handicap; - } + public static final String DEFAULT_NAME_HUMAN_PLAYER = "Human"; + public static final String DEFAULT_NAME_CPU_PLAYER = "Leela Zero"; + public static final double DEFAULT_KOMI = 7.5; + + private String playerBlack = ""; + private String playerWhite = ""; + private Date date = new Date(); + private double komi = DEFAULT_KOMI; + private int handicap = 0; + + public String getPlayerBlack() { + return playerBlack; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public void setPlayerBlack(String playerBlack) { + this.playerBlack = playerBlack; + } + + public String getPlayerWhite() { + return playerWhite; + } + + public void setPlayerWhite(String playerWhite) { + this.playerWhite = playerWhite; + } + + public double getKomi() { + return komi; + } + + public void setKomi(double komi) { + this.komi = komi; + } + + public int getHandicap() { + return handicap; + } + + public void setHandicap(int handicap) { + this.handicap = handicap; + } } diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index ca6a92f1f..f9224919b 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -2,493 +2,671 @@ import featurecat.lizzie.Lizzie; import featurecat.lizzie.Util; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import featurecat.lizzie.rules.Stone; - -import javax.swing.*; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.swing.*; +import org.json.JSONException; +import org.json.JSONObject; /** - * an interface with leelaz.exe go engine. Can be adapted for GTP, but is specifically designed for GCP's Leela Zero. - * leelaz is modified to output information as it ponders - * see www.github.com/gcp/leela-zero + * An interface with leelaz go engine. Can be adapted for GTP, but is specifically designed for + * GCP's Leela Zero. leelaz is modified to output information as it ponders see + * www.github.com/gcp/leela-zero */ public class Leelaz { - private static final ResourceBundle resourceBundle = ResourceBundle.getBundle("l10n.DisplayStrings"); + private static final ResourceBundle resourceBundle = + ResourceBundle.getBundle("l10n.DisplayStrings"); - private static final long MINUTE = 60 * 1000; // number of milliseconds in a minute - private static final String baseURL = "http://zero.sjeng.org"; + private static final long MINUTE = 60 * 1000; // number of milliseconds in a minute + private static final String baseURL = "http://zero.sjeng.org"; - private long maxAnalyzeTimeMillis;//, maxThinkingTimeMillis; - private int cmdNumber; - private int currentCmdNum; + private long maxAnalyzeTimeMillis; // , maxThinkingTimeMillis; + private int cmdNumber; + private int currentCmdNum; + private ArrayDeque cmdQueue; - private Process process; + private Process process; - private BufferedInputStream inputStream; - private BufferedOutputStream outputStream; + private BufferedInputStream inputStream; + private BufferedOutputStream outputStream; - private boolean printCommunication; + private boolean printCommunication; - private List bestMoves; - private List bestMovesTemp; + private List bestMoves; + private List bestMovesTemp; - private List listeners; + private List listeners; - private boolean isPondering; - private long startPonderTime; + private boolean isPondering; + private long startPonderTime; - // fixed_handicap - public boolean isSettingHandicap = false; + // fixed_handicap + public boolean isSettingHandicap = false; - // genmove - public boolean isThinking = false; + // genmove + public boolean isThinking = false; - private boolean isLoaded = false; - private boolean isCheckingVersion; + private boolean isLoaded = false; + private boolean isCheckingVersion; - /** - * Initializes the leelaz process and starts reading output - * - * @throws IOException - */ - public Leelaz() throws IOException, JSONException { - bestMoves = new ArrayList<>(); - bestMovesTemp = new ArrayList<>(); - listeners = new CopyOnWriteArrayList<>(); + // for Multiple Engine + private String engineCommand = null; + private List commands = null; + private JSONObject config = null; + private String currentWeight = null; + private boolean switching = false; + private int currentEngineN = -1; + private ScheduledExecutorService executor = null; - isPondering = false; - startPonderTime = System.currentTimeMillis(); - cmdNumber = 1; - currentCmdNum = -1; + // dynamic komi and opponent komi as reported by dynamic-komi version of leelaz + private float dynamicKomi = Float.NaN, dynamicOppKomi = Float.NaN; + /** + * Initializes the leelaz process and starts reading output + * + * @throws IOException + */ + public Leelaz() throws IOException, JSONException { + bestMoves = new ArrayList<>(); + bestMovesTemp = new ArrayList<>(); + listeners = new CopyOnWriteArrayList<>(); - JSONObject config = Lizzie.config.config.getJSONObject("leelaz"); + isPondering = false; + startPonderTime = System.currentTimeMillis(); + cmdNumber = 1; + currentCmdNum = 0; + cmdQueue = new ArrayDeque<>(); - printCommunication = config.getBoolean("print-comms"); - maxAnalyzeTimeMillis = MINUTE * config.getInt("max-analyze-time-minutes"); + // Move config to member for other method call + config = Lizzie.config.config.getJSONObject("leelaz"); - if (config.getBoolean("automatically-download-latest-network")) { - updateToLatestNetwork(); - } + printCommunication = config.getBoolean("print-comms"); + maxAnalyzeTimeMillis = MINUTE * config.getInt("max-analyze-time-minutes"); - // command string for starting the engine - String engineCommand = config.getString("engine-command"); - // substitute in the weights file - engineCommand = engineCommand.replaceAll("%network-file", config.getString("network-file")); - // create this as a list which gets passed into the processbuilder - List commands = Arrays.asList(engineCommand.split(" ")); - - // run leelaz - ProcessBuilder processBuilder = new ProcessBuilder(commands); - processBuilder.directory(new File(config.optString("engine-start-location", "."))); - processBuilder.redirectErrorStream(true); - process = processBuilder.start(); - - initializeStreams(); - - // Send a version request to check that we have a supported version - // Response handled in parseLine - isCheckingVersion = true; - sendCommand("version"); - - // start a thread to continuously read Leelaz output - new Thread(this::read).start(); - Lizzie.frame.refreshBackground(); + if (config.getBoolean("automatically-download-latest-network")) { + updateToLatestNetwork(); } - private void updateToLatestNetwork() { - try { - if (needToDownloadLatestNetwork()) { - int dialogResult = JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.display.download-latest-network-prompt")); - if (dialogResult == JOptionPane.YES_OPTION) { - Util.saveAsFile(new URL(baseURL + "/networks/" + getBestNetworkHash() + ".gz"), - new File(Lizzie.config.leelazConfig.getString("network-file"))); - } - } - } catch (IOException e) { - e.printStackTrace(); - // now we're probably still ok. Maybe we're offline -- then it's not a big problem. - } + // command string for starting the engine + engineCommand = config.getString("engine-command"); + // substitute in the weights file + engineCommand = engineCommand.replaceAll("%network-file", config.getString("network-file")); + + // Initialize current engine number and start engine + currentEngineN = 0; + startEngine(engineCommand); + Lizzie.frame.refreshBackground(); + } + + public void startEngine(String engineCommand) throws IOException { + // Check engine command + if (engineCommand == null || engineCommand.trim().isEmpty()) { + return; } - private String getBestNetworkHash() throws IOException { - // finds a valid network hash - Pattern networkHashFinder = Pattern.compile("(?<=/networks/)[a-f0-9]+"); - - String networks = null; - try { - networks = Util.downloadAsString(new URL(baseURL + "/network-profiles")); - } catch (MalformedURLException e) { - e.printStackTrace(); + File startfolder = new File(config.optString("engine-start-location", ".")); + String networkFile = config.getString("network-file"); + // substitute in the weights file + engineCommand = engineCommand.replaceAll("%network-file", networkFile); + // create this as a list which gets passed into the processbuilder + commands = Arrays.asList(engineCommand.split(" ")); + + // get weight name + if (engineCommand != null) { + Pattern wPattern = Pattern.compile("(?s).*?(--weights |-w )([^ ]+)(?s).*"); + Matcher wMatcher = wPattern.matcher(engineCommand); + if (wMatcher.matches()) { + currentWeight = wMatcher.group(2); + if (currentWeight != null) { + String[] names = currentWeight.split("[\\\\|/]"); + if (names != null && names.length > 1) { + currentWeight = names[names.length - 1]; + } } - if (networks == null) { - throw new IOException("Could not determine the best network URL"); - } - Matcher m = networkHashFinder.matcher(networks); - // get the first match - this will be the best network. - m.find(); - - return m.group(0); + } } - private boolean needToDownloadLatestNetwork() throws IOException { - File networkFile = new File(Lizzie.config.leelazConfig.getString("network-file")); - if (!networkFile.exists()) { - return true; - } else { - String currentNetworkHash = Util.getSha256Sum(networkFile); - if (currentNetworkHash == null) - return true; + // Check if engine is present + File lef = startfolder.toPath().resolve(new File(commands.get(0)).toPath()).toFile(); + if (!lef.exists()) { + JOptionPane.showMessageDialog( + null, + resourceBundle.getString("LizzieFrame.display.leelaz-missing"), + "Lizzie - Error!", + JOptionPane.ERROR_MESSAGE); + throw new IOException("engine not present"); + } - String bestNetworkHash = getBestNetworkHash(); + // Check if network file is present + File wf = startfolder.toPath().resolve(new File(networkFile).toPath()).toFile(); + if (!wf.exists()) { + JOptionPane.showMessageDialog( + null, resourceBundle.getString("LizzieFrame.display.network-missing")); + throw new IOException("network-file not present"); + } - return !currentNetworkHash.equals(bestNetworkHash); + // run leelaz + ProcessBuilder processBuilder = new ProcessBuilder(commands); + processBuilder.directory(startfolder); + processBuilder.redirectErrorStream(true); + process = processBuilder.start(); + + initializeStreams(); + + // Send a version request to check that we have a supported version + // Response handled in parseLine + isCheckingVersion = true; + sendCommand("version"); + + // start a thread to continuously read Leelaz output + // new Thread(this::read).start(); + // can stop engine for switching weights + executor = Executors.newSingleThreadScheduledExecutor(); + executor.execute(this::read); + } + + public void restartEngine(String engineCommand, int index) throws IOException { + if (engineCommand == null || engineCommand.trim().isEmpty()) { + return; + } + switching = true; + this.engineCommand = engineCommand; + // stop the ponder + if (Lizzie.leelaz.isPondering()) { + Lizzie.leelaz.togglePonder(); + } + normalQuit(); + startEngine(engineCommand); + currentEngineN = index; + togglePonder(); + } + + public void normalQuit() { + sendCommand("quit"); + executor.shutdown(); + try { + while (!executor.awaitTermination(1, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + if (executor.awaitTermination(1, TimeUnit.SECONDS)) { + shutdown(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + private void updateToLatestNetwork() { + try { + if (needToDownloadLatestNetwork()) { + int dialogResult = + JOptionPane.showConfirmDialog( + null, + resourceBundle.getString("LizzieFrame.display.download-latest-network-prompt")); + if (dialogResult == JOptionPane.YES_OPTION) { + Util.saveAsFile( + new URL(baseURL + "/networks/" + getBestNetworkHash() + ".gz"), + new File(Lizzie.config.leelazConfig.getString("network-file"))); } + } + } catch (IOException e) { + e.printStackTrace(); + // now we're probably still ok. Maybe we're offline -- then it's not a big problem. } + } - /** - * Initializes the input and output streams - */ - private void initializeStreams() { - inputStream = new BufferedInputStream(process.getInputStream()); - outputStream = new BufferedOutputStream(process.getOutputStream()); - } + private String getBestNetworkHash() throws IOException { + return Util.downloadAsString(new URL(baseURL + "/best-network-hash")).split("\n")[0]; + } - private void parseInfo(String line) { + private boolean needToDownloadLatestNetwork() throws IOException { + File networkFile = new File(Lizzie.config.leelazConfig.getString("network-file")); + if (!networkFile.exists()) { + return true; + } else { + String currentNetworkHash = Util.getSha256Sum(networkFile); + if (currentNetworkHash == null) return true; - bestMoves = new ArrayList<>(); - String[] variations = line.split(" info "); - for (String var : variations) { - bestMoves.add(new MoveData(var)); - } - // Not actually necessary to sort with current version of LZ (0.15) - // but not guaranteed to be ordered in the future - Collections.sort(bestMoves); - } + String bestNetworkHash = getBestNetworkHash(); - /** - * Parse a line of Leelaz output - * - * @param line output line - */ - private void parseLine(String line) { - synchronized (this) { - if (line.equals("\n")) { - // End of response - } else if (line.startsWith("info")) { - isLoaded = true; - if (currentCmdNum == cmdNumber - 1) { - // This should not be stale data when the command number match - parseInfo(line.substring(5)); - notifyBestMoveListeners(); - if (Lizzie.frame != null) Lizzie.frame.repaint(); - // don't follow the maxAnalyzeTime rule if we are in analysis mode - if (System.currentTimeMillis() - startPonderTime > maxAnalyzeTimeMillis && !Lizzie.board.inAnalysisMode()) { - togglePonder(); - } - } - } else if (line.startsWith("play")) { - // In lz-genmove_analyze - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.board.place(line.substring(5).trim()); - } - isThinking = false; - - } else if (Lizzie.frame != null && line.startsWith("=")) { - if (printCommunication) { - System.out.print(line); - } - String[] params = line.trim().split(" "); - currentCmdNum = Integer.parseInt(params[0].substring(1).trim()); - - if (params.length == 1) return; - - - if (isSettingHandicap) { - for (int i = 2; i < params.length; i++) { - int[] coordinates = Lizzie.board.convertNameToCoordinates(params[i]); - Lizzie.board.getHistory().setStone(coordinates, Stone.BLACK); - } - isSettingHandicap = false; - } else if (isThinking && !isPondering) { - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.board.place(params[1]); - isThinking = false; - } - } else if (isCheckingVersion) { - String[] ver = params[1].split("\\."); - int minor = Integer.parseInt(ver[1]); - // Gtp support added in version 15 - if (minor < 15) { - JOptionPane.showMessageDialog(Lizzie.frame, "Lizzie requires version 0.15 or later of Leela Zero for analysis (found " + params[1] + ")"); - } - isCheckingVersion = false; - } - } - } + return !currentNetworkHash.equals(bestNetworkHash); } - - /** - * Parse a move-data line of Leelaz output - * - * @param line output line - */ - private void parseMoveDataLine(String line) { - line = line.trim(); - // ignore passes, and only accept lines that start with a coordinate letter - if (line.length() > 0 && Character.isLetter(line.charAt(0)) && !line.startsWith("pass")) { - if (!(Lizzie.frame != null && Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack != Lizzie.board.getData().blackToPlay)) { - try { - bestMovesTemp.add(new MoveData(line)); - } catch (ArrayIndexOutOfBoundsException e) { - // this is very rare but is possible. ignore - } - } - } + } + + /** Initializes the input and output streams */ + private void initializeStreams() { + inputStream = new BufferedInputStream(process.getInputStream()); + outputStream = new BufferedOutputStream(process.getOutputStream()); + } + + private void parseInfo(String line) { + bestMoves = new ArrayList<>(); + String[] variations = line.split(" info "); + for (String var : variations) { + bestMoves.add(MoveData.fromInfo(var)); } - - /** - * Continually reads and processes output from leelaz - */ - private void read() { + } + + /** + * Parse a line of Leelaz output + * + * @param line output line + */ + private void parseLine(String line) { + synchronized (this) { + if (line.startsWith("komi=")) { try { - int c; - StringBuilder line = new StringBuilder(); - while ((c = inputStream.read()) != -1) { - line.append((char) c); - if ((c == '\n')) { - parseLine(line.toString()); - line = new StringBuilder(); - } - } - // this line will be reached when Leelaz shuts down - System.out.println("Leelaz process ended."); - - shutdown(); - System.exit(-1); - } catch (IOException e) { - e.printStackTrace(); - System.exit(-1); + dynamicKomi = Float.parseFloat(line.substring("komi=".length()).trim()); + } catch (NumberFormatException nfe) { + dynamicKomi = Float.NaN; } - } + } else if (line.startsWith("opp_komi=")) { + try { + dynamicOppKomi = Float.parseFloat(line.substring("opp_komi=".length()).trim()); + } catch (NumberFormatException nfe) { + dynamicOppKomi = Float.NaN; + } + } else if (line.equals("\n")) { + // End of response + } else if (line.startsWith("info")) { + isLoaded = true; + // Clear switching prompt + switching = false; + // Display engine command in the title + if (Lizzie.frame != null) Lizzie.frame.updateTitle(); + if (isResponseUpToDate()) { + // This should not be stale data when the command number match + parseInfo(line.substring(5)); + notifyBestMoveListeners(); + if (Lizzie.frame != null) Lizzie.frame.repaint(); + // don't follow the maxAnalyzeTime rule if we are in analysis mode + if (System.currentTimeMillis() - startPonderTime > maxAnalyzeTimeMillis + && !Lizzie.board.inAnalysisMode()) { + togglePonder(); + } + } + } else if (line.contains(" -> ")) { + isLoaded = true; + if (isResponseUpToDate()) { + bestMoves.add(MoveData.fromSummary(line)); + if (Lizzie.frame != null) Lizzie.frame.repaint(); + } + } else if (line.startsWith("play")) { + // In lz-genmove_analyze + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.board.place(line.substring(5).trim()); + } + isThinking = false; - /** - * Sends a command for leelaz to execute - * - * @param command a GTP command containing no newline characters - */ - public void sendCommand(String command) { - command = cmdNumber + " " + command; - cmdNumber++; + } else if (Lizzie.frame != null && (line.startsWith("=") || line.startsWith("?"))) { if (printCommunication) { - System.out.printf("> %s\n", command); + System.out.print(line); } - if (command.startsWith("fixed_handicap")) - isSettingHandicap = true; - try { - outputStream.write((command + "\n").getBytes()); - outputStream.flush(); - } catch (IOException e) { - e.printStackTrace(); + String[] params = line.trim().split(" "); + currentCmdNum = Integer.parseInt(params[0].substring(1).trim()); + + trySendCommandFromQueue(); + + if (line.startsWith("?") || params.length == 1) return; + + if (isSettingHandicap) { + for (int i = 1; i < params.length; i++) { + int[] coordinates = Lizzie.board.convertNameToCoordinates(params[i]); + Lizzie.board.getHistory().setStone(coordinates, Stone.BLACK); + } + isSettingHandicap = false; + } else if (isThinking && !isPondering) { + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.board.place(params[1]); + isThinking = false; + } + } else if (isCheckingVersion) { + String[] ver = params[1].split("\\."); + int minor = Integer.parseInt(ver[1]); + // Gtp support added in version 15 + if (minor < 15) { + JOptionPane.showMessageDialog( + Lizzie.frame, + "Lizzie requires version 0.15 or later of Leela Zero for analysis (found " + + params[1] + + ")"); + } + isCheckingVersion = false; } + } } - - /** - * @param color color of stone to play - * @param move coordinate of the coordinate - */ - public void playMove(Stone color, String move) { - synchronized (this) { - String colorString; - switch (color) { - case BLACK: - colorString = "B"; - break; - case WHITE: - colorString = "W"; - break; - default: - throw new IllegalArgumentException("The stone color must be B or W, but was " + color.toString()); - } - - sendCommand("play " + colorString + " " + move); - bestMoves = new ArrayList<>(); - - if (isPondering && !Lizzie.frame.isPlayingAgainstLeelaz) - ponder(); + } + + /** + * Parse a move-data line of Leelaz output + * + * @param line output line + */ + private void parseMoveDataLine(String line) { + line = line.trim(); + // ignore passes, and only accept lines that start with a coordinate letter + if (line.length() > 0 && Character.isLetter(line.charAt(0)) && !line.startsWith("pass")) { + if (!(Lizzie.frame != null + && Lizzie.frame.isPlayingAgainstLeelaz + && Lizzie.frame.playerIsBlack != Lizzie.board.getData().blackToPlay)) { + try { + bestMovesTemp.add(MoveData.fromInfo(line)); + } catch (ArrayIndexOutOfBoundsException e) { + // this is very rare but is possible. ignore } + } } - - public void genmove(String color) { - String command = "genmove " + color; - /* - * We don't support displaying this while playing, so no reason to request it (for now) - if (isPondering) { - command = "lz-genmove_analyze " + color + " 10"; - }*/ - sendCommand(command); - isThinking = true; - isPondering = false; - } - - public void undo() { - synchronized (this) { - sendCommand("undo"); - bestMoves = new ArrayList<>(); - if (isPondering) - ponder(); + } + + /** Continually reads and processes output from leelaz */ + private void read() { + try { + int c; + StringBuilder line = new StringBuilder(); + while ((c = inputStream.read()) != -1) { + line.append((char) c); + if ((c == '\n')) { + parseLine(line.toString()); + line = new StringBuilder(); } + } + // this line will be reached when Leelaz shuts down + System.out.println("Leelaz process ended."); + + shutdown(); + // Do no exit for switching weights + // System.exit(-1); + } catch (IOException e) { + e.printStackTrace(); + System.exit(-1); } - - /** - * this initializes leelaz's pondering mode at its current position - */ - private void ponder() { - isPondering = true; - startPonderTime = System.currentTimeMillis(); - sendCommand("lz-analyze " + Lizzie.config.config.getJSONObject("leelaz").getInt("analyze-update-interval-centisec")); // until it responds to this, incoming ponder results are obsolete + } + + /** + * Sends a command to command queue for leelaz to execute + * + * @param command a GTP command containing no newline characters + */ + public void sendCommand(String command) { + synchronized (cmdQueue) { + String lastCommand = cmdQueue.peekLast(); + // For efficiency, delete unnecessary "lz-analyze" that will be stopped immediately + if (lastCommand != null && lastCommand.startsWith("lz-analyze")) { + cmdQueue.removeLast(); + } + cmdQueue.addLast(command); + trySendCommandFromQueue(); } - - public void togglePonder() { - isPondering = !isPondering; - if (isPondering) { - ponder(); - } else { - sendCommand("name"); // ends pondering - } + } + + /** Sends a command from command queue for leelaz to execute if it is ready */ + private void trySendCommandFromQueue() { + // Defer sending "lz-analyze" if leelaz is not ready yet. + // Though all commands should be deferred theoretically, + // only "lz-analyze" is differed here for fear of + // possible hang-up by missing response for some reason. + // cmdQueue can be replaced with a mere String variable in this case, + // but it is kept for future change of our mind. + synchronized (cmdQueue) { + String command = cmdQueue.peekFirst(); + if (command == null || (command.startsWith("lz-analyze") && !isResponseUpToDate())) { + return; + } + cmdQueue.removeFirst(); + sendCommandToLeelaz(command); } - - /** - * End the process - */ - public void shutdown() { - process.destroy(); + } + + /** + * Sends a command for leelaz to execute + * + * @param command a GTP command containing no newline characters + */ + private void sendCommandToLeelaz(String command) { + if (command.startsWith("fixed_handicap")) isSettingHandicap = true; + command = cmdNumber + " " + command; + cmdNumber++; + if (printCommunication) { + System.out.printf("> %s\n", command); } - - public List getBestMoves() { - synchronized (this) { - return bestMoves; - } + try { + outputStream.write((command + "\n").getBytes()); + outputStream.flush(); + } catch (IOException e) { + e.printStackTrace(); } - - public boolean isPondering() { - return isPondering; + } + + /** Check whether leelaz is responding to the last command */ + private boolean isResponseUpToDate() { + // Use >= instead of == for avoiding hang-up, though it cannot happen + return currentCmdNum >= cmdNumber - 1; + } + + /** + * @param color color of stone to play + * @param move coordinate of the coordinate + */ + public void playMove(Stone color, String move) { + synchronized (this) { + String colorString; + switch (color) { + case BLACK: + colorString = "B"; + break; + case WHITE: + colorString = "W"; + break; + default: + throw new IllegalArgumentException( + "The stone color must be B or W, but was " + color.toString()); + } + + sendCommand("play " + colorString + " " + move); + bestMoves = new ArrayList<>(); + + if (isPondering && !Lizzie.frame.isPlayingAgainstLeelaz) ponder(); } + } - public class WinrateStats { - public double maxWinrate; - public int totalPlayouts; - - public WinrateStats(double maxWinrate, int totalPlayouts) { - this.maxWinrate = maxWinrate; - this.totalPlayouts = totalPlayouts; - } + public void genmove(String color) { + String command = "genmove " + color; + /* + * We don't support displaying this while playing, so no reason to request it (for now) + if (isPondering) { + command = "lz-genmove_analyze " + color + " 10"; + }*/ + sendCommand(command); + isThinking = true; + isPondering = false; + } + + public void undo() { + synchronized (this) { + sendCommand("undo"); + bestMoves = new ArrayList<>(); + if (isPondering) ponder(); } + } + + /** This initializes leelaz's pondering mode at its current position */ + private void ponder() { + isPondering = true; + startPonderTime = System.currentTimeMillis(); + sendCommand( + "lz-analyze " + + Lizzie.config + .config + .getJSONObject("leelaz") + .getInt("analyze-update-interval-centisec")); // until it responds to this, incoming + // ponder results are obsolete + } + + public void togglePonder() { + isPondering = !isPondering; + if (isPondering) { + ponder(); + } else { + sendCommand("name"); // ends pondering + } + } - /* - * Return the best win rate and total number of playouts. - * If no analysis available, win rate is negative and playouts is 0. - */ - public WinrateStats getWinrateStats() { - WinrateStats stats = new WinrateStats(-100, 0); - - if (bestMoves != null && !bestMoves.isEmpty()) { - // we should match the Leelaz UCTNode get_eval, which is a weighted average - // copy the list to avoid concurrent modification exception... TODO there must be a better way - // (note the concurrent modification exception is very very rare) - final List moves = new ArrayList(bestMoves); - - // get the total number of playouts in moves - stats.totalPlayouts = moves.stream().reduce(0, - (Integer result, MoveData move) -> result + move.playouts, - (Integer a, Integer b) -> a + b); - - // set maxWinrate to the weighted average winrate of moves - stats.maxWinrate = moves.stream().reduce(0d, - (Double result, MoveData move) -> - result + move.winrate * move.playouts / stats.totalPlayouts, - (Double a, Double b) -> a + b); - } + /** End the process */ + public void shutdown() { + process.destroy(); + } - return stats; + public List getBestMoves() { + synchronized (this) { + return bestMoves; } + } - /* - * initializes the normalizing factor for winrate_to_handicap_stones conversion. - */ - public void estimatePassWinrate() { - // we use A1 instead of pass, because valuenetwork is more accurate for A1 on empty board than a pass. - // probably the reason for higher accuracy is that networks have randomness which produces occasionally A1 as first move, but never pass. - // for all practical purposes, A1 should equal pass for the value it provides, hence good replacement. - // this way we avoid having to run lots of playouts for accurate winrate for pass. - playMove(Stone.BLACK, "A1"); - togglePonder(); - WinrateStats stats = getWinrateStats(); - - // we could use a timelimit or higher minimum playouts to get a more accurate measurement. - while (stats.totalPlayouts < 1) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - throw new Error(e); - } - stats = getWinrateStats(); - } - mHandicapWinrate = stats.maxWinrate; - togglePonder(); - undo(); - Lizzie.board.clear(); + public String getDynamicKomi() { + if (Float.isNaN(dynamicKomi) || Float.isNaN(dynamicOppKomi)) { + return null; } + return String.format("%.1f / %.1f", dynamicKomi, dynamicOppKomi); + } - public static double mHandicapWinrate = 25; - - /** - * Convert winrate to handicap stones, by normalizing winrate by first move pass winrate (one stone handicap). - */ - public static double winrateToHandicap(double pWinrate) { - // we assume each additional handicap lowers winrate by fixed percentage. - // this is pretty accurate for human handicap games at least. - // also this kind of property is a requirement for handicaps to determined based on rank difference. + public boolean isPondering() { + return isPondering; + } - // lets convert the 0%-50% range and 100%-50% from both the move and and pass into range of 0-1 - double moveWinrateSymmetric = 1 - Math.abs(1 - (pWinrate / 100) * 2); - double passWinrateSymmetric = 1 - Math.abs(1 - (mHandicapWinrate / 100) * 2); + public class WinrateStats { + public double maxWinrate; + public int totalPlayouts; - // convert the symmetric move winrate into correctly scaled log scale, so that winrate of passWinrate equals 1 handicap. - double handicapSymmetric = Math.log(moveWinrateSymmetric) / Math.log(passWinrateSymmetric); - - // make it negative if we had low winrate below 50. - return Math.signum(pWinrate - 50) * handicapSymmetric; + public WinrateStats(double maxWinrate, int totalPlayouts) { + this.maxWinrate = maxWinrate; + this.totalPlayouts = totalPlayouts; } - - public synchronized void addListener(LeelazListener listener) { - listeners.add(listener); + } + + /* + * Return the best win rate and total number of playouts. + * If no analysis available, win rate is negative and playouts is 0. + */ + public WinrateStats getWinrateStats() { + WinrateStats stats = new WinrateStats(-100, 0); + + if (bestMoves != null && !bestMoves.isEmpty()) { + // we should match the Leelaz UCTNode get_eval, which is a weighted average + // copy the list to avoid concurrent modification exception... TODO there must be a better way + // (note the concurrent modification exception is very very rare) + final List moves = new ArrayList(bestMoves); + + // get the total number of playouts in moves + int totalPlayouts = moves.stream().mapToInt(move -> move.playouts).sum(); + stats.totalPlayouts = totalPlayouts; + + // set maxWinrate to the weighted average winrate of moves + stats.maxWinrate = + moves.stream().mapToDouble(move -> move.winrate * move.playouts / totalPlayouts).sum(); } - // Beware, due to race conditions, bestMoveNotification can be called once even after item is removed - // with removeListener - public synchronized void removeListener(LeelazListener listener) { - listeners.remove(listener); + return stats; + } + + /* + * initializes the normalizing factor for winrate_to_handicap_stones conversion. + */ + public void estimatePassWinrate() { + // we use A1 instead of pass, because valuenetwork is more accurate for A1 on empty board than a + // pass. + // probably the reason for higher accuracy is that networks have randomness which produces + // occasionally A1 as first move, but never pass. + // for all practical purposes, A1 should equal pass for the value it provides, hence good + // replacement. + // this way we avoid having to run lots of playouts for accurate winrate for pass. + playMove(Stone.BLACK, "A1"); + togglePonder(); + WinrateStats stats = getWinrateStats(); + + // we could use a timelimit or higher minimum playouts to get a more accurate measurement. + while (stats.totalPlayouts < 1) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new Error(e); + } + stats = getWinrateStats(); } - - private synchronized void notifyBestMoveListeners() { - for (LeelazListener listener : listeners) { - listener.bestMoveNotification(bestMoves); - } + mHandicapWinrate = stats.maxWinrate; + togglePonder(); + undo(); + Lizzie.board.clear(); + } + + public static double mHandicapWinrate = 25; + + /** + * Convert winrate to handicap stones, by normalizing winrate by first move pass winrate (one + * stone handicap). + */ + public static double winrateToHandicap(double pWinrate) { + // we assume each additional handicap lowers winrate by fixed percentage. + // this is pretty accurate for human handicap games at least. + // also this kind of property is a requirement for handicaps to determined based on rank + // difference. + + // lets convert the 0%-50% range and 100%-50% from both the move and and pass into range of 0-1 + double moveWinrateSymmetric = 1 - Math.abs(1 - (pWinrate / 100) * 2); + double passWinrateSymmetric = 1 - Math.abs(1 - (mHandicapWinrate / 100) * 2); + + // convert the symmetric move winrate into correctly scaled log scale, so that winrate of + // passWinrate equals 1 handicap. + double handicapSymmetric = Math.log(moveWinrateSymmetric) / Math.log(passWinrateSymmetric); + + // make it negative if we had low winrate below 50. + return Math.signum(pWinrate - 50) * handicapSymmetric; + } + + public synchronized void addListener(LeelazListener listener) { + listeners.add(listener); + } + + // Beware, due to race conditions, bestMoveNotification can be called once even after item is + // removed + // with removeListener + public synchronized void removeListener(LeelazListener listener) { + listeners.remove(listener); + } + + private synchronized void notifyBestMoveListeners() { + for (LeelazListener listener : listeners) { + listener.bestMoveNotification(bestMoves); } + } - public boolean isLoaded() { - return isLoaded; - } + public boolean isLoaded() { + return isLoaded; + } + + public String currentWeight() { + return currentWeight; + } + + public boolean switching() { + return switching; + } + + public int currentEngineN() { + return currentEngineN; + } + + public String engineCommand() { + return this.engineCommand; + } } diff --git a/src/main/java/featurecat/lizzie/analysis/LeelazListener.java b/src/main/java/featurecat/lizzie/analysis/LeelazListener.java index 51e81a206..a07789c8b 100644 --- a/src/main/java/featurecat/lizzie/analysis/LeelazListener.java +++ b/src/main/java/featurecat/lizzie/analysis/LeelazListener.java @@ -3,5 +3,5 @@ import java.util.List; public interface LeelazListener { - void bestMoveNotification(List bestMoves); + void bestMoveNotification(List bestMoves); } diff --git a/src/main/java/featurecat/lizzie/analysis/MoveData.java b/src/main/java/featurecat/lizzie/analysis/MoveData.java index 0ba3ef154..988182925 100644 --- a/src/main/java/featurecat/lizzie/analysis/MoveData.java +++ b/src/main/java/featurecat/lizzie/analysis/MoveData.java @@ -3,35 +3,74 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -/** - * holds the data from Leelaz's pondering mode - */ -public class MoveData implements Comparable { - public String coordinate; - public int playouts; - public double winrate; - public int order; - public List variation; +/** Holds the data from Leelaz's pondering mode */ +public class MoveData { + public String coordinate; + public int playouts; + public double winrate; + public List variation; - /** - * Parses a leelaz ponder output line - * @param line line of ponder output - */ - public MoveData(String line) throws ArrayIndexOutOfBoundsException { - String[] data = line.trim().split(" "); + private MoveData() {} - // Todo: Proper tag parsing in case gtp protocol is extended(?)/changed - coordinate = data[1]; - playouts = Integer.parseInt(data[3]); - winrate = Integer.parseInt(data[5])/100.0; - order = Integer.parseInt(data[7]); + /** + * Parses a leelaz ponder output line. For example: + * + *

info move R5 visits 38 winrate 5404 order 0 pv R5 Q5 R6 S4 Q10 C3 D3 C4 C6 C5 D5 + * + * @param line line of ponder output + */ + public static MoveData fromInfo(String line) throws ArrayIndexOutOfBoundsException { + MoveData result = new MoveData(); + String[] data = line.trim().split(" "); - variation = new ArrayList<>(Arrays.asList(data)); - variation = variation.subList(9, variation.size()); + // Todo: Proper tag parsing in case gtp protocol is extended(?)/changed + for (int i = 0; i < data.length; i++) { + String key = data[i]; + if (key.equals("pv")) { + // Read variation to the end of line + result.variation = new ArrayList<>(Arrays.asList(data)); + result.variation = result.variation.subList(i + 1, data.length); + break; + } else { + String value = data[++i]; + if (key.equals("move")) { + result.coordinate = value; + } + if (key.equals("visits")) { + result.playouts = Integer.parseInt(value); + } + if (key.equals("winrate")) { + result.winrate = Integer.parseInt(value) / 100.0; + } + } } + return result; + } - public int compareTo(MoveData b) { - return order - b.order; + /** + * Parses a leelaz summary output line. For example: + * + *

P16 -> 4 (V: 50.94%) (N: 5.79%) PV: P16 N18 R5 Q5 + * + * @param line line of summary output + */ + public static MoveData fromSummary(String summary) { + Matcher match = summaryPattern.matcher(summary.trim()); + if (!match.matches()) { + throw new IllegalArgumentException("Unexpected summary format: " + summary); + } else { + MoveData result = new MoveData(); + result.coordinate = match.group(1); + result.playouts = Integer.parseInt(match.group(2)); + result.winrate = Double.parseDouble(match.group(3)); + result.variation = Arrays.asList(match.group(4).split(" ")); + return result; } + } + + private static Pattern summaryPattern = + Pattern.compile("^ *(\\w\\d*) -> *(\\d+) \\(V: ([^%)]+)%\\) \\([^\\)]+\\) PV: (.+).*$"); } diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 8a85a4e51..efff7f5a5 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -1,1005 +1,1149 @@ package featurecat.lizzie.gui; -import org.json.JSONArray; -import org.json.JSONObject; +import static java.awt.RenderingHints.*; +import static java.awt.image.BufferedImage.TYPE_INT_ARGB; +import static java.lang.Math.log; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.Math.round; + import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.Branch; import featurecat.lizzie.analysis.MoveData; -import featurecat.lizzie.plugin.PluginManager; import featurecat.lizzie.rules.Board; import featurecat.lizzie.rules.BoardHistoryNode; import featurecat.lizzie.rules.Stone; import featurecat.lizzie.rules.Zobrist; - import java.awt.*; import java.awt.font.TextAttribute; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; +import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; - -import featurecat.lizzie.theme.DefaultTheme; -import featurecat.lizzie.theme.ITheme; +import javax.imageio.ImageIO; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; public class BoardRenderer { - private static final double MARGIN = 0.03; // percentage of the boardLength to offset before drawing black lines - private static final double MARGIN_WITH_COORDINATES = 0.06; - private static final double STARPOINT_DIAMETER = 0.015; + // Percentage of the boardLength to offset before drawing black lines + private static final double MARGIN = 0.03; + private static final double MARGIN_WITH_COORDINATES = 0.06; + private static final double STARPOINT_DIAMETER = 0.015; - private int x, y; - private int boardLength; + private int x, y; + private int boardLength; - private JSONObject uiConfig; + private JSONObject uiConfig, uiPersist; + private int scaledMargin, availableLength, squareLength, stoneRadius; + private Branch branch; + private List bestMoves; - private int scaledMargin, availableLength, squareLength, stoneRadius; - private Branch branch; - private List bestMoves; + private BufferedImage cachedBackgroundImage = null; + private boolean cachedBackgroundImageHasCoordinatesEnabled = false; + private int cachedX, cachedY; - private BufferedImage cachedBackgroundImage = null; - private boolean cachedBackgroundImageHasCoordinatesEnabled = false; - private int cachedX, cachedY; + private BufferedImage cachedStonesImage = null; + private BufferedImage cachedBoardImage = null; + private BufferedImage cachedWallpaperImage = null; + private BufferedImage cachedStonesShadowImage = null; + private Zobrist cachedZhash = new Zobrist(); // defaults to an empty board - private BufferedImage cachedStonesImage = null; - private BufferedImage cachedStonesShadowImage = null; - private Zobrist cachedZhash = new Zobrist(); // defaults to an empty board + private BufferedImage cachedBlackStoneImage = null; + private BufferedImage cachedWhiteStoneImage = null; - private BufferedImage branchStonesImage = null; - private BufferedImage branchStonesShadowImage = null; + private BufferedImage branchStonesImage = null; + private BufferedImage branchStonesShadowImage = null; - private boolean lastInScoreMode = false; + private boolean lastInScoreMode = false; - public ITheme theme; - public List variation; + public List variation; - // special values of displayedBranchLength - public static final int SHOW_RAW_BOARD = -1; - public static final int SHOW_NORMAL_BOARD = -2; + // special values of displayedBranchLength + public static final int SHOW_RAW_BOARD = -1; + public static final int SHOW_NORMAL_BOARD = -2; - private int displayedBranchLength = SHOW_NORMAL_BOARD; - private int cachedDisplayedBranchLength = SHOW_RAW_BOARD; - private boolean showingBranch = false; - private boolean isMainBoard = false; + private int displayedBranchLength = SHOW_NORMAL_BOARD; + private int cachedDisplayedBranchLength = SHOW_RAW_BOARD; + private boolean showingBranch = false; + private boolean isMainBoard = false; - public BoardRenderer(boolean isMainBoard) { - uiConfig = Lizzie.config.config.getJSONObject("ui"); - theme = ITheme.loadTheme(uiConfig.getString("theme")); - if (theme == null) { - theme = new DefaultTheme(); - } - this.isMainBoard = isMainBoard; - } + private int maxAlpha = 240; - /** - * Draw a go board - */ - public void draw(Graphics2D g) { - if (Lizzie.frame == null || Lizzie.board == null) - return; - - setupSizeParameters(); - -// Stopwatch timer = new Stopwatch(); - drawBackground(g); -// timer.lap("background"); - drawStones(); -// timer.lap("stones"); - if (Lizzie.board.inScoreMode() && isMainBoard) { - drawScore(g); - } else { - drawBranch(); - } -// timer.lap("branch"); - - renderImages(g); -// timer.lap("rendering images"); - - if (!isMainBoard) { - drawMoveNumbers(g); - return; - } - - if (!isShowingRawBoard()) { - drawMoveNumbers(g); -// timer.lap("movenumbers"); - if (!Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.config.showBestMoves) - drawLeelazSuggestions(g); - - if (Lizzie.config.showNextMoves) { - drawNextMoves(g); - } - } - - PluginManager.onDraw(g); -// timer.lap("leelaz"); - -// timer.print(); + public BoardRenderer(boolean isMainBoard) { + uiConfig = Lizzie.config.config.getJSONObject("ui"); + uiPersist = Lizzie.config.persisted.getJSONObject("ui-persist"); + try { + maxAlpha = uiPersist.getInt("max-alpha"); + } catch (JSONException e) { } - - /** - * Return the best move of Leelaz's suggestions - * - * @return the coordinate name of the best move - */ - public String bestMoveCoordinateName() { - if (bestMoves == null || bestMoves.size() == 0) { - return null; - } else { - return bestMoves.get(0).coordinate; - } + this.isMainBoard = isMainBoard; + } + + /** Draw a go board */ + public void draw(Graphics2D g) { + if (Lizzie.frame == null || Lizzie.board == null) return; + + setupSizeParameters(); + + // Stopwatch timer = new Stopwatch(); + drawGoban(g); + // timer.lap("background"); + drawStones(); + // timer.lap("stones"); + if (Lizzie.board.inScoreMode() && isMainBoard) { + drawScore(g); + } else { + drawBranch(); } + // timer.lap("branch"); - /** - * Calculate good values for boardLength, scaledMargin, availableLength, and squareLength - */ - private void setupSizeParameters() { - int originalBoardLength = boardLength; + renderImages(g); + // timer.lap("rendering images"); - int[] calculatedPixelMargins = calculatePixelMargins(); - boardLength = calculatedPixelMargins[0]; - scaledMargin = calculatedPixelMargins[1]; - availableLength = calculatedPixelMargins[2]; - - squareLength = calculateSquareLength(availableLength); - stoneRadius = squareLength / 2 - 1; - - // re-center board - setLocation(x + (originalBoardLength - boardLength) / 2, y + (originalBoardLength - boardLength) / 2); + if (!isMainBoard) { + drawMoveNumbers(g); + return; } - /** - * Draw the green background and go board with lines. We cache the image for a performance boost. - */ - private void drawBackground(Graphics2D g0) { - // draw the cached background image if frame size changes - if (cachedBackgroundImage == null || cachedBackgroundImage.getWidth() != Lizzie.frame.getWidth() || - cachedBackgroundImage.getHeight() != Lizzie.frame.getHeight() || - cachedX != x || cachedY != y || - cachedBackgroundImageHasCoordinatesEnabled != showCoordinates()) { - - cachedBackgroundImage = new BufferedImage(Lizzie.frame.getWidth(), Lizzie.frame.getHeight(), - BufferedImage.TYPE_INT_ARGB); - Graphics2D g = cachedBackgroundImage.createGraphics(); - - // draw the wooden background - drawWoodenBoard(g); + if (!isShowingRawBoard()) { + drawMoveNumbers(g); + // timer.lap("movenumbers"); + if (!Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.config.showBestMoves) + drawLeelazSuggestions(g); - // draw the lines - g.setColor(Color.BLACK); - for (int i = 0; i < Board.BOARD_SIZE; i++) { - g.drawLine(x + scaledMargin, y + scaledMargin + squareLength * i, - x + scaledMargin + availableLength - 1, y + scaledMargin + squareLength * i); - } - for (int i = 0; i < Board.BOARD_SIZE; i++) { - g.drawLine(x + scaledMargin + squareLength * i, y + scaledMargin, - x + scaledMargin + squareLength * i, y + scaledMargin + availableLength - 1); - } - - // draw the star points - drawStarPoints(g); - - // draw coordinates if enabled - if (showCoordinates()) { - g.setColor(Color.BLACK); - String alphabet = "ABCDEFGHJKLMNOPQRST"; - for (int i = 0; i < Board.BOARD_SIZE; i++) { - drawString(g, x + scaledMargin + squareLength * i, y + scaledMargin / 2, LizzieFrame.OpenSansRegularBase, "" + alphabet.charAt(i), stoneRadius * 4 / 5, stoneRadius); - drawString(g, x + scaledMargin + squareLength * i, y - scaledMargin / 2 + boardLength, LizzieFrame.OpenSansRegularBase, "" + alphabet.charAt(i), stoneRadius * 4 / 5, stoneRadius); - } - for (int i = 0; i < Board.BOARD_SIZE; i++) { - drawString(g, x + scaledMargin / 2, y + scaledMargin + squareLength * i, LizzieFrame.OpenSansRegularBase, "" + (Board.BOARD_SIZE - i), stoneRadius * 4 / 5, stoneRadius); - drawString(g, x - scaledMargin / 2 + +boardLength, y + scaledMargin + squareLength * i, LizzieFrame.OpenSansRegularBase, "" + (Board.BOARD_SIZE - i), stoneRadius * 4 / 5, stoneRadius); - } - } - cachedBackgroundImageHasCoordinatesEnabled = showCoordinates(); - g.dispose(); - } - - g0.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - g0.drawImage(cachedBackgroundImage, 0, 0, null); - cachedX = x; - cachedY = y; + if (Lizzie.config.showNextMoves) { + drawNextMoves(g); + } } - /** - * Draw the star points on the board, according to board size - * - * @param g graphics2d object to draw - */ - private void drawStarPoints(Graphics2D g) { - if (Board.BOARD_SIZE == 9) { - drawStarPoints9x9(g); - } else if (Board.BOARD_SIZE == 13) { - drawStarPoints13x13(g); - } else { - drawStarPoints19x19(g); - } + // timer.lap("leelaz"); + + // timer.print(); + } + + /** + * Return the best move of Leelaz's suggestions + * + * @return the coordinate name of the best move + */ + public String bestMoveCoordinateName() { + if (bestMoves == null || bestMoves.size() == 0) { + return null; + } else { + return bestMoves.get(0).coordinate; } - - private void drawStarPoints19x19(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; - final int NUM_STARPOINTS = 3; - final int STARPOINT_EDGE_OFFSET = 3; - final int STARPOINT_GRID_DISTANCE = 6; - for (int i = 0; i < NUM_STARPOINTS; i++) { - for (int j = 0; j < NUM_STARPOINTS; j++) { - int centerX = x + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * i); - int centerY = y + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * j); - fillCircle(g, centerX, centerY, starPointRadius); - } + } + + /** Calculate good values for boardLength, scaledMargin, availableLength, and squareLength */ + private void setupSizeParameters() { + int boardLength0 = boardLength; + + int[] calculatedPixelMargins = calculatePixelMargins(); + boardLength = calculatedPixelMargins[0]; + scaledMargin = calculatedPixelMargins[1]; + availableLength = calculatedPixelMargins[2]; + + squareLength = calculateSquareLength(availableLength); + stoneRadius = squareLength / 2 - 1; + + // re-center board + setLocation(x + (boardLength0 - boardLength) / 2, y + (boardLength0 - boardLength) / 2); + } + + /** + * Draw the green background and go board with lines. We cache the image for a performance boost. + */ + private void drawGoban(Graphics2D g0) { + int width = Lizzie.frame.getWidth(); + int height = Lizzie.frame.getHeight(); + + // draw the cached background image if frame size changes + if (cachedBackgroundImage == null + || cachedBackgroundImage.getWidth() != width + || cachedBackgroundImage.getHeight() != height + || cachedX != x + || cachedY != y + || cachedBackgroundImageHasCoordinatesEnabled != showCoordinates()) { + + cachedBackgroundImage = new BufferedImage(width, height, TYPE_INT_ARGB); + Graphics2D g = cachedBackgroundImage.createGraphics(); + + // draw the wooden background + drawWoodenBoard(g); + + // draw the lines + g.setColor(Color.BLACK); + for (int i = 0; i < Board.BOARD_SIZE; i++) { + g.drawLine( + x + scaledMargin, + y + scaledMargin + squareLength * i, + x + scaledMargin + availableLength - 1, + y + scaledMargin + squareLength * i); + } + for (int i = 0; i < Board.BOARD_SIZE; i++) { + g.drawLine( + x + scaledMargin + squareLength * i, + y + scaledMargin, + x + scaledMargin + squareLength * i, + y + scaledMargin + availableLength - 1); + } + + // draw the star points + drawStarPoints(g); + + // draw coordinates if enabled + if (showCoordinates()) { + g.setColor(Color.BLACK); + String alphabet = "ABCDEFGHJKLMNOPQRST"; + for (int i = 0; i < Board.BOARD_SIZE; i++) { + drawString( + g, + x + scaledMargin + squareLength * i, + y + scaledMargin / 2, + LizzieFrame.OpenSansRegularBase, + "" + alphabet.charAt(i), + stoneRadius * 4 / 5, + stoneRadius); + drawString( + g, + x + scaledMargin + squareLength * i, + y - scaledMargin / 2 + boardLength, + LizzieFrame.OpenSansRegularBase, + "" + alphabet.charAt(i), + stoneRadius * 4 / 5, + stoneRadius); } - } - - private void drawStarPoints13x13(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; - final int NUM_STARPOINTS = 2; - final int STARPOINT_EDGE_OFFSET = 3; - final int STARPOINT_GRID_DISTANCE = 6; - for (int i = 0; i < NUM_STARPOINTS; i++) { - for (int j = 0; j < NUM_STARPOINTS; j++) { - int centerX = x + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * i); - int centerY = y + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * j); - fillCircle(g, centerX, centerY, starPointRadius); - } + for (int i = 0; i < Board.BOARD_SIZE; i++) { + drawString( + g, + x + scaledMargin / 2, + y + scaledMargin + squareLength * i, + LizzieFrame.OpenSansRegularBase, + "" + (Board.BOARD_SIZE - i), + stoneRadius * 4 / 5, + stoneRadius); + drawString( + g, + x - scaledMargin / 2 + +boardLength, + y + scaledMargin + squareLength * i, + LizzieFrame.OpenSansRegularBase, + "" + (Board.BOARD_SIZE - i), + stoneRadius * 4 / 5, + stoneRadius); } - - // Draw center - int centerX = x + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; - int centerY = y + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; - fillCircle(g, centerX, centerY, starPointRadius); + } + cachedBackgroundImageHasCoordinatesEnabled = showCoordinates(); + g.dispose(); } - private void drawStarPoints9x9(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; - final int NUM_STARPOINTS = 2; - final int STARPOINT_EDGE_OFFSET = 2; - final int STARPOINT_GRID_DISTANCE = 4; - for (int i = 0; i < NUM_STARPOINTS; i++) { - for (int j = 0; j < NUM_STARPOINTS; j++) { - int centerX = x + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * i); - int centerY = y + scaledMargin + squareLength * (STARPOINT_EDGE_OFFSET + STARPOINT_GRID_DISTANCE * j); - fillCircle(g, centerX, centerY, starPointRadius); - } - } - - // Draw center - int centerX = x + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; - int centerY = y + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; + g0.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF); + g0.drawImage(cachedBackgroundImage, 0, 0, null); + cachedX = x; + cachedY = y; + } + + /** + * Draw the star points on the board, according to board size + * + * @param g graphics2d object to draw + */ + private void drawStarPoints(Graphics2D g) { + if (Board.BOARD_SIZE == 9) { + drawStarPoints0(2, 2, 4, true, g); + } else if (Board.BOARD_SIZE == 13) { + drawStarPoints0(2, 3, 6, true, g); + } else { + drawStarPoints0(3, 3, 6, false, g); + } + } + + private void drawStarPoints0( + int nStarpoints, int edgeOffset, int gridDistance, boolean center, Graphics2D g) { + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + int starPointRadius = (int) (STARPOINT_DIAMETER * boardLength) / 2; + for (int i = 0; i < nStarpoints; i++) { + for (int j = 0; j < nStarpoints; j++) { + int centerX = x + scaledMargin + squareLength * (edgeOffset + gridDistance * i); + int centerY = y + scaledMargin + squareLength * (edgeOffset + gridDistance * j); fillCircle(g, centerX, centerY, starPointRadius); + } } - /** - * Draw the stones. We cache the image for a performance boost. - */ - private void drawStones() { - // draw a new image if frame size changes or board state changes - if (cachedStonesImage == null || cachedStonesImage.getWidth() != boardLength || - cachedStonesImage.getHeight() != boardLength || - cachedDisplayedBranchLength != displayedBranchLength || - !cachedZhash.equals(Lizzie.board.getData().zobrist) - || Lizzie.board.inScoreMode() || lastInScoreMode) { - - cachedStonesImage = new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); - cachedStonesShadowImage = new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = cachedStonesImage.createGraphics(); - Graphics2D gShadow = cachedStonesShadowImage.createGraphics(); - - // we need antialiasing to make the stones pretty. Java is a bit slow at antialiasing; that's why we want the cache - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - gShadow.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - for (int i = 0; i < Board.BOARD_SIZE; i++) { - for (int j = 0; j < Board.BOARD_SIZE; j++) { - int stoneX = scaledMargin + squareLength * i; - int stoneY = scaledMargin + squareLength * j; - drawStone(g, gShadow, stoneX, stoneY, Lizzie.board.getStones()[Board.getIndex(i, j)], i, j); - } - } - - cachedZhash = Lizzie.board.getData().zobrist.clone(); - cachedDisplayedBranchLength = displayedBranchLength; - g.dispose(); - gShadow.dispose(); - lastInScoreMode = false; + if (center) { + int centerX = x + scaledMargin + squareLength * gridDistance; + int centerY = y + scaledMargin + squareLength * gridDistance; + fillCircle(g, centerX, centerY, starPointRadius); + } + } + + /** Draw the stones. We cache the image for a performance boost. */ + private void drawStones() { + // draw a new image if frame size changes or board state changes + if (cachedStonesImage == null + || cachedStonesImage.getWidth() != boardLength + || cachedStonesImage.getHeight() != boardLength + || cachedDisplayedBranchLength != displayedBranchLength + || !cachedZhash.equals(Lizzie.board.getData().zobrist) + || Lizzie.board.inScoreMode() + || lastInScoreMode) { + + cachedStonesImage = new BufferedImage(boardLength, boardLength, TYPE_INT_ARGB); + cachedStonesShadowImage = new BufferedImage(boardLength, boardLength, TYPE_INT_ARGB); + Graphics2D g = cachedStonesImage.createGraphics(); + Graphics2D gShadow = cachedStonesShadowImage.createGraphics(); + + // we need antialiasing to make the stones pretty. Java is a bit slow at antialiasing; that's + // why we want the cache + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + gShadow.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + + for (int i = 0; i < Board.BOARD_SIZE; i++) { + for (int j = 0; j < Board.BOARD_SIZE; j++) { + int stoneX = scaledMargin + squareLength * i; + int stoneY = scaledMargin + squareLength * j; + drawStone( + g, gShadow, stoneX, stoneY, Lizzie.board.getStones()[Board.getIndex(i, j)], i, j); } - if (Lizzie.board.inScoreMode()) lastInScoreMode = true; + } + cachedZhash = Lizzie.board.getData().zobrist.clone(); + cachedDisplayedBranchLength = displayedBranchLength; + g.dispose(); + gShadow.dispose(); + lastInScoreMode = false; } - - /* - * Draw a white/black dot on territory and captured stones. Dame is drawn as red dot. - */ - private void drawScore(Graphics2D go) { - Graphics2D g = cachedStonesImage.createGraphics(); - Stone scorestones[] = Lizzie.board.scoreStones(); - int scoreRadius = stoneRadius / 4; - for (int i = 0; i < Board.BOARD_SIZE; i++) { - for (int j = 0; j < Board.BOARD_SIZE; j++) { - int stoneX = scaledMargin + squareLength * i; - int stoneY = scaledMargin + squareLength * j; - switch (scorestones[Board.getIndex(i, j)]) { - case WHITE_POINT: - case BLACK_CAPTURED: - g.setColor(Color.white); - fillCircle(g, stoneX, stoneY, scoreRadius); - break; - case BLACK_POINT: - case WHITE_CAPTURED: - g.setColor(Color.black); - fillCircle(g, stoneX, stoneY, scoreRadius); - break; - case DAME: - g.setColor(Color.red); - fillCircle(g, stoneX, stoneY, scoreRadius); - break; - } - } + if (Lizzie.board.inScoreMode()) lastInScoreMode = true; + } + + /* + * Draw a white/black dot on territory and captured stones. Dame is drawn as red dot. + */ + private void drawScore(Graphics2D go) { + Graphics2D g = cachedStonesImage.createGraphics(); + Stone scorestones[] = Lizzie.board.scoreStones(); + int scoreRadius = stoneRadius / 4; + for (int i = 0; i < Board.BOARD_SIZE; i++) { + for (int j = 0; j < Board.BOARD_SIZE; j++) { + int stoneX = scaledMargin + squareLength * i; + int stoneY = scaledMargin + squareLength * j; + switch (scorestones[Board.getIndex(i, j)]) { + case WHITE_POINT: + case BLACK_CAPTURED: + g.setColor(Color.white); + fillCircle(g, stoneX, stoneY, scoreRadius); + break; + case BLACK_POINT: + case WHITE_CAPTURED: + g.setColor(Color.black); + fillCircle(g, stoneX, stoneY, scoreRadius); + break; + case DAME: + g.setColor(Color.red); + fillCircle(g, stoneX, stoneY, scoreRadius); + break; } - g.dispose(); + } } + g.dispose(); + } + + /** Draw the 'ghost stones' which show a variation Leelaz is thinking about */ + private void drawBranch() { + showingBranch = false; + branchStonesImage = new BufferedImage(boardLength, boardLength, TYPE_INT_ARGB); + branchStonesShadowImage = new BufferedImage(boardLength, boardLength, TYPE_INT_ARGB); + branch = null; + + if (Lizzie.frame.isPlayingAgainstLeelaz || Lizzie.leelaz == null) { + return; + } + // calculate best moves and branch + bestMoves = Lizzie.leelaz.getBestMoves(); + branch = null; + variation = null; - /** - * Draw the 'ghost stones' which show a variation Leelaz is thinking about - */ - private void drawBranch() { - showingBranch = false; - branchStonesImage = new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); - branchStonesShadowImage = new BufferedImage(boardLength, boardLength, BufferedImage.TYPE_INT_ARGB); - branch = null; - - if (Lizzie.frame.isPlayingAgainstLeelaz || Lizzie.leelaz == null) { - return; - } - // calculate best moves and branch - bestMoves = Lizzie.leelaz.getBestMoves(); - branch = null; - variation = null; - - if (isMainBoard && (isShowingRawBoard() || !Lizzie.config.showBranch)) { - return; - } + if (isMainBoard && (isShowingRawBoard() || !Lizzie.config.showBranch)) { + return; + } - Graphics2D g = (Graphics2D) branchStonesImage.getGraphics(); - Graphics2D gShadow = (Graphics2D) branchStonesShadowImage.getGraphics(); + Graphics2D g = (Graphics2D) branchStonesImage.getGraphics(); + Graphics2D gShadow = (Graphics2D) branchStonesShadowImage.getGraphics(); - MoveData suggestedMove = (isMainBoard ? mouseHoveredMove() : getBestMove()); - if (suggestedMove == null) - return; - variation = suggestedMove.variation; - branch = new Branch(Lizzie.board, variation); + MoveData suggestedMove = (isMainBoard ? mouseOveredMove() : getBestMove()); + if (suggestedMove == null) return; + variation = suggestedMove.variation; + branch = new Branch(Lizzie.board, variation); - if (branch == null) - return; - showingBranch = true; + if (branch == null) return; + showingBranch = true; - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); - for (int i = 0; i < Board.BOARD_SIZE; i++) { - for (int j = 0; j < Board.BOARD_SIZE; j++) { - if (Lizzie.board.getData().stones[Board.getIndex(i,j)] != Stone.EMPTY) - continue; - if (branch.data.moveNumberList[Board.getIndex(i,j)] > maxBranchMoves()) - continue; + for (int i = 0; i < Board.BOARD_SIZE; i++) { + for (int j = 0; j < Board.BOARD_SIZE; j++) { + if (Lizzie.board.getData().stones[Board.getIndex(i, j)] != Stone.EMPTY) continue; + if (branch.data.moveNumberList[Board.getIndex(i, j)] > maxBranchMoves()) continue; - int stoneX = scaledMargin + squareLength * i; - int stoneY = scaledMargin + squareLength * j; + int stoneX = scaledMargin + squareLength * i; + int stoneY = scaledMargin + squareLength * j; - drawStone(g, gShadow, stoneX, stoneY, branch.data.stones[Board.getIndex(i, j)].unGhosted(), i, j); + drawStone( + g, gShadow, stoneX, stoneY, branch.data.stones[Board.getIndex(i, j)].unGhosted(), i, j); + } + } - } + g.dispose(); + gShadow.dispose(); + } + + private MoveData mouseOveredMove() { + if (Lizzie.frame.mouseOverCoordinate != null) { + for (int i = 0; i < bestMoves.size(); i++) { + MoveData move = bestMoves.get(i); + int[] coord = Board.convertNameToCoordinates(move.coordinate); + if (coord == null) { + continue; } - g.dispose(); - gShadow.dispose(); - } - - private MoveData mouseHoveredMove() { - if (Lizzie.frame.mouseHoverCoordinate != null) { - for (int i = 0; i < bestMoves.size(); i++) { - MoveData move = bestMoves.get(i); - int[] coord = Board.convertNameToCoordinates(move.coordinate); - if (coord == null) { - continue; - } - - if (coord[0] == Lizzie.frame.mouseHoverCoordinate[0] && coord[1] == Lizzie.frame.mouseHoverCoordinate[1]) { - return move; - } - } + if (Lizzie.frame.isMouseOver(coord[0], coord[1])) { + return move; } - return null; + } } - - private MoveData getBestMove() { - return bestMoves.isEmpty() ? null : bestMoves.get(0); + return null; + } + + private MoveData getBestMove() { + return bestMoves.isEmpty() ? null : bestMoves.get(0); + } + + /** render the shadows and stones in correct background-foreground order */ + private void renderImages(Graphics2D g) { + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF); + g.drawImage(cachedStonesShadowImage, x, y, null); + if (Lizzie.config.showBranch) { + g.drawImage(branchStonesShadowImage, x, y, null); } - - /** - * render the shadows and stones in correct background-foreground order - */ - private void renderImages(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - g.drawImage(cachedStonesShadowImage, x, y, null); - if (Lizzie.config.showBranch) { - g.drawImage(branchStonesShadowImage, x, y, null); - } - g.drawImage(cachedStonesImage, x, y, null); - if (Lizzie.config.showBranch) { - g.drawImage(branchStonesImage, x, y, null); - } + g.drawImage(cachedStonesImage, x, y, null); + if (Lizzie.config.showBranch) { + g.drawImage(branchStonesImage, x, y, null); + } + } + + /** Draw move numbers and/or mark the last played move */ + private void drawMoveNumbers(Graphics2D g) { + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + Board board = Lizzie.board; + int[] lastMove = branch == null ? board.getLastMove() : branch.data.lastMove; + if (!Lizzie.config.showMoveNumber && branch == null) { + if (lastMove != null) { + // mark the last coordinate + int lastMoveMarkerRadius = stoneRadius / 2; + int stoneX = x + scaledMargin + squareLength * lastMove[0]; + int stoneY = y + scaledMargin + squareLength * lastMove[1]; + + // set color to the opposite color of whatever is on the board + boolean isWhite = board.getStones()[Board.getIndex(lastMove[0], lastMove[1])].isWhite(); + g.setColor(isWhite ? Color.BLACK : Color.WHITE); + + drawCircle(g, stoneX, stoneY, lastMoveMarkerRadius); + } else if (lastMove == null && board.getData().moveNumber != 0 && !board.inScoreMode()) { + g.setColor( + board.getData().blackToPlay ? new Color(255, 255, 255, 150) : new Color(0, 0, 0, 150)); + g.fillOval( + x + boardLength / 2 - 4 * stoneRadius, + y + boardLength / 2 - 4 * stoneRadius, + stoneRadius * 8, + stoneRadius * 8); + g.setColor( + board.getData().blackToPlay ? new Color(0, 0, 0, 255) : new Color(255, 255, 255, 255)); + drawString( + g, + x + boardLength / 2, + y + boardLength / 2, + LizzieFrame.OpenSansRegularBase, + "pass", + stoneRadius * 4, + stoneRadius * 6); + } + + return; } - /** - * Draw move numbers and/or mark the last played move - */ - private void drawMoveNumbers(Graphics2D g) { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - int[] lastMove = branch == null ? Lizzie.board.getLastMove() : branch.data.lastMove; - if (!Lizzie.config.showMoveNumber && branch == null) { - if (lastMove != null) { - // mark the last coordinate - int lastMoveMarkerRadius = stoneRadius / 2; - int stoneX = x + scaledMargin + squareLength * lastMove[0]; - int stoneY = y + scaledMargin + squareLength * lastMove[1]; - - // set color to the opposite color of whatever is on the board - g.setColor(Lizzie.board.getStones()[Board.getIndex(lastMove[0], lastMove[1])].isWhite() ? - Color.BLACK : Color.WHITE); - drawCircle(g, stoneX, stoneY, lastMoveMarkerRadius); - } else if (lastMove == null && Lizzie.board.getData().moveNumber != 0 && !Lizzie.board.inScoreMode()) { - g.setColor(Lizzie.board.getData().blackToPlay ? new Color(255, 255, 255, 150) : new Color(0, 0, 0, 150)); - g.fillOval(x + boardLength / 2 - 4 * stoneRadius, y + boardLength / 2 - 4 * stoneRadius, stoneRadius * 8, stoneRadius * 8); - g.setColor(Lizzie.board.getData().blackToPlay ? new Color(0, 0, 0, 255) : new Color(255, 255, 255, 255)); - drawString(g, x + boardLength / 2, y + boardLength / 2, LizzieFrame.OpenSansRegularBase, "pass", stoneRadius * 4, stoneRadius * 6); - } + int[] moveNumberList = branch == null ? board.getMoveNumberList() : branch.data.moveNumberList; - return; - } + // Allow to display only last move number + int lastMoveNumber = branch == null ? board.getData().moveNumber : branch.data.moveNumber; + int onlyLastMoveNumber = Lizzie.config.uiConfig.optInt("only-last-move-number", 9999); - int[] moveNumberList = branch == null ? Lizzie.board.getMoveNumberList() : branch.data.moveNumberList; + for (int i = 0; i < Board.BOARD_SIZE; i++) { + for (int j = 0; j < Board.BOARD_SIZE; j++) { + int stoneX = x + scaledMargin + squareLength * i; + int stoneY = y + scaledMargin + squareLength * j; - for (int i = 0; i < Board.BOARD_SIZE; i++) { - for (int j = 0; j < Board.BOARD_SIZE; j++) { - int stoneX = x + scaledMargin + squareLength * i; - int stoneY = y + scaledMargin + squareLength * j; - - Stone stoneAtThisPoint = branch == null ? Lizzie.board.getStones()[Board.getIndex(i, j)] : - branch.data.stones[Board.getIndex(i, j)]; - - // don't write the move number if either: the move number is 0, or there will already be playout information written - if (moveNumberList[Board.getIndex(i, j)] > 0 && !(branch != null && Lizzie.frame.mouseHoverCoordinate != null && i == Lizzie.frame.mouseHoverCoordinate[0] && j == Lizzie.frame.mouseHoverCoordinate[1])) { - if (lastMove != null && i == lastMove[0] && j == lastMove[1]) - g.setColor(Color.RED.brighter());//stoneAtThisPoint.isBlack() ? Color.RED.brighter() : Color.BLUE.brighter()); - else { - // Draw white letters on black stones nomally. - // But use black letters for showing black moves without stones. - boolean reverse = (moveNumberList[Board.getIndex(i, j)] > maxBranchMoves()); - g.setColor(stoneAtThisPoint.isBlack() ^ reverse ? Color.WHITE : Color.BLACK); - } - - String moveNumberString = moveNumberList[Board.getIndex(i, j)] + ""; - drawString(g, stoneX, stoneY, LizzieFrame.OpenSansRegularBase, moveNumberString, (float) (stoneRadius * 1.4), (int) (stoneRadius * 1.4)); - } - } + // Allow to display only last move number + if (lastMoveNumber - moveNumberList[Board.getIndex(i, j)] >= onlyLastMoveNumber) { + continue; } - } - - /** - * Draw all of Leelaz's suggestions as colored stones with winrate/playout statistics overlayed - */ - private void drawLeelazSuggestions(Graphics2D g) { - if (Lizzie.leelaz == null) - return; - - final int MIN_ALPHA = 32; - final int MIN_ALPHA_TO_DISPLAY_TEXT = 64; - final int MAX_ALPHA = 240; - final double HUE_SCALING_FACTOR = 3.0; - final double ALPHA_SCALING_FACTOR = 5.0; - final float GREEN_HUE = Color.RGBtoHSB(0,1,0,null)[0]; - final float CYAN_HUE = Color.RGBtoHSB(0,1,1,null)[0]; - - if (!bestMoves.isEmpty()) { - - int maxPlayouts = 0; - double maxWinrate = 0; - for (MoveData move : bestMoves) { - if (move.playouts > maxPlayouts) - maxPlayouts = move.playouts; - if (move.winrate > maxWinrate) - maxWinrate = move.winrate; - } - - for (int i = 0; i < Board.BOARD_SIZE; i++) { - for (int j = 0; j < Board.BOARD_SIZE; j++) { - MoveData move = null; - - // this is inefficient but it looks better with shadows - for (MoveData m : bestMoves) { - int[] coord = Board.convertNameToCoordinates(m.coordinate); - // Handle passes - if (coord == null) { - continue; - } - if (coord[0] == i && coord[1] == j) { - move = m; - break; - } - } - - if (move == null) - continue; - - boolean isBestMove = bestMoves.get(0) == move; - - if (move.playouts == 0) // this actually can happen - continue; - - double percentPlayouts = (double) move.playouts / maxPlayouts; - - int[] coordinates = Board.convertNameToCoordinates(move.coordinate); - int suggestionX = x + scaledMargin + squareLength * coordinates[0]; - int suggestionY = y + scaledMargin + squareLength * coordinates[1]; - - - // 0 = Reddest hue - float hue = isBestMove ? CYAN_HUE : (float) (-GREEN_HUE * Math.max(0, Math.log(percentPlayouts) / HUE_SCALING_FACTOR + 1)); - float saturation = 0.75f; //saturation - float brightness = 0.85f; //brightness - int alpha = (int) (MIN_ALPHA + (MAX_ALPHA - MIN_ALPHA) * Math.max(0, Math.log(percentPlayouts) / - ALPHA_SCALING_FACTOR + 1)); -// if (uiConfig.getBoolean("shadows-enabled")) -// alpha = 255; - - Color hsbColor = Color.getHSBColor(hue, saturation, brightness); - Color color = new Color(hsbColor.getRed(), hsbColor.getBlue(), hsbColor.getGreen(), alpha); - - if (branch == null) { - drawShadow(g, suggestionX, suggestionY, true, (float) alpha / 255); - g.setColor(color); - fillCircle(g, suggestionX, suggestionY, stoneRadius); - } - - if (branch == null || (isBestMove && Lizzie.frame.mouseHoverCoordinate != null && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { - int strokeWidth = 1; - if (isBestMove != (move.winrate == maxWinrate)) { - strokeWidth = 2; - g.setColor(isBestMove ? Color.RED : Color.BLUE); - g.setStroke(new BasicStroke(strokeWidth)); - } else { - g.setColor(color.darker()); - } - drawCircle(g, suggestionX, suggestionY, stoneRadius - strokeWidth / 2); - g.setStroke(new BasicStroke(1)); - } - - - if (branch == null && alpha >= MIN_ALPHA_TO_DISPLAY_TEXT || (Lizzie.frame.mouseHoverCoordinate != null && coordinates[0] == Lizzie.frame.mouseHoverCoordinate[0] && coordinates[1] == Lizzie.frame.mouseHoverCoordinate[1])) { - double roundedWinrate = Math.round(move.winrate * 10) / 10.0; - if (uiConfig.getBoolean("win-rate-always-black") && !Lizzie.board.getData().blackToPlay) { - roundedWinrate = 100.0 - roundedWinrate; - } - g.setColor(Color.BLACK); - if (branch != null && Lizzie.board.getData().blackToPlay) - g.setColor(Color.WHITE); - - String text; - if (Lizzie.config.handicapInsteadOfWinrate) { - text=String.format("%.2f", Lizzie.leelaz.winrateToHandicap(move.winrate)); - } else { - text=String.format("%.1f", roundedWinrate); - } - - drawString(g, suggestionX, suggestionY, LizzieFrame.OpenSansSemiboldBase, Font.PLAIN, text, stoneRadius, stoneRadius * 1.5, 1); - drawString(g, suggestionX, suggestionY + stoneRadius * 2 / 5, LizzieFrame.OpenSansRegularBase, getPlayoutsString(move.playouts), (float) (stoneRadius * 0.8), stoneRadius * 1.4); - } - } - } - + int here = Board.getIndex(i, j); + Stone stoneHere = branch == null ? board.getStones()[here] : branch.data.stones[here]; + + // don't write the move number if either: the move number is 0, or there will already be + // playout information written + if (moveNumberList[Board.getIndex(i, j)] > 0 + && !(branch != null && Lizzie.frame.isMouseOver(i, j))) { + if (lastMove != null && i == lastMove[0] && j == lastMove[1]) + g.setColor(Color.RED.brighter()); // stoneHere.isBlack() ? Color.RED.brighter() : + // Color.BLUE.brighter()); + else { + // Draw white letters on black stones nomally. + // But use black letters for showing black moves without stones. + boolean reverse = (moveNumberList[Board.getIndex(i, j)] > maxBranchMoves()); + g.setColor(stoneHere.isBlack() ^ reverse ? Color.WHITE : Color.BLACK); + } + + String moveNumberString = moveNumberList[Board.getIndex(i, j)] + ""; + drawString( + g, + stoneX, + stoneY, + LizzieFrame.OpenSansRegularBase, + moveNumberString, + (float) (stoneRadius * 1.4), + (int) (stoneRadius * 1.4)); } + } } - - private void drawNextMoves(Graphics2D g) { - - List nexts = Lizzie.board.getHistory().getNexts(); - - for (int i = 0; i < nexts.size(); i++) { - int[] nextMove = nexts.get(i).getData().lastMove; - if (nextMove == null) continue; - if (Lizzie.board.getData().blackToPlay) { - g.setColor(Color.BLACK); - } else { - g.setColor(Color.WHITE); + } + + /** + * Draw all of Leelaz's suggestions as colored stones with winrate/playout statistics overlayed + */ + private void drawLeelazSuggestions(Graphics2D g) { + if (Lizzie.leelaz == null) return; + + int minAlpha = 32; + float hueFactor = 3.0f; + float alphaFactor = 5.0f; + float greenHue = Color.RGBtoHSB(0, 1, 0, null)[0]; + float cyanHue = Color.RGBtoHSB(0, 1, 1, null)[0]; + + if (!bestMoves.isEmpty()) { + + int maxPlayouts = 0; + double maxWinrate = 0; + for (MoveData move : bestMoves) { + if (move.playouts > maxPlayouts) maxPlayouts = move.playouts; + if (move.winrate > maxWinrate) maxWinrate = move.winrate; + } + + for (int i = 0; i < Board.BOARD_SIZE; i++) { + for (int j = 0; j < Board.BOARD_SIZE; j++) { + MoveData move = null; + + // this is inefficient but it looks better with shadows + for (MoveData m : bestMoves) { + int[] coord = Board.convertNameToCoordinates(m.coordinate); + // Handle passes + if (coord == null) { + continue; } - int moveX = x + scaledMargin + squareLength * nextMove[0]; - int moveY = y + scaledMargin + squareLength * nextMove[1]; - if (i == 0) { - g.setStroke(new BasicStroke(3.0f)); + if (coord[0] == i && coord[1] == j) { + move = m; + break; } - drawCircle(g, moveX, moveY, stoneRadius + 1); // slightly outside best move circle - if (i == 0) { - g.setStroke(new BasicStroke(1.0f)); + } + + if (move == null) continue; + + boolean isBestMove = bestMoves.get(0) == move; + boolean hasMaxWinrate = move.winrate == maxWinrate; + + if (move.playouts == 0) // this actually can happen + continue; + + float percentPlayouts = (float) move.playouts / maxPlayouts; + + int[] coordinates = Board.convertNameToCoordinates(move.coordinate); + int suggestionX = x + scaledMargin + squareLength * coordinates[0]; + int suggestionY = y + scaledMargin + squareLength * coordinates[1]; + + // 0 = Reddest hue + float logPlayouts = (float) log(percentPlayouts); + float otherHue = -greenHue * max(0, logPlayouts / hueFactor + 1); + float hue = isBestMove ? cyanHue : otherHue; + float saturation = 0.75f; + float brightness = 0.85f; + float alpha = (minAlpha + (maxAlpha - minAlpha) * max(0, logPlayouts / alphaFactor + 1)); + + Color hsbColor = Color.getHSBColor(hue, saturation, brightness); + Color color = + new Color(hsbColor.getRed(), hsbColor.getBlue(), hsbColor.getGreen(), (int) alpha); + + boolean isMouseOver = Lizzie.frame.isMouseOver(coordinates[0], coordinates[1]); + if (branch == null) { + drawShadow(g, suggestionX, suggestionY, true, alpha / 255.0f); + g.setColor(color); + fillCircle(g, suggestionX, suggestionY, stoneRadius); + } + + if (branch == null || isBestMove && isMouseOver) { + int strokeWidth = 1; + if (isBestMove != hasMaxWinrate) { + strokeWidth = 2; + g.setColor(isBestMove ? Color.RED : Color.BLUE); + g.setStroke(new BasicStroke(strokeWidth)); + } else { + g.setColor(color.darker()); } - } - } - - private void drawWoodenBoard(Graphics2D g) { - if (uiConfig.getBoolean("fancy-board")) { - // fancy version - int shadowRadius = (int) (boardLength * MARGIN / 6); - Image boardImage = theme.getBoard(); - g.drawImage(boardImage == null ? theme.getBoard() : boardImage, x - 2 * shadowRadius, y - 2 * shadowRadius, boardLength + 4 * shadowRadius, boardLength + 4 * shadowRadius, null); - g.setStroke(new BasicStroke(shadowRadius * 2)); - // draw border - g.setColor(new Color(0, 0, 0, 50)); - g.drawRect(x - shadowRadius, y - shadowRadius, boardLength + 2 * shadowRadius, boardLength + 2 * shadowRadius); + drawCircle(g, suggestionX, suggestionY, stoneRadius - strokeWidth / 2); g.setStroke(new BasicStroke(1)); + } + + if (branch == null + && (hasMaxWinrate + || percentPlayouts >= uiConfig.getDouble("min-playout-ratio-for-stats")) + || isMouseOver) { + double roundedWinrate = round(move.winrate * 10) / 10.0; + if (uiConfig.getBoolean("win-rate-always-black") + && !Lizzie.board.getData().blackToPlay) { + roundedWinrate = 100.0 - roundedWinrate; + } + g.setColor(Color.BLACK); + if (branch != null && Lizzie.board.getData().blackToPlay) g.setColor(Color.WHITE); - } else { - // simple version - JSONArray boardColor = uiConfig.getJSONArray("board-color"); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - g.setColor(new Color(boardColor.getInt(0), boardColor.getInt(1), boardColor.getInt(2))); - g.fillRect(x, y, boardLength, boardLength); - } - } + String text; + if (Lizzie.config.handicapInsteadOfWinrate) { + text = String.format("%.2f", Lizzie.leelaz.winrateToHandicap(move.winrate)); + } else { + text = String.format("%.1f", roundedWinrate); + } - /** - * Calculates the lengths and pixel margins from a given boardLength. - * - * @param boardLength go board's length in pixels; must be boardLength >= BOARD_SIZE - 1 - * @return an array containing the three outputs: new boardLength, scaledMargin, availableLength - */ - private int[] calculatePixelMargins(int boardLength) { - //boardLength -= boardLength*MARGIN/3; // account for the shadows we will draw around the edge of the board -// if (boardLength < Board.BOARD_SIZE - 1) -// throw new IllegalArgumentException("boardLength may not be less than " + (Board.BOARD_SIZE - 1) + ", but was " + boardLength); - - int scaledMargin; - int availableLength; - - // decrease boardLength until the availableLength will result in square board intersections - double margin = showCoordinates() ? MARGIN_WITH_COORDINATES : MARGIN; - boardLength++; - do { - boardLength--; - scaledMargin = (int) (margin * boardLength); - availableLength = boardLength - 2 * scaledMargin; + drawString( + g, + suggestionX, + suggestionY, + LizzieFrame.OpenSansSemiboldBase, + Font.PLAIN, + text, + stoneRadius, + stoneRadius * 1.5, + 1); + + drawString( + g, + suggestionX, + suggestionY + stoneRadius * 2 / 5, + LizzieFrame.OpenSansRegularBase, + getPlayoutsString(move.playouts), + (float) (stoneRadius * 0.8), + stoneRadius * 1.4); + } } - while (!((availableLength - 1) % (Board.BOARD_SIZE - 1) == 0)); - // this will be true if BOARD_SIZE - 1 square intersections, plus one line, will fit - - return new int[]{boardLength, scaledMargin, availableLength}; + } } - - private void drawShadow(Graphics2D g, int centerX, int centerY, boolean isGhost) { - drawShadow(g, centerX, centerY, isGhost, 1); + } + + private void drawNextMoves(Graphics2D g) { + + List nexts = Lizzie.board.getHistory().getNexts(); + + for (int i = 0; i < nexts.size(); i++) { + int[] nextMove = nexts.get(i).getData().lastMove; + if (nextMove == null) continue; + if (Lizzie.board.getData().blackToPlay) { + g.setColor(Color.BLACK); + } else { + g.setColor(Color.WHITE); + } + int moveX = x + scaledMargin + squareLength * nextMove[0]; + int moveY = y + scaledMargin + squareLength * nextMove[1]; + if (i == 0) { + g.setStroke(new BasicStroke(3.0f)); + } + drawCircle(g, moveX, moveY, stoneRadius + 1); // slightly outside best move circle + if (i == 0) { + g.setStroke(new BasicStroke(1.0f)); + } } - - private void drawShadow(Graphics2D g, int centerX, int centerY, boolean isGhost, float shadowStrength) { - if (!uiConfig.getBoolean("shadows-enabled")) - return; - - final int shadowSize = (int) (stoneRadius * 0.3 * uiConfig.getInt("shadow-size") / 100); - final int fartherShadowSize = (int) (stoneRadius * 0.17 * uiConfig.getInt("shadow-size") / 100); - - - final Paint TOP_GRADIENT_PAINT; - final Paint LOWER_RIGHT_GRADIENT_PAINT; - - if (isGhost) { - TOP_GRADIENT_PAINT = new RadialGradientPaint(new Point2D.Float(centerX, centerY), - stoneRadius + shadowSize, new float[]{((float) stoneRadius / (stoneRadius + shadowSize)) - 0.0001f, ((float) stoneRadius / (stoneRadius + shadowSize)), 1.0f}, new Color[]{ - new Color(0, 0, 0, 0), new Color(50, 50, 50, (int) (120 * shadowStrength)), new Color(0, 0, 0, 0) - }); - - LOWER_RIGHT_GRADIENT_PAINT = new RadialGradientPaint(new Point2D.Float(centerX + shadowSize * 2 / 3, centerY + shadowSize * 2 / 3), - stoneRadius + fartherShadowSize, new float[]{0.6f, 1.0f}, new Color[]{ - new Color(0, 0, 0, 180), new Color(0, 0, 0, 0) - }); - } else { - TOP_GRADIENT_PAINT = new RadialGradientPaint(new Point2D.Float(centerX, centerY), - stoneRadius + shadowSize, new float[]{0.3f, 1.0f}, new Color[]{ - new Color(50, 50, 50, 150), new Color(0, 0, 0, 0) - }); - LOWER_RIGHT_GRADIENT_PAINT = new RadialGradientPaint(new Point2D.Float(centerX + shadowSize, centerY + shadowSize), - stoneRadius + fartherShadowSize, new float[]{0.6f, 1.0f}, new Color[]{ - new Color(0, 0, 0, 140), new Color(0, 0, 0, 0) - }); + } + + private void drawWoodenBoard(Graphics2D g) { + if (uiConfig.getBoolean("fancy-board")) { + if (cachedBoardImage == null) { + try { + cachedBoardImage = ImageIO.read(getClass().getResourceAsStream("/assets/board.png")); + } catch (IOException e) { + e.printStackTrace(); } - - final Paint originalPaint = g.getPaint(); - - g.setPaint(TOP_GRADIENT_PAINT); - fillCircle(g, centerX, centerY, stoneRadius + shadowSize); - if (!isGhost) { - g.setPaint(LOWER_RIGHT_GRADIENT_PAINT); - fillCircle(g, centerX + shadowSize, centerY + shadowSize, stoneRadius + fartherShadowSize); - } - g.setPaint(originalPaint); + } + + int shadowRadius = (int) (boardLength * MARGIN / 6); + drawTextureImage( + g, + cachedBoardImage, + x - 2 * shadowRadius, + y - 2 * shadowRadius, + boardLength + 4 * shadowRadius, + boardLength + 4 * shadowRadius); + + g.setStroke(new BasicStroke(shadowRadius * 2)); + + // draw border + g.setColor(new Color(0, 0, 0, 50)); + g.drawRect( + x - shadowRadius, + y - shadowRadius, + boardLength + 2 * shadowRadius, + boardLength + 2 * shadowRadius); + g.setStroke(new BasicStroke(1)); + + } else { + JSONArray boardColor = uiConfig.getJSONArray("board-color"); + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF); + g.setColor(new Color(boardColor.getInt(0), boardColor.getInt(1), boardColor.getInt(2))); + g.fillRect(x, y, boardLength, boardLength); } - - /** - * Draws a stone centered at (centerX, centerY) - */ - private void drawStone(Graphics2D g, Graphics2D gShadow, int centerX, int centerY, Stone color, int x, int y) { -// g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, -// RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, - RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - - // if no shadow graphics is supplied, just draw onto the same graphics - if (gShadow == null) - gShadow = g; - - switch (color) { - case BLACK: - case BLACK_CAPTURED: - if (uiConfig.getBoolean("fancy-stones")) { - drawShadow(gShadow, centerX, centerY, false); - Image stone = theme.getBlackStone(new int[]{x, y}); - g.drawImage(stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); - } else { - drawShadow(gShadow, centerX, centerY, true); - g.setColor(Color.BLACK); - fillCircle(g, centerX, centerY, stoneRadius); - } - break; - - case WHITE: - case WHITE_CAPTURED: - if (uiConfig.getBoolean("fancy-stones")) { - drawShadow(gShadow, centerX, centerY, false); - Image stone = theme.getWhiteStone(new int[]{x, y}); - g.drawImage(stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); - } else { - drawShadow(gShadow, centerX, centerY, true); - g.setColor(Color.WHITE); - fillCircle(g, centerX, centerY, stoneRadius); - g.setColor(Color.BLACK); - drawCircle(g, centerX, centerY, stoneRadius); - } - break; - - case BLACK_GHOST: - if (uiConfig.getBoolean("fancy-stones")) { - drawShadow(gShadow, centerX, centerY, true); - Image stone = theme.getBlackStone(new int[]{x, y}); - g.drawImage(stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); - } else { - drawShadow(gShadow, centerX, centerY, true); - g.setColor(new Color(0, 0, 0));//, uiConfig.getInt("branch-stone-alpha"))); - fillCircle(g, centerX, centerY, stoneRadius); - } - break; - - case WHITE_GHOST: - if (uiConfig.getBoolean("fancy-stones")) { - drawShadow(gShadow, centerX, centerY, true); - Image stone = theme.getWhiteStone(new int[]{x, y}); - g.drawImage(stone, centerX - stoneRadius, centerY - stoneRadius, stoneRadius * 2 + 1, stoneRadius * 2 + 1, null); - } else { - drawShadow(gShadow, centerX, centerY, true); - g.setColor(new Color(255, 255, 255));//, uiConfig.getInt("branch-stone-alpha"))); - fillCircle(g, centerX, centerY, stoneRadius); - g.setColor(new Color(0, 0, 0));//, uiConfig.getInt("branch-stone-alpha"))); - drawCircle(g, centerX, centerY, stoneRadius); - } - break; - - default: - } - } - - /** - * Fills in a circle centered at (centerX, centerY) with radius $radius$ - */ - private void fillCircle(Graphics2D g, int centerX, int centerY, int radius) { - g.fillOval(centerX - radius, centerY - radius, 2 * radius + 1, 2 * radius + 1); - } - - /** - * Draws the outline of a circle centered at (centerX, centerY) with radius $radius$ - */ - private void drawCircle(Graphics2D g, int centerX, int centerY, int radius) { - g.drawOval(centerX - radius, centerY - radius, 2 * radius + 1, 2 * radius + 1); - } - - /** - * Draws a string centered at (x, y) of font $fontString$, whose contents are $string$. - * The maximum/default fontsize will be $maximumFontHeight$, and the length of the drawn string will be at most maximumFontWidth. - * The resulting actual size depends on the length of $string$. - * aboveOrBelow is a param that lets you set: - * aboveOrBelow = -1 -> y is the top of the string - * aboveOrBelow = 0 -> y is the vertical center of the string - * aboveOrBelow = 1 -> y is the bottom of the string - */ - private void drawString(Graphics2D g, int x, int y, Font fontBase, int style, String string, float maximumFontHeight, double maximumFontWidth, int aboveOrBelow) { - - Font font = makeFont(fontBase, style); - - // set maximum size of font - font = font.deriveFont((float) (font.getSize2D() * maximumFontWidth / g.getFontMetrics(font).stringWidth(string))); - font = font.deriveFont(Math.min(maximumFontHeight, font.getSize())); - g.setFont(font); - - FontMetrics metrics = g.getFontMetrics(font); - - int height = metrics.getAscent() - metrics.getDescent(); - int verticalOffset; - switch (aboveOrBelow) { - case -1: - verticalOffset = height / 2; - break; - - case 1: - verticalOffset = -height / 2; - break; - - default: - verticalOffset = 0; - } - // bounding box for debugging - // g.drawRect(x-(int)maximumFontWidth/2, y - height/2 + verticalOffset, (int)maximumFontWidth, height+verticalOffset ); - g.drawString(string, x - metrics.stringWidth(string) / 2, y + height / 2 + verticalOffset); + } + + /** + * Calculates the lengths and pixel margins from a given boardLength. + * + * @param boardLength go board's length in pixels; must be boardLength >= BOARD_SIZE - 1 + * @return an array containing the three outputs: new boardLength, scaledMargin, availableLength + */ + private int[] calculatePixelMargins(int boardLength) { + // boardLength -= boardLength*MARGIN/3; // account for the shadows we will draw around the edge + // of the board + // if (boardLength < Board.BOARD_SIZE - 1) + // throw new IllegalArgumentException("boardLength may not be less than " + + // (Board.BOARD_SIZE - 1) + ", but was " + boardLength); + + int scaledMargin; + int availableLength; + + // decrease boardLength until the availableLength will result in square board intersections + double margin = showCoordinates() ? MARGIN_WITH_COORDINATES : MARGIN; + boardLength++; + do { + boardLength--; + scaledMargin = (int) (margin * boardLength); + availableLength = boardLength - 2 * scaledMargin; + } while (!((availableLength - 1) % (Board.BOARD_SIZE - 1) == 0)); + // this will be true if BOARD_SIZE - 1 square intersections, plus one line, will fit + + return new int[] {boardLength, scaledMargin, availableLength}; + } + + private void drawShadow(Graphics2D g, int centerX, int centerY, boolean isGhost) { + drawShadow(g, centerX, centerY, isGhost, 1); + } + + private void drawShadow( + Graphics2D g, int centerX, int centerY, boolean isGhost, float shadowStrength) { + if (!uiConfig.getBoolean("shadows-enabled")) return; + + final int shadowSize = (int) (stoneRadius * 0.3 * uiConfig.getInt("shadow-size") / 100); + final int fartherShadowSize = (int) (stoneRadius * 0.17 * uiConfig.getInt("shadow-size") / 100); + + final Paint TOP_GRADIENT_PAINT; + final Paint LOWER_RIGHT_GRADIENT_PAINT; + + if (isGhost) { + TOP_GRADIENT_PAINT = + new RadialGradientPaint( + new Point2D.Float(centerX, centerY), + stoneRadius + shadowSize, + new float[] { + ((float) stoneRadius / (stoneRadius + shadowSize)) - 0.0001f, + ((float) stoneRadius / (stoneRadius + shadowSize)), + 1.0f + }, + new Color[] { + new Color(0, 0, 0, 0), + new Color(50, 50, 50, (int) (120 * shadowStrength)), + new Color(0, 0, 0, 0) + }); + + LOWER_RIGHT_GRADIENT_PAINT = + new RadialGradientPaint( + new Point2D.Float(centerX + shadowSize * 2 / 3, centerY + shadowSize * 2 / 3), + stoneRadius + fartherShadowSize, + new float[] {0.6f, 1.0f}, + new Color[] {new Color(0, 0, 0, 180), new Color(0, 0, 0, 0)}); + } else { + TOP_GRADIENT_PAINT = + new RadialGradientPaint( + new Point2D.Float(centerX, centerY), + stoneRadius + shadowSize, + new float[] {0.3f, 1.0f}, + new Color[] {new Color(50, 50, 50, 150), new Color(0, 0, 0, 0)}); + LOWER_RIGHT_GRADIENT_PAINT = + new RadialGradientPaint( + new Point2D.Float(centerX + shadowSize, centerY + shadowSize), + stoneRadius + fartherShadowSize, + new float[] {0.6f, 1.0f}, + new Color[] {new Color(0, 0, 0, 140), new Color(0, 0, 0, 0)}); } - private void drawString(Graphics2D g, int x, int y, Font fontBase, String string, float maximumFontHeight, double maximumFontWidth) { - drawString(g, x, y, fontBase, Font.PLAIN, string, maximumFontHeight, maximumFontWidth, 0); - } + final Paint originalPaint = g.getPaint(); - /** - * @return a font with kerning enabled - */ - private Font makeFont(Font fontBase, int style) { - Font font = fontBase.deriveFont(style, 100); - Map atts = new HashMap<>(); - atts.put(TextAttribute.KERNING, TextAttribute.KERNING_ON); - return font.deriveFont(atts); + g.setPaint(TOP_GRADIENT_PAINT); + fillCircle(g, centerX, centerY, stoneRadius + shadowSize); + if (!isGhost) { + g.setPaint(LOWER_RIGHT_GRADIENT_PAINT); + fillCircle(g, centerX + shadowSize, centerY + shadowSize, stoneRadius + fartherShadowSize); } - - - /** - * @return a shorter, rounded string version of playouts. e.g. 345 -> 345, 1265 -> 1.3k, 44556 -> 45k, 133523 -> 134k, 1234567 -> 1.2m - */ - private String getPlayoutsString(int playouts) { - if (playouts >= 1_000_000) { - double playoutsDouble = (double) playouts / 100_000; // 1234567 -> 12.34567 - return Math.round(playoutsDouble) / 10.0 + "m"; - } else if (playouts >= 10_000) { - double playoutsDouble = (double) playouts / 1_000; // 13265 -> 13.265 - return Math.round(playoutsDouble) + "k"; - } else if (playouts >= 1_000) { - double playoutsDouble = (double) playouts / 100; // 1265 -> 12.65 - return Math.round(playoutsDouble) / 10.0 + "k"; - } else { - return String.valueOf(playouts); + g.setPaint(originalPaint); + } + + /** Draws a stone centered at (centerX, centerY) */ + private void drawStone( + Graphics2D g, Graphics2D gShadow, int centerX, int centerY, Stone color, int x, int y) { + // g.setRenderingHint(KEY_ALPHA_INTERPOLATION, + // VALUE_ALPHA_INTERPOLATION_QUALITY); + g.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR); + g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + + // if no shadow graphics is supplied, just draw onto the same graphics + if (gShadow == null) gShadow = g; + + if (color.isBlack() || color.isWhite()) { + boolean isBlack = color.isBlack(); + boolean isGhost = (color == Stone.BLACK_GHOST || color == Stone.WHITE_GHOST); + if (uiConfig.getBoolean("fancy-stones")) { + drawShadow(gShadow, centerX, centerY, isGhost); + int size = stoneRadius * 2 + 1; + g.drawImage( + getScaleStone(isBlack, size), + centerX - stoneRadius, + centerY - stoneRadius, + size, + size, + null); + } else { + drawShadow(gShadow, centerX, centerY, true); + Color blackColor = isGhost ? new Color(0, 0, 0) : Color.BLACK; + Color whiteColor = isGhost ? new Color(255, 255, 255) : Color.WHITE; + g.setColor(isBlack ? blackColor : whiteColor); + fillCircle(g, centerX, centerY, stoneRadius); + if (!isBlack) { + g.setColor(blackColor); + drawCircle(g, centerX, centerY, stoneRadius); } + } } - - - private int[] calculatePixelMargins() { - return calculatePixelMargins(boardLength); - } - - /** - * Set the location to render the board - * - * @param x x coordinate - * @param y y coordinate - */ - public void setLocation(int x, int y) { - this.x = x; - this.y = y; - } - - public Point getLocation() { - return new Point(x, y); - } - - /** - * Set the maximum boardLength to render the board - * - * @param boardLength the boardLength of the board - */ - public void setBoardLength(int boardLength) { - this.boardLength = boardLength; - } - - /** - * @return the actual board length, including the shadows drawn at the edge of the wooden board - */ - public int getActualBoardLength() { - return (int) (boardLength * (1 + MARGIN / 3)); - } - - /** - * Converts a location on the screen to a location on the board - * - * @param x x pixel coordinate - * @param y y pixel coordinate - * @return if there is a valid coordinate, an array (x, y) where x and y are between 0 and BOARD_SIZE - 1. Otherwise, returns null - */ - public int[] convertScreenToCoordinates(int x, int y) { - int marginLength; // the pixel width of the margins - int boardLengthWithoutMargins; // the pixel width of the game board without margins - - // calculate a good set of boardLength, scaledMargin, and boardLengthWithoutMargins to use - int[] calculatedPixelMargins = calculatePixelMargins(); - setBoardLength(calculatedPixelMargins[0]); - marginLength = calculatedPixelMargins[1]; - boardLengthWithoutMargins = calculatedPixelMargins[2]; - - int squareSize = calculateSquareLength(boardLengthWithoutMargins); - - // transform the pixel coordinates to board coordinates - x = (x - this.x - marginLength + squareSize / 2) / squareSize; - y = (y - this.y - marginLength + squareSize / 2) / squareSize; - - // return these values if they are valid board coordinates - if (Board.isValid(x, y)) - return new int[]{x, y}; - else - return null; + } + + /** Get scaled stone, if cached then return cached */ + private BufferedImage getScaleStone(boolean isBlack, int size) { + BufferedImage stone = isBlack ? cachedBlackStoneImage : cachedWhiteStoneImage; + if (stone == null) { + stone = new BufferedImage(size, size, TYPE_INT_ARGB); + String imgPath = isBlack ? "/assets/black0.png" : "/assets/white0.png"; + Image img = null; + try { + img = ImageIO.read(getClass().getResourceAsStream(imgPath)); + } catch (IOException e) { + e.printStackTrace(); + } + Graphics2D g2 = stone.createGraphics(); + g2.drawImage(img.getScaledInstance(size, size, java.awt.Image.SCALE_SMOOTH), 0, 0, null); + g2.dispose(); + if (isBlack) { + cachedBlackStoneImage = stone; + } else { + cachedWhiteStoneImage = stone; + } } - - /** - * Calculate the boardLength of each intersection square - * - * @param availableLength the pixel board length of the game board without margins - * @return the board length of each intersection square - */ - private int calculateSquareLength(int availableLength) { - return availableLength / (Board.BOARD_SIZE - 1); + return stone; + } + + public BufferedImage getWallpaper() { + if (cachedWallpaperImage == null) { + try { + String wallpaperPath = "/assets/background.jpg"; + cachedWallpaperImage = ImageIO.read(getClass().getResourceAsStream(wallpaperPath)); + } catch (IOException e) { + e.printStackTrace(); + } } - - private boolean isShowingRawBoard() { - return (displayedBranchLength == SHOW_RAW_BOARD || displayedBranchLength == 0); + return cachedWallpaperImage; + } + + /** + * Draw scale smooth image, enhanced display quality (Not use, for future) This function use the + * traditional Image.getScaledInstance() method to provide the nice quality, but the performance + * is poor. Recommended for use in a few drawings + */ + // public void drawScaleSmoothImage(Graphics2D g, BufferedImage img, int x, int y, int width, + // int height, ImageObserver observer) { + // BufferedImage newstone = new BufferedImage(width, height, TYPE_INT_ARGB); + // Graphics2D g2 = newstone.createGraphics(); + // g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, + // observer); + // g2.dispose(); + // g.drawImage(newstone, x, y, width, height, observer); + // } + + /** + * Draw scale smooth image, enhanced display quality (Not use, for future) This functions use a + * multi-step approach to prevent the information loss and produces a much higher quality that is + * close to the Image.getScaledInstance() and faster than Image.getScaledInstance() method. + */ + // public void drawScaleImage(Graphics2D g, BufferedImage img, int x, int y, int width, int + // height, ImageObserver observer) { + // BufferedImage newstone = (BufferedImage)img; + // int w = img.getWidth(); + // int h = img.getHeight(); + // do { + // if (w > width) { + // w /= 2; + // if (w < width) { + // w = width; + // } + // } + // if (h > height) { + // h /= 2; + // if (h < height) { + // h = height; + // } + // } + // BufferedImage tmp = new BufferedImage(w, h, TYPE_INT_ARGB); + // Graphics2D g2 = tmp.createGraphics(); + // g2.setRenderingHint(KEY_INTERPOLATION, + // VALUE_INTERPOLATION_BICUBIC); + // g2.drawImage(newstone, 0, 0, w, h, null); + // g2.dispose(); + // newstone = tmp; + // } + // while (w != width || h != height); + // g.drawImage(newstone, x, y, width, height, observer); + // } + + /** Draw texture image */ + public void drawTextureImage( + Graphics2D g, BufferedImage img, int x, int y, int width, int height) { + TexturePaint paint = + new TexturePaint(img, new Rectangle(0, 0, img.getWidth(), img.getHeight())); + g.setPaint(paint); + g.fill(new Rectangle(x, y, width, height)); + } + + /** Fills in a circle centered at (centerX, centerY) with radius $radius$ */ + private void fillCircle(Graphics2D g, int centerX, int centerY, int radius) { + g.fillOval(centerX - radius, centerY - radius, 2 * radius + 1, 2 * radius + 1); + } + + /** Draws the outline of a circle centered at (centerX, centerY) with radius $radius$ */ + private void drawCircle(Graphics2D g, int centerX, int centerY, int radius) { + g.drawOval(centerX - radius, centerY - radius, 2 * radius + 1, 2 * radius + 1); + } + + /** + * Draws a string centered at (x, y) of font $fontString$, whose contents are $string$. The + * maximum/default fontsize will be $maximumFontHeight$, and the length of the drawn string will + * be at most maximumFontWidth. The resulting actual size depends on the length of $string$. + * aboveOrBelow is a param that lets you set: aboveOrBelow = -1 -> y is the top of the string + * aboveOrBelow = 0 -> y is the vertical center of the string aboveOrBelow = 1 -> y is the bottom + * of the string + */ + private void drawString( + Graphics2D g, + int x, + int y, + Font fontBase, + int style, + String string, + float maximumFontHeight, + double maximumFontWidth, + int aboveOrBelow) { + + Font font = makeFont(fontBase, style); + + // set maximum size of font + FontMetrics fm = g.getFontMetrics(font); + font = font.deriveFont((float) (font.getSize2D() * maximumFontWidth / fm.stringWidth(string))); + font = font.deriveFont(min(maximumFontHeight, font.getSize())); + g.setFont(font); + + int height = fm.getAscent() - fm.getDescent(); + int verticalOffset; + if (aboveOrBelow == -1) { + verticalOffset = height / 2; + } else if (aboveOrBelow == 1) { + verticalOffset = -height / 2; + } else { + verticalOffset = 0; } - private int maxBranchMoves() { - switch (displayedBranchLength) { - case SHOW_NORMAL_BOARD: - return Integer.MAX_VALUE; - case SHOW_RAW_BOARD: - return -1; - default: - return displayedBranchLength; - } + // bounding box for debugging + // g.drawRect(x-(int)maximumFontWidth/2, y - height/2 + verticalOffset, (int)maximumFontWidth, + // height+verticalOffset ); + g.drawString(string, x - fm.stringWidth(string) / 2, y + height / 2 + verticalOffset); + } + + private void drawString( + Graphics2D g, + int x, + int y, + Font fontBase, + String string, + float maximumFontHeight, + double maximumFontWidth) { + drawString(g, x, y, fontBase, Font.PLAIN, string, maximumFontHeight, maximumFontWidth, 0); + } + + /** @return a font with kerning enabled */ + private Font makeFont(Font fontBase, int style) { + Font font = fontBase.deriveFont(style, 100); + Map atts = new HashMap<>(); + atts.put(TextAttribute.KERNING, TextAttribute.KERNING_ON); + return font.deriveFont(atts); + } + + /** + * @return a shorter, rounded string version of playouts. e.g. 345 -> 345, 1265 -> 1.3k, 44556 -> + * 45k, 133523 -> 134k, 1234567 -> 1.2m + */ + private String getPlayoutsString(int playouts) { + if (playouts >= 1_000_000) { + double playoutsDouble = (double) playouts / 100_000; // 1234567 -> 12.34567 + return round(playoutsDouble) / 10.0 + "m"; + } else if (playouts >= 10_000) { + double playoutsDouble = (double) playouts / 1_000; // 13265 -> 13.265 + return round(playoutsDouble) + "k"; + } else if (playouts >= 1_000) { + double playoutsDouble = (double) playouts / 100; // 1265 -> 12.65 + return round(playoutsDouble) / 10.0 + "k"; + } else { + return String.valueOf(playouts); } - - public boolean isShowingBranch() { - return showingBranch; + } + + private int[] calculatePixelMargins() { + return calculatePixelMargins(boardLength); + } + + /** + * Set the location to render the board + * + * @param x x coordinate + * @param y y coordinate + */ + public void setLocation(int x, int y) { + this.x = x; + this.y = y; + } + + public Point getLocation() { + return new Point(x, y); + } + + /** + * Set the maximum boardLength to render the board + * + * @param boardLength the boardLength of the board + */ + public void setBoardLength(int boardLength) { + this.boardLength = boardLength; + } + + /** + * @return the actual board length, including the shadows drawn at the edge of the wooden board + */ + public int getActualBoardLength() { + return (int) (boardLength * (1 + MARGIN / 3)); + } + + /** + * Converts a location on the screen to a location on the board + * + * @param x x pixel coordinate + * @param y y pixel coordinate + * @return if there is a valid coordinate, an array (x, y) where x and y are between 0 and + * BOARD_SIZE - 1. Otherwise, returns null + */ + public int[] convertScreenToCoordinates(int x, int y) { + int marginLength; // the pixel width of the margins + int boardLengthWithoutMargins; // the pixel width of the game board without margins + + // calculate a good set of boardLength, scaledMargin, and boardLengthWithoutMargins to use + int[] calculatedPixelMargins = calculatePixelMargins(); + setBoardLength(calculatedPixelMargins[0]); + marginLength = calculatedPixelMargins[1]; + boardLengthWithoutMargins = calculatedPixelMargins[2]; + + int squareSize = calculateSquareLength(boardLengthWithoutMargins); + + // transform the pixel coordinates to board coordinates + x = (x - this.x - marginLength + squareSize / 2) / squareSize; + y = (y - this.y - marginLength + squareSize / 2) / squareSize; + + // return these values if they are valid board coordinates + if (Board.isValid(x, y)) return new int[] {x, y}; + else return null; + } + + /** + * Calculate the boardLength of each intersection square + * + * @param availableLength the pixel board length of the game board without margins + * @return the board length of each intersection square + */ + private int calculateSquareLength(int availableLength) { + return availableLength / (Board.BOARD_SIZE - 1); + } + + private boolean isShowingRawBoard() { + return (displayedBranchLength == SHOW_RAW_BOARD || displayedBranchLength == 0); + } + + private int maxBranchMoves() { + switch (displayedBranchLength) { + case SHOW_NORMAL_BOARD: + return Integer.MAX_VALUE; + case SHOW_RAW_BOARD: + return -1; + default: + return displayedBranchLength; } - - public void setDisplayedBranchLength(int n) { - displayedBranchLength = n; + } + + public boolean isShowingBranch() { + return showingBranch; + } + + public void setDisplayedBranchLength(int n) { + displayedBranchLength = n; + } + + public boolean incrementDisplayedBranchLength(int n) { + switch (displayedBranchLength) { + case SHOW_NORMAL_BOARD: + case SHOW_RAW_BOARD: + return false; + default: + // force nonnegative + displayedBranchLength = max(0, displayedBranchLength + n); + return true; } + } - public boolean incrementDisplayedBranchLength(int n) { - switch (displayedBranchLength) { - case SHOW_NORMAL_BOARD: - case SHOW_RAW_BOARD: - return false; - default: - // force nonnegative - displayedBranchLength = Math.max(0, displayedBranchLength + n); - return true; - } - } + public boolean isInside(int x1, int y1) { + return x <= x1 && x1 < x + boardLength && y <= y1 && y1 < y + boardLength; + } - public boolean isInside(int x1, int y1) { - return (x <= x1 && x1 < x + boardLength && y <= y1 && y1 < y + boardLength); - } + private boolean showCoordinates() { + return isMainBoard && Lizzie.frame.showCoordinates; + } - private boolean showCoordinates() { - return isMainBoard && Lizzie.frame.showCoordinates; - } + public void increaseMaxAlpha(int k) { + maxAlpha = min(maxAlpha + k, 255); + uiPersist.put("max-alpha", maxAlpha); + } } diff --git a/src/main/java/featurecat/lizzie/gui/GameInfoDialog.java b/src/main/java/featurecat/lizzie/gui/GameInfoDialog.java index 3c5c1a655..871a4cfa5 100644 --- a/src/main/java/featurecat/lizzie/gui/GameInfoDialog.java +++ b/src/main/java/featurecat/lizzie/gui/GameInfoDialog.java @@ -5,127 +5,135 @@ package featurecat.lizzie.gui; import featurecat.lizzie.analysis.GameInfo; - import java.awt.*; import java.text.DecimalFormat; import javax.swing.*; import javax.swing.border.*; -/** - * @author unknown - */ +/** @author unknown */ public class GameInfoDialog extends JDialog { - // create formatters - public static final DecimalFormat FORMAT_KOMI = new DecimalFormat("#0.0"); - public static final DecimalFormat FORMAT_HANDICAP = new DecimalFormat("0"); - - static { - FORMAT_HANDICAP.setMaximumIntegerDigits(1); - } - - private JPanel dialogPane = new JPanel(); - private JPanel contentPanel = new JPanel(); - private JPanel buttonBar = new JPanel(); - private JButton okButton = new JButton(); - - private JTextField textFieldBlack; - private JTextField textFieldWhite; - private JTextField textFieldKomi; - private JTextField textFieldHandicap; - - private GameInfo gameInfo; - - public GameInfoDialog() { - initComponents(); - } - - private void initComponents() { - setMinimumSize(new Dimension(100, 100)); - setResizable(false); - setTitle("Game Info"); - setModal(true); - - Container contentPane = getContentPane(); - contentPane.setLayout(new BorderLayout()); - - initDialogPane(contentPane); - - pack(); - setLocationRelativeTo(getOwner()); - } - - private void initDialogPane(Container contentPane) { - dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); - dialogPane.setLayout(new BorderLayout()); - - initContentPanel(); - initButtonBar(); - - contentPane.add(dialogPane, BorderLayout.CENTER); - } - - private void initContentPanel() { - GridLayout gridLayout = new GridLayout(4, 2, 4, 4); - contentPanel.setLayout(gridLayout); - - // editable - textFieldWhite = new JTextField(); - textFieldBlack = new JTextField(); - - // read-only - textFieldKomi = new JFormattedTextField(FORMAT_KOMI); - textFieldHandicap = new JFormattedTextField(FORMAT_HANDICAP); - textFieldKomi.setEditable(false); - textFieldHandicap.setEditable(false); - - contentPanel.add(new JLabel("Black")); - contentPanel.add(textFieldBlack); - contentPanel.add(new JLabel("White")); - contentPanel.add(textFieldWhite); - contentPanel.add(new JLabel("Komi")); - contentPanel.add(textFieldKomi); - contentPanel.add(new JLabel("Handicap")); - contentPanel.add(textFieldHandicap); - - dialogPane.add(contentPanel, BorderLayout.CENTER); - } - - private void initButtonBar() { - buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); - buttonBar.setLayout(new GridBagLayout()); - ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[]{0, 80}; - ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[]{1.0, 0.0}; - - //---- okButton ---- - okButton.setText("OK"); - okButton.addActionListener(e -> apply()); - - buttonBar.add(okButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), 0, 0)); - - dialogPane.add(buttonBar, BorderLayout.SOUTH); - } - - public void setGameInfo(GameInfo gameInfo) { - this.gameInfo = gameInfo; - - textFieldBlack.setText(gameInfo.getPlayerBlack()); - textFieldWhite.setText(gameInfo.getPlayerWhite()); - textFieldHandicap.setText(FORMAT_HANDICAP.format(gameInfo.getHandicap())); - textFieldKomi.setText(FORMAT_KOMI.format(gameInfo.getKomi())); - } - - public void apply() { - // validate data - String playerBlack = textFieldBlack.getText(); - String playerWhite = textFieldWhite.getText(); - - // apply new values - gameInfo.setPlayerBlack(playerBlack); - gameInfo.setPlayerWhite(playerWhite); - - // close window - setVisible(false); - } + // create formatters + public static final DecimalFormat FORMAT_KOMI = new DecimalFormat("#0.0"); + public static final DecimalFormat FORMAT_HANDICAP = new DecimalFormat("0"); + + static { + FORMAT_HANDICAP.setMaximumIntegerDigits(1); + } + + private JPanel dialogPane = new JPanel(); + private JPanel contentPanel = new JPanel(); + private JPanel buttonBar = new JPanel(); + private JButton okButton = new JButton(); + + private JTextField textFieldBlack; + private JTextField textFieldWhite; + private JTextField textFieldKomi; + private JTextField textFieldHandicap; + + private GameInfo gameInfo; + + public GameInfoDialog() { + initComponents(); + } + + private void initComponents() { + setMinimumSize(new Dimension(100, 100)); + setResizable(false); + setTitle("Game Info"); + setModal(true); + + Container contentPane = getContentPane(); + contentPane.setLayout(new BorderLayout()); + + initDialogPane(contentPane); + + pack(); + setLocationRelativeTo(getOwner()); + } + + private void initDialogPane(Container contentPane) { + dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); + dialogPane.setLayout(new BorderLayout()); + + initContentPanel(); + initButtonBar(); + + contentPane.add(dialogPane, BorderLayout.CENTER); + } + + private void initContentPanel() { + GridLayout gridLayout = new GridLayout(4, 2, 4, 4); + contentPanel.setLayout(gridLayout); + + // editable + textFieldWhite = new JTextField(); + textFieldBlack = new JTextField(); + + // read-only + textFieldKomi = new JFormattedTextField(FORMAT_KOMI); + textFieldHandicap = new JFormattedTextField(FORMAT_HANDICAP); + textFieldKomi.setEditable(false); + textFieldHandicap.setEditable(false); + + contentPanel.add(new JLabel("Black")); + contentPanel.add(textFieldBlack); + contentPanel.add(new JLabel("White")); + contentPanel.add(textFieldWhite); + contentPanel.add(new JLabel("Komi")); + contentPanel.add(textFieldKomi); + contentPanel.add(new JLabel("Handicap")); + contentPanel.add(textFieldHandicap); + + dialogPane.add(contentPanel, BorderLayout.CENTER); + } + + private void initButtonBar() { + buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); + buttonBar.setLayout(new GridBagLayout()); + ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[] {0, 80}; + ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[] {1.0, 0.0}; + + // ---- okButton ---- + okButton.setText("OK"); + okButton.addActionListener(e -> apply()); + + buttonBar.add( + okButton, + new GridBagConstraints( + 1, + 0, + 1, + 1, + 0.0, + 0.0, + GridBagConstraints.CENTER, + GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), + 0, + 0)); + + dialogPane.add(buttonBar, BorderLayout.SOUTH); + } + + public void setGameInfo(GameInfo gameInfo) { + this.gameInfo = gameInfo; + + textFieldBlack.setText(gameInfo.getPlayerBlack()); + textFieldWhite.setText(gameInfo.getPlayerWhite()); + textFieldHandicap.setText(FORMAT_HANDICAP.format(gameInfo.getHandicap())); + textFieldKomi.setText(FORMAT_KOMI.format(gameInfo.getKomi())); + } + + public void apply() { + // validate data + String playerBlack = textFieldBlack.getText(); + String playerWhite = textFieldWhite.getText(); + + // apply new values + gameInfo.setPlayerBlack(playerBlack); + gameInfo.setPlayerWhite(playerWhite); + + // close window + setVisible(false); + } } diff --git a/src/main/java/featurecat/lizzie/gui/Input.java b/src/main/java/featurecat/lizzie/gui/Input.java index 044146db5..fc36096ad 100644 --- a/src/main/java/featurecat/lizzie/gui/Input.java +++ b/src/main/java/featurecat/lizzie/gui/Input.java @@ -1,395 +1,402 @@ package featurecat.lizzie.gui; -import featurecat.lizzie.Lizzie; - -import java.awt.event.*; - import static java.awt.event.KeyEvent.*; -import featurecat.lizzie.plugin.PluginManager; +import featurecat.lizzie.Lizzie; +import java.awt.event.*; import javax.swing.*; public class Input implements MouseListener, KeyListener, MouseWheelListener, MouseMotionListener { - @Override - public void mouseClicked(MouseEvent e) { - + @Override + public void mouseClicked(MouseEvent e) {} + + @Override + public void mousePressed(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) // left click + Lizzie.frame.onClicked(e.getX(), e.getY()); + else if (e.getButton() == MouseEvent.BUTTON3) // right click + undo(); + } + + @Override + public void mouseReleased(MouseEvent e) {} + + @Override + public void mouseEntered(MouseEvent e) {} + + @Override + public void mouseExited(MouseEvent e) {} + + @Override + public void mouseDragged(MouseEvent e) { + Lizzie.frame.onMouseDragged(e.getX(), e.getY()); + } + + @Override + public void mouseMoved(MouseEvent e) { + Lizzie.frame.onMouseMoved(e.getX(), e.getY()); + } + + @Override + public void keyTyped(KeyEvent e) {} + + private void undo() { + undo(1); + } + + private void undo(int movesToAdvance) { + if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.frame.isPlayingAgainstLeelaz = false; } - - @Override - public void mousePressed(MouseEvent e) { - PluginManager.onMousePressed(e); - int x = e.getX(); - int y = e.getY(); - - if (e.getButton() == MouseEvent.BUTTON1) // left mouse click - Lizzie.frame.onClicked(x, y); - else if (e.getButton() == MouseEvent.BUTTON3) // right mouse click - undo(); - } - - @Override - public void mouseReleased(MouseEvent e) { - PluginManager.onMouseReleased(e); - } - - @Override - public void mouseEntered(MouseEvent e) { - + if (Lizzie.frame.incrementDisplayedBranchLength(-movesToAdvance)) { + return; } - @Override - public void mouseExited(MouseEvent e) { - + for (int i = 0; i < movesToAdvance; i++) Lizzie.board.previousMove(); + } + + private void undoToChildOfPreviousWithVariation() { + // Undo until the position just after the junction position. + // If we are already on such a position, we go to + // the junction position for convenience. + // Use cases: + // [Delete branch] Call this function and then deleteMove. + // [Go to junction] Call this function twice. + if (!Lizzie.board.undoToChildOfPreviousWithVariation()) Lizzie.board.previousMove(); + } + + private void redo() { + redo(1); + } + + private void redo(int movesToAdvance) { + if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.frame.isPlayingAgainstLeelaz = false; } - - @Override - public void mouseDragged(MouseEvent e) { - int x = e.getX(); - int y = e.getY(); - - Lizzie.frame.onMouseDragged(x, y); + if (Lizzie.frame.incrementDisplayedBranchLength(movesToAdvance)) { + return; } - @Override - public void mouseMoved(MouseEvent e) { - PluginManager.onMouseMoved(e); - int x = e.getX(); - int y = e.getY(); + for (int i = 0; i < movesToAdvance; i++) Lizzie.board.nextMove(); + } - Lizzie.frame.onMouseMoved(x, y); + private void startRawBoard() { + if (!Lizzie.config.showRawBoard) { + Lizzie.frame.startRawBoard(); } - - @Override - public void keyTyped(KeyEvent e) { - + Lizzie.config.showRawBoard = true; + } + + private void stopRawBoard() { + Lizzie.frame.stopRawBoard(); + Lizzie.config.showRawBoard = false; + } + + private void toggleHints() { + Lizzie.config.toggleShowBranch(); + Lizzie.config.showSubBoard = + Lizzie.config.showNextMoves = Lizzie.config.showBestMoves = Lizzie.config.showBranch; + } + + private void nextBranch() { + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.frame.isPlayingAgainstLeelaz = false; } + Lizzie.board.nextBranch(); + } - private void undo() { - undo(1); + private void previousBranch() { + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.frame.isPlayingAgainstLeelaz = false; } + Lizzie.board.previousBranch(); + } + + private void moveBranchUp() { + Lizzie.board.moveBranchUp(); + } + + private void moveBranchDown() { + Lizzie.board.moveBranchDown(); + } + + private void deleteMove() { + Lizzie.board.deleteMove(); + } + + private void deleteBranch() { + Lizzie.board.deleteBranch(); + } + + private boolean controlIsPressed(KeyEvent e) { + boolean mac = System.getProperty("os.name", "").toUpperCase().startsWith("MAC"); + return e.isControlDown() || (mac && e.isMetaDown()); + } + + private void toggleShowDynamicKomi() { + Lizzie.config.showDynamicKomi = !Lizzie.config.showDynamicKomi; + } + + @Override + public void keyPressed(KeyEvent e) { + // If any controls key is pressed, let's disable analysis mode. + // This is probably the user attempting to exit analysis mode. + boolean shouldDisableAnalysis = true; + + switch (e.getKeyCode()) { + case VK_RIGHT: + if (e.isShiftDown()) { + moveBranchDown(); + } else { + nextBranch(); + } + break; - private void undo(int movesToAdvance) { - if (Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.frame.isPlayingAgainstLeelaz = false; + case VK_LEFT: + if (e.isShiftDown()) { + moveBranchUp(); + } else { + previousBranch(); } - if (Lizzie.frame.incrementDisplayedBranchLength(- movesToAdvance)) { - return; + break; + + case VK_UP: + if (e.isShiftDown()) { + undoToChildOfPreviousWithVariation(); + } else if (controlIsPressed(e)) { + undo(10); + } else { + undo(); } + break; - for (int i = 0; i < movesToAdvance; i++) - Lizzie.board.previousMove(); - } - - private void undoToChildOfPreviousWithVariation() { - // Undo until the position just after the junction position. - // If we are already on such a position, we go to - // the junction position for convenience. - // Use cases: - // [Delete branch] Call this function and then deleteMove. - // [Go to junction] Call this function twice. - if (!Lizzie.board.undoToChildOfPreviousWithVariation()) - Lizzie.board.previousMove(); - } - - private void redo() { - redo(1); - } + case VK_PAGE_DOWN: + if (controlIsPressed(e) && e.isShiftDown()) { + Lizzie.frame.increaseMaxAlpha(-5); + } else { + redo(10); + } + break; - private void redo(int movesToAdvance) { - if (Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); + case VK_DOWN: + if (controlIsPressed(e)) { + redo(10); + } else { + redo(); + } + break; + + case VK_N: + // stop the ponder + if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); + LizzieFrame.startNewGame(); + break; + case VK_SPACE: if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.frame.isPlayingAgainstLeelaz = false; + Lizzie.frame.isPlayingAgainstLeelaz = false; + Lizzie.leelaz.togglePonder(); // we must toggle twice for it to restart pondering + Lizzie.leelaz.isThinking = false; } - if (Lizzie.frame.incrementDisplayedBranchLength(movesToAdvance)) { - return; + Lizzie.leelaz.togglePonder(); + break; + + case VK_P: + Lizzie.board.pass(); + break; + + case VK_COMMA: + if (!Lizzie.frame.playCurrentVariation()) Lizzie.frame.playBestMove(); + break; + + case VK_M: + Lizzie.config.toggleShowMoveNumber(); + break; + + case VK_F: + Lizzie.config.toggleShowNextMoves(); + break; + + case VK_H: + Lizzie.config.toggleHandicapInsteadOfWinrate(); + break; + + case VK_PAGE_UP: + if (controlIsPressed(e) && e.isShiftDown()) { + Lizzie.frame.increaseMaxAlpha(5); + } else { + undo(10); } - - for (int i = 0; i < movesToAdvance; i++) - Lizzie.board.nextMove(); - } - - private void startRawBoard() { - if (!Lizzie.config.showRawBoard) { - Lizzie.frame.startRawBoard(); + break; + + case VK_I: + // stop the ponder + if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); + Lizzie.frame.editGameInfo(); + break; + case VK_S: + // stop the ponder + if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); + LizzieFrame.saveFile(); + break; + + case VK_O: + if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); + LizzieFrame.openFile(); + break; + + case VK_V: + if (controlIsPressed(e)) { + Lizzie.frame.pasteSgf(); + } else { + Lizzie.config.toggleShowBranch(); } - Lizzie.config.showRawBoard = true; - } - - private void stopRawBoard() { - Lizzie.frame.stopRawBoard(); - Lizzie.config.showRawBoard = false; - } - - private void toggleHints() { - Lizzie.config.toggleShowBranch(); - Lizzie.config.showSubBoard = Lizzie.config.showNextMoves = Lizzie.config.showBestMoves - = Lizzie.config.showBranch; - } - - private void nextBranch() { - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.frame.isPlayingAgainstLeelaz = false; + break; + + case VK_HOME: + while (Lizzie.board.previousMove()) ; + break; + + case VK_END: + while (Lizzie.board.nextMove()) ; + break; + + case VK_X: + if (!Lizzie.frame.showControls) { + if (Lizzie.leelaz.isPondering()) { + wasPonderingWhenControlsShown = true; + Lizzie.leelaz.togglePonder(); + } else { + wasPonderingWhenControlsShown = false; + } + Lizzie.frame.drawControls(); } - Lizzie.board.nextBranch(); - } + Lizzie.frame.showControls = true; + break; + + case VK_W: + Lizzie.config.toggleShowWinrate(); + break; + + case VK_G: + Lizzie.config.toggleShowVariationGraph(); + break; + + case VK_T: + Lizzie.config.toggleShowComment(); + break; + + case VK_C: + if (controlIsPressed(e)) { + Lizzie.frame.copySgf(); + } else { + Lizzie.frame.toggleCoordinates(); + } + break; + + case VK_ENTER: + if (!Lizzie.leelaz.isThinking) { + Lizzie.leelaz.sendCommand( + "time_settings 0 " + + Lizzie.config + .config + .getJSONObject("leelaz") + .getInt("max-game-thinking-time-seconds") + + " 1"); + Lizzie.frame.playerIsBlack = !Lizzie.board.getData().blackToPlay; + Lizzie.frame.isPlayingAgainstLeelaz = true; + Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "B" : "W")); + } + break; + + case VK_DELETE: + case VK_BACK_SPACE: + if (e.isShiftDown()) { + deleteBranch(); + } else { + deleteMove(); + } + break; - private void previousBranch() { - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.frame.isPlayingAgainstLeelaz = false; + case VK_Z: + if (e.isShiftDown()) { + toggleHints(); + } else { + startRawBoard(); } - Lizzie.board.previousBranch(); - } + break; - private void moveBranchUp() { - Lizzie.board.moveBranchUp(); - } + case VK_A: + shouldDisableAnalysis = false; + Lizzie.board.toggleAnalysis(); + break; - private void moveBranchDown() { - Lizzie.board.moveBranchDown(); + case VK_PERIOD: + if (Lizzie.board.getHistory().getNext() == null) { + Lizzie.board.setScoreMode(!Lizzie.board.inScoreMode()); + } + break; + + case VK_D: + toggleShowDynamicKomi(); + break; + + // Use Ctrl+Num to switching multiple engine + case VK_0: + case VK_1: + case VK_2: + case VK_3: + case VK_4: + case VK_5: + case VK_6: + case VK_7: + case VK_8: + case VK_9: + if (controlIsPressed(e)) { + Lizzie.switchEngine(e.getKeyCode() - VK_0); + } + break; + default: + shouldDisableAnalysis = false; } - private void deleteMove() { Lizzie.board.deleteMove(); } - - private void deleteBranch() { Lizzie.board.deleteBranch(); } + if (shouldDisableAnalysis && Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); - private boolean controlIsPressed(KeyEvent e) { - boolean mac = System.getProperty("os.name", "").toUpperCase().startsWith("MAC"); - return e.isControlDown() || (mac && e.isMetaDown()); - } + Lizzie.frame.repaint(); + } - @Override - public void keyPressed(KeyEvent e) { - - PluginManager.onKeyPressed(e); - - // If any controls key is pressed, let's disable analysis mode. - // This is probably the user attempting to exit analysis mode. - boolean shouldDisableAnalysis = true; - - switch (e.getKeyCode()) { - case VK_RIGHT: - if (e.isShiftDown()) { - moveBranchDown(); - } else { - nextBranch(); - } - break; - - case VK_LEFT: - if (e.isShiftDown()) { - moveBranchUp(); - } else { - previousBranch(); - } - break; - - case VK_UP: - if (e.isShiftDown()) { - undoToChildOfPreviousWithVariation(); - } else if (controlIsPressed(e)) { - undo(10); - } else { - undo(); - } - break; - - case VK_PAGE_DOWN: - redo(10); - break; - - case VK_DOWN: - if (controlIsPressed(e)) { - redo(10); - } else { - redo(); - } - break; - - case VK_N: - // stop the ponder - if (Lizzie.leelaz.isPondering()) - Lizzie.leelaz.togglePonder(); - LizzieFrame.startNewGame(); - break; - case VK_SPACE: - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.frame.isPlayingAgainstLeelaz = false; - Lizzie.leelaz.togglePonder(); // we must toggle twice for it to restart pondering - Lizzie.leelaz.isThinking = false; - } - Lizzie.leelaz.togglePonder(); - break; - - case VK_P: - Lizzie.board.pass(); - break; - - case VK_COMMA: - if (!Lizzie.frame.playCurrentVariation()) - Lizzie.frame.playBestMove(); - break; - - case VK_M: - Lizzie.config.toggleShowMoveNumber(); - break; - - case VK_F: - Lizzie.config.toggleShowNextMoves(); - break; - - case VK_H: - Lizzie.config.toggleHandicapInsteadOfWinrate(); - break; - - case VK_PAGE_UP: - undo(10); - break; - - case VK_I: - // stop the ponder - if (Lizzie.leelaz.isPondering()) - Lizzie.leelaz.togglePonder(); - Lizzie.frame.editGameInfo(); - break; - case VK_S: - // stop the ponder - if (Lizzie.leelaz.isPondering()) - Lizzie.leelaz.togglePonder(); - LizzieFrame.saveSgf(); - break; - - case VK_O: - if (Lizzie.leelaz.isPondering()) - Lizzie.leelaz.togglePonder(); - LizzieFrame.openSgf(); - break; - - case VK_V: - if (controlIsPressed(e)) { - Lizzie.frame.pasteSgf(); - } else { - Lizzie.config.toggleShowBranch(); - } - break; - - case VK_HOME: - while (Lizzie.board.previousMove()) ; - break; - - case VK_END: - while (Lizzie.board.nextMove()) ; - break; - - case VK_X: - if (!Lizzie.frame.showControls) { - if (Lizzie.leelaz.isPondering()) { - wasPonderingWhenControlsShown = true; - Lizzie.leelaz.togglePonder(); - } else { - wasPonderingWhenControlsShown = false; - } - Lizzie.frame.drawControls(); - } - Lizzie.frame.showControls = true; - break; - - case VK_W: - Lizzie.config.toggleShowWinrate(); - break; - - case VK_G: - Lizzie.config.toggleShowVariationGraph(); - break; - - case VK_C: - if (controlIsPressed(e)) { - Lizzie.frame.copySgf(); - } else { - Lizzie.frame.toggleCoordinates(); - } - break; - - case VK_ENTER: - if (!Lizzie.leelaz.isThinking) { - Lizzie.leelaz.sendCommand("time_settings 0 " + Lizzie.config.config.getJSONObject("leelaz").getInt("max-game-thinking-time-seconds") + " 1"); - Lizzie.frame.playerIsBlack = !Lizzie.board.getData().blackToPlay; - Lizzie.frame.isPlayingAgainstLeelaz = true; - Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "B" : "W")); - } - break; - - case VK_DELETE: - case VK_BACK_SPACE: - if (e.isShiftDown()) { - deleteBranch(); - } else { - deleteMove(); - } - break; - - case VK_Z: - if (e.isShiftDown()) { - toggleHints(); - } else { - startRawBoard(); - } - break; - - case VK_A: - shouldDisableAnalysis = false; - Lizzie.board.toggleAnalysis(); - break; - - case VK_PERIOD: - if (Lizzie.board.getHistory().getNext() == null) - { - Lizzie.board.setScoreMode(!Lizzie.board.inScoreMode()); - } - break; - - default: - shouldDisableAnalysis = false; - } + private boolean wasPonderingWhenControlsShown = false; - if (shouldDisableAnalysis && Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); + @Override + public void keyReleased(KeyEvent e) { + switch (e.getKeyCode()) { + case VK_X: + if (wasPonderingWhenControlsShown) Lizzie.leelaz.togglePonder(); + Lizzie.frame.showControls = false; + Lizzie.frame.repaint(); + break; + case VK_Z: + stopRawBoard(); Lizzie.frame.repaint(); - } + break; - private boolean wasPonderingWhenControlsShown = false; - @Override - public void keyReleased(KeyEvent e) { - PluginManager.onKeyReleased(e); - switch (e.getKeyCode()) { - case VK_X: - if (wasPonderingWhenControlsShown) - Lizzie.leelaz.togglePonder(); - Lizzie.frame.showControls = false; - Lizzie.frame.repaint(); - break; - - case VK_Z: - stopRawBoard(); - Lizzie.frame.repaint(); - break; - - default: - } + default: } + } - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - if (Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); - if (e.getWheelRotation() > 0) { - redo(); - } else if (e.getWheelRotation() < 0) { - undo(); - } - Lizzie.frame.repaint(); + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (Lizzie.frame.processCommentMouseWheelMoved(e)) { + return; + } + if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); + if (e.getWheelRotation() > 0) { + redo(); + } else if (e.getWheelRotation() < 0) { + undo(); } + Lizzie.frame.repaint(); + } } diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index fa45ff9de..1893a6414 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -1,15 +1,5 @@ package featurecat.lizzie.gui; -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Font; -import java.awt.FontFormatException; -import java.awt.FontMetrics; -import java.awt.Frame; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.RenderingHints; - import com.jhlabs.image.GaussianFilter; import featurecat.lizzie.Lizzie; import featurecat.lizzie.Util; @@ -19,881 +9,1139 @@ import featurecat.lizzie.rules.BoardData; import featurecat.lizzie.rules.GIBParser; import featurecat.lizzie.rules.SGFParser; -import org.json.JSONObject; - -import javax.swing.*; -import javax.swing.filechooser.FileNameExtensionFilter; import java.awt.*; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.FontMetrics; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; - +import java.awt.event.MouseWheelEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferStrategy; import java.awt.image.BufferedImage; import java.io.*; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.ResourceBundle; +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; +import org.json.JSONArray; +import org.json.JSONObject; -/** - * The window used to display the game. - */ +/** The window used to display the game. */ public class LizzieFrame extends JFrame { - private static final ResourceBundle resourceBundle = ResourceBundle.getBundle("l10n.DisplayStrings"); - - private static final String[] commands = { - resourceBundle.getString("LizzieFrame.commands.keyN"), - resourceBundle.getString("LizzieFrame.commands.keyEnter"), - resourceBundle.getString("LizzieFrame.commands.keySpace"), - resourceBundle.getString("LizzieFrame.commands.keyUpArrow"), - resourceBundle.getString("LizzieFrame.commands.keyDownArrow"), - resourceBundle.getString("LizzieFrame.commands.rightClick"), - resourceBundle.getString("LizzieFrame.commands.mouseWheelScroll"), - resourceBundle.getString("LizzieFrame.commands.keyC"), - resourceBundle.getString("LizzieFrame.commands.keyP"), - resourceBundle.getString("LizzieFrame.commands.keyPeriod"), - resourceBundle.getString("LizzieFrame.commands.keyA"), - resourceBundle.getString("LizzieFrame.commands.keyM"), - resourceBundle.getString("LizzieFrame.commands.keyI"), - resourceBundle.getString("LizzieFrame.commands.keyO"), - resourceBundle.getString("LizzieFrame.commands.keyS"), - resourceBundle.getString("LizzieFrame.commands.keyAltC"), - resourceBundle.getString("LizzieFrame.commands.keyAltV"), - resourceBundle.getString("LizzieFrame.commands.keyF"), - resourceBundle.getString("LizzieFrame.commands.keyV"), - resourceBundle.getString("LizzieFrame.commands.keyW"), - resourceBundle.getString("LizzieFrame.commands.keyG"), - resourceBundle.getString("LizzieFrame.commands.keyHome"), - resourceBundle.getString("LizzieFrame.commands.keyEnd"), - resourceBundle.getString("LizzieFrame.commands.keyControl"), - }; - private static final String DEFAULT_TITLE = "Lizzie - Leela Zero Interface"; - private static BoardRenderer boardRenderer; - private static BoardRenderer subBoardRenderer; - private static VariationTree variationTree; - private static WinrateGraph winrateGraph; - - public static Font OpenSansRegularBase; - public static Font OpenSansSemiboldBase; - - private final BufferStrategy bs; - - public int[] mouseHoverCoordinate; - public boolean showControls = false; - public boolean showCoordinates = false; - public boolean isPlayingAgainstLeelaz = false; - public boolean playerIsBlack = true; - public int winRateGridLines = 3; - - // Get the font name in current system locale - private String systemDefaultFontName = new JLabel().getFont().getFontName(); - - static { - // load fonts - try { - OpenSansRegularBase = Font.createFont(Font.TRUETYPE_FONT, Thread.currentThread().getContextClassLoader().getResourceAsStream("fonts/OpenSans-Regular.ttf")); - OpenSansSemiboldBase = Font.createFont(Font.TRUETYPE_FONT, Thread.currentThread().getContextClassLoader().getResourceAsStream("fonts/OpenSans-Semibold.ttf")); - } catch (IOException | FontFormatException e) { - e.printStackTrace(); - } + private static final ResourceBundle resourceBundle = + ResourceBundle.getBundle("l10n.DisplayStrings"); + + private static final String[] commands = { + resourceBundle.getString("LizzieFrame.commands.keyN"), + resourceBundle.getString("LizzieFrame.commands.keyEnter"), + resourceBundle.getString("LizzieFrame.commands.keySpace"), + resourceBundle.getString("LizzieFrame.commands.keyUpArrow"), + resourceBundle.getString("LizzieFrame.commands.keyDownArrow"), + resourceBundle.getString("LizzieFrame.commands.rightClick"), + resourceBundle.getString("LizzieFrame.commands.mouseWheelScroll"), + resourceBundle.getString("LizzieFrame.commands.keyC"), + resourceBundle.getString("LizzieFrame.commands.keyP"), + resourceBundle.getString("LizzieFrame.commands.keyPeriod"), + resourceBundle.getString("LizzieFrame.commands.keyA"), + resourceBundle.getString("LizzieFrame.commands.keyM"), + resourceBundle.getString("LizzieFrame.commands.keyI"), + resourceBundle.getString("LizzieFrame.commands.keyO"), + resourceBundle.getString("LizzieFrame.commands.keyS"), + resourceBundle.getString("LizzieFrame.commands.keyAltC"), + resourceBundle.getString("LizzieFrame.commands.keyAltV"), + resourceBundle.getString("LizzieFrame.commands.keyF"), + resourceBundle.getString("LizzieFrame.commands.keyV"), + resourceBundle.getString("LizzieFrame.commands.keyW"), + resourceBundle.getString("LizzieFrame.commands.keyG"), + resourceBundle.getString("LizzieFrame.commands.keyT"), + resourceBundle.getString("LizzieFrame.commands.keyHome"), + resourceBundle.getString("LizzieFrame.commands.keyEnd"), + resourceBundle.getString("LizzieFrame.commands.keyControl"), + }; + private static final String DEFAULT_TITLE = "Lizzie - Leela Zero Interface"; + private static BoardRenderer boardRenderer; + private static BoardRenderer subBoardRenderer; + private static VariationTree variationTree; + private static WinrateGraph winrateGraph; + + public static Font OpenSansRegularBase; + public static Font OpenSansSemiboldBase; + + private final BufferStrategy bs; + + public int[] mouseOverCoordinate; + public boolean showControls = false; + public boolean showCoordinates = false; + public boolean isPlayingAgainstLeelaz = false; + public boolean playerIsBlack = true; + public int winRateGridLines = 3; + + // Get the font name in current system locale + private String systemDefaultFontName = new JLabel().getFont().getFontName(); + + private long lastAutosaveTime = System.currentTimeMillis(); + + // Save the player title + private String playerTitle = null; + + // Display Comment + private JScrollPane scrollPane = null; + private JTextPane commentPane = null; + private BufferedImage commentImage = null; + private String cachedComment = null; + private Rectangle commentRect = null; + + static { + // load fonts + try { + OpenSansRegularBase = + Font.createFont( + Font.TRUETYPE_FONT, + Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream("fonts/OpenSans-Regular.ttf")); + OpenSansSemiboldBase = + Font.createFont( + Font.TRUETYPE_FONT, + Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream("fonts/OpenSans-Semibold.ttf")); + } catch (IOException | FontFormatException e) { + e.printStackTrace(); } + } - /** - * Creates a window - */ - public LizzieFrame() { - super(DEFAULT_TITLE); + /** Creates a window */ + public LizzieFrame() { + super(DEFAULT_TITLE); - boardRenderer = new BoardRenderer(true); - subBoardRenderer = new BoardRenderer(false); - variationTree = new VariationTree(); - winrateGraph = new WinrateGraph(); + boardRenderer = new BoardRenderer(true); + subBoardRenderer = new BoardRenderer(false); + variationTree = new VariationTree(); + winrateGraph = new WinrateGraph(); - // on 1080p screens in Windows, this is a good width/height. removing a default size causes problems in Linux - setSize(657, 687); - setMinimumSize( new Dimension(640,480) ); - setLocationRelativeTo(null); // start centered - setExtendedState(Frame.MAXIMIZED_BOTH); // start maximized + setMinimumSize(new Dimension(640, 480)); + setLocationRelativeTo(null); // start centered + JSONArray windowSize = Lizzie.config.uiConfig.getJSONArray("window-size"); + setSize(windowSize.getInt(0), windowSize.getInt(1)); // use config file window size - setVisible(true); - - createBufferStrategy(2); - bs = getBufferStrategy(); - - Input input = new Input(); - - this.addMouseListener(input); - this.addKeyListener(input); - this.addMouseWheelListener(input); - this.addMouseMotionListener(input); - - // necessary for Windows users - otherwise Lizzie shows a blank white screen on startup until updates occur. - repaint(); + if (Lizzie.config.startMaximized) { + setExtendedState(Frame.MAXIMIZED_BOTH); // start maximized + } - // when the window is closed: save the SGF file, then run shutdown() - this.addWindowListener(new WindowAdapter() { - public void windowClosing(WindowEvent e) { - Lizzie.shutdown(); - } + // Comment Pane + commentPane = new JTextPane(); + commentPane.setEditable(false); + commentPane.setMargin(new Insets(5, 5, 5, 5)); + commentPane.setBackground(new Color(0, 0, 0, 200)); + commentPane.setForeground(Color.WHITE); + scrollPane = new JScrollPane(); + scrollPane.setViewportView(commentPane); + scrollPane.setBorder(null); + scrollPane.setVerticalScrollBarPolicy( + javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + + setVisible(true); + + createBufferStrategy(2); + bs = getBufferStrategy(); + + Input input = new Input(); + + this.addMouseListener(input); + this.addKeyListener(input); + this.addMouseWheelListener(input); + this.addMouseMotionListener(input); + + // necessary for Windows users - otherwise Lizzie shows a blank white screen on startup until + // updates occur. + repaint(); + + // when the window is closed: save the SGF file, then run shutdown() + this.addWindowListener( + new WindowAdapter() { + public void windowClosing(WindowEvent e) { + Lizzie.shutdown(); + } }); - + } + + public static void startNewGame() { + GameInfo gameInfo = Lizzie.board.getHistory().getGameInfo(); + + NewGameDialog newGameDialog = new NewGameDialog(); + newGameDialog.setGameInfo(gameInfo); + newGameDialog.setVisible(true); + boolean playerIsBlack = newGameDialog.playerIsBlack(); + newGameDialog.dispose(); + if (newGameDialog.isCancelled()) return; + + Lizzie.board.clear(); + Lizzie.leelaz.sendCommand("komi " + gameInfo.getKomi()); + + Lizzie.leelaz.sendCommand( + "time_settings 0 " + + Lizzie.config.config.getJSONObject("leelaz").getInt("max-game-thinking-time-seconds") + + " 1"); + Lizzie.frame.playerIsBlack = playerIsBlack; + Lizzie.frame.isPlayingAgainstLeelaz = true; + + boolean isHandicapGame = gameInfo.getHandicap() != 0; + if (isHandicapGame) { + Lizzie.board.getHistory().getData().blackToPlay = false; + Lizzie.leelaz.sendCommand("fixed_handicap " + gameInfo.getHandicap()); + if (playerIsBlack) Lizzie.leelaz.genmove("W"); + } else if (!playerIsBlack) { + Lizzie.leelaz.genmove("B"); } - - public static void startNewGame() { - GameInfo gameInfo = Lizzie.board.getHistory().getGameInfo(); - - NewGameDialog newGameDialog = new NewGameDialog(); - newGameDialog.setGameInfo(gameInfo); - newGameDialog.setVisible(true); - boolean playerIsBlack = newGameDialog.playerIsBlack(); - newGameDialog.dispose(); - if (newGameDialog.isCancelled()) return; - - Lizzie.board.clear(); - Lizzie.leelaz.sendCommand("komi " + gameInfo.getKomi()); - - Lizzie.leelaz.sendCommand("time_settings 0 " + Lizzie.config.config.getJSONObject("leelaz").getInt("max-game-thinking-time-seconds") + " 1"); - Lizzie.frame.playerIsBlack = playerIsBlack; - Lizzie.frame.isPlayingAgainstLeelaz = true; - - boolean isHandicapGame = gameInfo.getHandicap() != 0; - if (isHandicapGame) { - Lizzie.board.getHistory().getData().blackToPlay = false; - Lizzie.leelaz.sendCommand("fixed_handicap " + gameInfo.getHandicap()); - if (playerIsBlack) Lizzie.leelaz.genmove("W"); - } else if (!playerIsBlack) { - Lizzie.leelaz.genmove("B"); + } + + public static void editGameInfo() { + GameInfo gameInfo = Lizzie.board.getHistory().getGameInfo(); + + GameInfoDialog gameInfoDialog = new GameInfoDialog(); + gameInfoDialog.setGameInfo(gameInfo); + gameInfoDialog.setVisible(true); + + gameInfoDialog.dispose(); + } + + public static void saveFile() { + FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf", "SGF"); + JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); + JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); + chooser.setFileFilter(filter); + chooser.setMultiSelectionEnabled(false); + int result = chooser.showSaveDialog(null); + if (result == JFileChooser.APPROVE_OPTION) { + File file = chooser.getSelectedFile(); + if (file.exists()) { + int ret = + JOptionPane.showConfirmDialog( + null, + resourceBundle.getString("LizzieFrame.prompt.sgfExists"), + "Warning", + JOptionPane.OK_CANCEL_OPTION); + if (ret == JOptionPane.CANCEL_OPTION) { + return; } + } + if (!file.getPath().endsWith(".sgf")) { + file = new File(file.getPath() + ".sgf"); + } + try { + SGFParser.save(Lizzie.board, file.getPath()); + filesystem.put("last-folder", file.getParent()); + } catch (IOException err) { + JOptionPane.showConfirmDialog( + null, + resourceBundle.getString("LizzieFrame.prompt.failedTosaveFile"), + "Error", + JOptionPane.ERROR); + } } - - public static void editGameInfo() { - GameInfo gameInfo = Lizzie.board.getHistory().getGameInfo(); - - GameInfoDialog gameInfoDialog = new GameInfoDialog(); - gameInfoDialog.setGameInfo(gameInfo); - gameInfoDialog.setVisible(true); - - gameInfoDialog.dispose(); + } + + public static void openFile() { + FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf or *.gib", "SGF", "GIB"); + JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); + JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); + + chooser.setFileFilter(filter); + chooser.setMultiSelectionEnabled(false); + int result = chooser.showOpenDialog(null); + if (result == JFileChooser.APPROVE_OPTION) loadFile(chooser.getSelectedFile()); + } + + public static void loadFile(File file) { + JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); + if (!(file.getPath().endsWith(".sgf") || file.getPath().endsWith(".gib"))) { + file = new File(file.getPath() + ".sgf"); } + try { + System.out.println(file.getPath()); + if (file.getPath().endsWith(".sgf")) { + SGFParser.load(file.getPath()); + } else { + GIBParser.load(file.getPath()); + } + filesystem.put("last-folder", file.getParent()); + } catch (IOException err) { + JOptionPane.showConfirmDialog( + null, + resourceBundle.getString("LizzieFrame.prompt.failedToOpenFile"), + "Error", + JOptionPane.ERROR); + } + } + + private BufferedImage cachedImage = null; + + private BufferedImage cachedBackground = null; + private int cachedBackgroundWidth = 0, cachedBackgroundHeight = 0; + private boolean cachedBackgroundShowControls = false; + private boolean cachedShowWinrate = true; + private boolean cachedShowVariationGraph = true; + private boolean redrawBackgroundAnyway = false; + + /** + * Draws the game board and interface + * + * @param g0 not used + */ + public void paint(Graphics g0) { + autosaveMaybe(); + if (bs == null) return; + + Graphics2D backgroundG; + if (cachedBackgroundWidth != getWidth() + || cachedBackgroundHeight != getHeight() + || cachedBackgroundShowControls != showControls + || cachedShowWinrate != Lizzie.config.showWinrate + || cachedShowVariationGraph != Lizzie.config.showVariationGraph + || redrawBackgroundAnyway) backgroundG = createBackground(); + else backgroundG = null; + + if (!showControls) { + // layout parameters + + int topInset = this.getInsets().top; + + // board + int maxSize = (int) (Math.min(getWidth(), getHeight() - topInset) * 0.98); + maxSize = Math.max(maxSize, Board.BOARD_SIZE + 5); // don't let maxWidth become too small + int boardX = (getWidth() - maxSize) / 2; + int boardY = topInset + (getHeight() - topInset - maxSize) / 2 + 3; + + int panelMargin = (int) (maxSize * 0.05); + + // move statistics (winrate bar) + // boardX equals width of space on each side + int statx = 0; + int staty = boardY + maxSize / 8; + int statw = boardX - statx - panelMargin; + int stath = maxSize / 10; + + // winrate graph + int grx = statx; + int gry = staty + stath; + int grw = statw; + int grh = statw; + + // graph container + int contx = statx; + int conty = staty; + int contw = statw; + int conth = stath; + + // captured stones + int capx = 0; + int capy = this.getInsets().top; + int capw = boardX - (int) (maxSize * 0.05); + int caph = boardY + maxSize / 8 - this.getInsets().top; + + // variation tree container + int vx = boardX + maxSize + panelMargin; + int vy = 0; + int vw = getWidth() - vx; + int vh = getHeight(); + + // variation tree + int treex = vx; + int treey = vy; + int treew = vw + 1; + int treeh = vh; + + // pondering message + int ponderingX = this.getInsets().left; + int ponderingY = boardY + (int) (maxSize * 0.93); + double ponderingSize = .02; + + // dynamic komi + int dynamicKomiLabelX = this.getInsets().left; + int dynamicKomiLabelY = boardY + (int) (maxSize * 0.86); + + int dynamicKomiX = this.getInsets().left; + int dynamicKomiY = boardY + (int) (maxSize * 0.89); + double dynamicKomiSize = .02; + + // loading message + int loadingX = ponderingX; + int loadingY = ponderingY; + double loadingSize = 0.03; + + // subboard + int subBoardX = 0; + int subBoardY = gry + grh; + int subBoardWidth = grw; + int subBoardHeight = ponderingY - subBoardY; + int subBoardLength = Math.min(subBoardWidth, subBoardHeight); + + if (Lizzie.config.showLargeSubBoard()) { + boardX = getWidth() - maxSize - panelMargin; + int spaceW = boardX - panelMargin; + int spaceH = getHeight() - topInset; + int panelW = spaceW / 2; + int panelH = spaceH / 4; + capx = 0; + capy = topInset; + capw = panelW; + caph = (int) (panelH * 0.2); + statx = 0; + staty = capy + caph; + statw = panelW; + stath = (int) (panelH * 0.4); + grx = statx; + gry = staty + stath; + grw = statw; + grh = panelH - caph - stath; + contx = statx; + conty = staty; + contw = statw; + conth = stath + grh; + vx = panelW; + vy = 0; + vw = panelW; + vh = topInset + panelH; + treex = vx; + treey = vy; + treew = vw + 1; + treeh = vh; + subBoardX = 0; + subBoardY = topInset + panelH; + subBoardWidth = spaceW; + subBoardHeight = ponderingY - subBoardY; + subBoardLength = Math.min(subBoardWidth, subBoardHeight); + } + + // initialize + + cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D g = (Graphics2D) cachedImage.getGraphics(); + + if (Lizzie.config.showStatus) drawCommandString(g); + + boardRenderer.setLocation(boardX, boardY); + boardRenderer.setBoardLength(maxSize); + boardRenderer.draw(g); + + if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { + if (Lizzie.config.showStatus) { + String pondKey = "LizzieFrame.display." + (Lizzie.leelaz.isPondering() ? "on" : "off"); + String pondText = resourceBundle.getString(pondKey); + String switchText = resourceBundle.getString("LizzieFrame.prompt.switching"); + String weightText = Lizzie.leelaz.currentWeight().toString(); + String text = pondText + " " + weightText + (Lizzie.leelaz.switching() ? switchText : ""); + drawPonderingState(g, text, ponderingX, ponderingY, ponderingSize); + } - public static void saveSgf() { - FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf", "SGF"); - JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); - JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); - chooser.setFileFilter(filter); - chooser.setMultiSelectionEnabled(false); - int result = chooser.showSaveDialog(null); - if (result == JFileChooser.APPROVE_OPTION) { - File file = chooser.getSelectedFile(); - if (file.exists()) { - int ret = JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.sgfExists"), "Warning", JOptionPane.OK_CANCEL_OPTION); - if (ret == JOptionPane.CANCEL_OPTION) { - return; - } - } - if (!file.getPath().endsWith(".sgf")) { - file = new File(file.getPath() + ".sgf"); - } - try { - SGFParser.save(Lizzie.board, file.getPath()); - filesystem.put("last-folder", file.getParent()); - } catch (IOException err) { - JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.failedToSaveSgf"), "Error", JOptionPane.ERROR); - } + String dynamicKomi = Lizzie.leelaz.getDynamicKomi(); + if (Lizzie.config.showDynamicKomi && dynamicKomi != null) { + String text = resourceBundle.getString("LizzieFrame.display.dynamic-komi"); + drawPonderingState(g, text, dynamicKomiLabelX, dynamicKomiLabelY, dynamicKomiSize); + drawPonderingState(g, dynamicKomi, dynamicKomiX, dynamicKomiY, dynamicKomiSize); } - } - public static void openSgf() { - FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf or *.gib", "SGF", "GIB"); - JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); - JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); - - chooser.setFileFilter(filter); - chooser.setMultiSelectionEnabled(false); - int result = chooser.showOpenDialog(null); - if (result == JFileChooser.APPROVE_OPTION) { - File file = chooser.getSelectedFile(); - if (!(file.getPath().endsWith(".sgf") || file.getPath().endsWith(".gib"))) { - file = new File(file.getPath() + ".sgf"); - } - try { - System.out.println(file.getPath()); - if (file.getPath().endsWith(".sgf")) { - SGFParser.load(file.getPath()); - } else { - GIBParser.load(file.getPath()); - } - filesystem.put("last-folder", file.getParent()); - } catch (IOException err) { - JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.failedToOpenSgf"), "Error", JOptionPane.ERROR); - } + // Todo: Make board move over when there is no space beside the board + if (Lizzie.config.showWinrate) { + drawWinrateGraphContainer(backgroundG, contx, conty, contw, conth); + drawMoveStatistics(g, statx, staty, statw, stath); + winrateGraph.draw(g, grx, gry, grw, grh); } - } - private BufferedImage cachedImage = null; - - private BufferedImage cachedBackground = null; - private int cachedBackgroundWidth = 0, cachedBackgroundHeight = 0; - private boolean cachedBackgroundShowControls = false; - private boolean cachedShowWinrate = true; - private boolean cachedShowVariationGraph = true; - private boolean redrawBackgroundAnyway = false; - - /** - * Draws the game board and interface - * - * @param g0 not used - */ - public void paint(Graphics g0) { - if (bs == null) - return; - - Graphics2D backgroundG; - if (cachedBackgroundWidth != getWidth() || cachedBackgroundHeight != getHeight() || cachedBackgroundShowControls != showControls || cachedShowWinrate != Lizzie.config.showWinrate || cachedShowVariationGraph != Lizzie.config.showVariationGraph || redrawBackgroundAnyway) - backgroundG = createBackground(); - else - backgroundG = null; - - if (!showControls) { - // layout parameters - - int topInset = this.getInsets().top; - - // board - int maxSize = (int) (Math.min(getWidth(), getHeight() - topInset) * 0.98); - maxSize = Math.max(maxSize, Board.BOARD_SIZE + 5); // don't let maxWidth become too small - int boardX = (getWidth() - maxSize) / 2; - int boardY = topInset + (getHeight() - topInset - maxSize) / 2 + 3; - - int panelMargin = (int) (maxSize * 0.05); - - // move statistics (winrate bar) - // boardX equals width of space on each side - int statx = 0; - int staty = boardY + maxSize / 8; - int statw = boardX - statx - panelMargin; - int stath = maxSize / 10; - - // winrate graph - int grx = statx; - int gry = staty + stath; - int grw = statw; - int grh = statw; - - // graph container - int contx = statx; - int conty = staty; - int contw = statw; - int conth = stath; - - // captured stones - int capx = 0; - int capy = this.getInsets().top; - int capw = boardX - (int)(maxSize*0.05); - int caph = boardY+ maxSize/8 - this.getInsets().top; - - // variation tree container - int vx = boardX + maxSize + panelMargin; - int vy = 0; - int vw = getWidth() - vx; - int vh = getHeight(); - - // variation tree - int treex = vx; - int treey = vy; - int treew = vw + 1; - int treeh = vh; - - // pondering message - int ponderingX = this.getInsets().left; - int ponderingY = boardY + (int) (maxSize*0.93); - double ponderingSize = .02; - - // loading message - int loadingX = ponderingX; - int loadingY = ponderingY; - double loadingSize = 0.03; - - // subboard - int subBoardX = 0; - int subBoardY = gry + grh; - int subBoardWidth = grw; - int subBoardHeight = ponderingY - subBoardY; - int subBoardLength = Math.min(subBoardWidth, subBoardHeight); - - if (Lizzie.config.showLargeSubBoard()) { - boardX = getWidth() - maxSize - panelMargin; - int spaceW = boardX - panelMargin; - int spaceH = getHeight() - topInset; - int panelW = spaceW / 2; - int panelH = spaceH / 4; - capx = 0; - capy = topInset; - capw = panelW; - caph = (int) (panelH * 0.2); - statx = 0; - staty = capy + caph; - statw = panelW; - stath = (int) (panelH * 0.4); - grx = statx; - gry = staty + stath; - grw = statw; - grh = panelH - caph - stath; - contx = statx; - conty = staty; - contw = statw; - conth = stath + grh; - vx = panelW; - vy = 0; - vw = panelW; - vh = topInset + panelH; - treex = vx; - treey = vy; - treew = vw + 1; - treeh = vh; - subBoardX = 0; - subBoardY = topInset + panelH; - subBoardWidth = spaceW; - subBoardHeight = ponderingY - subBoardY; - subBoardLength = Math.min(subBoardWidth, subBoardHeight); - } - - // initialize - - cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g = (Graphics2D) cachedImage.getGraphics(); - - drawCommandString(g); - - boardRenderer.setLocation(boardX, boardY); - boardRenderer.setBoardLength(maxSize); - boardRenderer.draw(g); - - if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { - drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.pondering") + - (Lizzie.leelaz.isPondering()?resourceBundle.getString("LizzieFrame.display.on"):resourceBundle.getString("LizzieFrame.display.off")), - ponderingX, ponderingY, ponderingSize); - - // Todo: Make board move over when there is no space beside the board - if (Lizzie.config.showWinrate) { - drawWinrateGraphContainer(backgroundG, contx, conty, contw, conth); - drawMoveStatistics(g, statx, staty, statw, stath); - winrateGraph.draw(g, grx, gry, grw, grh); - } - - if (Lizzie.config.showVariationGraph) { - drawVariationTreeContainer(backgroundG, vx, vy, vw, vh); - variationTree.draw(g, treex, treey, treew, treeh); - } - if (Lizzie.config.showSubBoard) { - try { - subBoardRenderer.setLocation(subBoardX, subBoardY); - subBoardRenderer.setBoardLength(subBoardLength); - subBoardRenderer.draw(g); - } catch (Exception e) { - // This can happen when no space is left for subboard. - } - } - } else { - drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.loading"), loadingX, loadingY, loadingSize); - } - - drawCaptured(g, capx, capy, capw, caph); - - // cleanup - g.dispose(); + if (Lizzie.config.showVariationGraph) { + drawVariationTreeContainer(backgroundG, vx, vy, vw, vh); + int cHeight = 0; + if (Lizzie.config.showComment) { + // Draw the Comment of the Sgf + cHeight = drawComment(g, vx, vy, vw, vh, false); + } + variationTree.draw(g, treex, treey, treew, treeh - cHeight); + } else { + if (Lizzie.config.showComment) { + // Draw the Comment of the Sgf + drawComment(g, vx, topInset, vw, vh - topInset + vy, true); + } } - // draw the image - Graphics2D bsGraphics = (Graphics2D) bs.getDrawGraphics(); - bsGraphics.drawImage(cachedBackground, 0, 0, null); - bsGraphics.drawImage(cachedImage, 0, 0, null); + if (Lizzie.config.showSubBoard) { + try { + subBoardRenderer.setLocation(subBoardX, subBoardY); + subBoardRenderer.setBoardLength(subBoardLength); + subBoardRenderer.draw(g); + } catch (Exception e) { + // This can happen when no space is left for subboard. + } + } + } else if (Lizzie.config.showStatus) { + String loadingText = resourceBundle.getString("LizzieFrame.display.loading"); + drawPonderingState(g, loadingText, loadingX, loadingY, loadingSize); + } - // cleanup - bsGraphics.dispose(); - bs.show(); - } + if (Lizzie.config.showCaptured) drawCaptured(g, capx, capy, capw, caph); - /** - * temporary measure to refresh background. ideally we shouldn't need this - * (but we want to release Lizzie 0.5 today, not tomorrow!). Refactor me out please! (you need to get blurring to - * work properly on startup). - */ - public void refreshBackground() { - redrawBackgroundAnyway = true; + // cleanup + g.dispose(); } - private Graphics2D createBackground() { - cachedBackground = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); - cachedBackgroundWidth = cachedBackground.getWidth(); - cachedBackgroundHeight = cachedBackground.getHeight(); - cachedBackgroundShowControls = showControls; - cachedShowWinrate = Lizzie.config.showWinrate; - cachedShowVariationGraph = Lizzie.config.showVariationGraph; - - redrawBackgroundAnyway = false; - - Graphics2D g = cachedBackground.createGraphics(); - - BufferedImage background = boardRenderer.theme.getBackground(); - int drawWidth = Math.max(background.getWidth(), getWidth()); - int drawHeight = Math.max(background.getHeight(), getHeight()); - - g.drawImage(background, 0, 0, drawWidth, drawHeight, null); - - return g; + // draw the image + Graphics2D bsGraphics = (Graphics2D) bs.getDrawGraphics(); + bsGraphics.drawImage(cachedBackground, 0, 0, null); + bsGraphics.drawImage(cachedImage, 0, 0, null); + + // cleanup + bsGraphics.dispose(); + bs.show(); + } + + /** + * temporary measure to refresh background. ideally we shouldn't need this (but we want to release + * Lizzie 0.5 today, not tomorrow!). Refactor me out please! (you need to get blurring to work + * properly on startup). + */ + public void refreshBackground() { + redrawBackgroundAnyway = true; + } + + private Graphics2D createBackground() { + cachedBackground = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); + cachedBackgroundWidth = cachedBackground.getWidth(); + cachedBackgroundHeight = cachedBackground.getHeight(); + cachedBackgroundShowControls = showControls; + cachedShowWinrate = Lizzie.config.showWinrate; + cachedShowVariationGraph = Lizzie.config.showVariationGraph; + + redrawBackgroundAnyway = false; + + Graphics2D g = cachedBackground.createGraphics(); + + BufferedImage wallpaper = boardRenderer.getWallpaper(); + int drawWidth = Math.max(wallpaper.getWidth(), getWidth()); + int drawHeight = Math.max(wallpaper.getHeight(), getHeight()); + // Support seamless texture + boardRenderer.drawTextureImage(g, wallpaper, 0, 0, drawWidth, drawHeight); + + return g; + } + + private void drawVariationTreeContainer(Graphics2D g, int vx, int vy, int vw, int vh) { + vw = cachedBackground.getWidth() - vx; + + if (g == null || vw <= 0 || vh <= 0) return; + + BufferedImage result = new BufferedImage(vw, vh, BufferedImage.TYPE_INT_ARGB); + filter20.filter(cachedBackground.getSubimage(vx, vy, vw, vh), result); + g.drawImage(result, vx, vy, null); + } + + private void drawPonderingState(Graphics2D g, String text, int x, int y, double size) { + int fontSize = (int) (Math.max(getWidth(), getHeight()) * size); + Font font = new Font(systemDefaultFontName, Font.PLAIN, fontSize); + FontMetrics fm = g.getFontMetrics(font); + int stringWidth = fm.stringWidth(text); + // Truncate too long text when display switching prompt + if (Lizzie.leelaz.isLoaded()) { + int mainBoardX = + (boardRenderer != null && boardRenderer.getLocation() != null) + ? boardRenderer.getLocation().x + : 0; + if ((mainBoardX > x) && stringWidth > (mainBoardX - x)) { + text = Util.truncateStringByWidth(text, fm, mainBoardX - x); + stringWidth = fm.stringWidth(text); + } } - - private void drawVariationTreeContainer(Graphics2D g, int vx, int vy, int vw, int vh) { - vw = cachedBackground.getWidth() - vx; - - if (g == null || vw <= 0 || vh <= 0) - return; - - BufferedImage result = new BufferedImage(vw, vh, BufferedImage.TYPE_INT_ARGB); - filter20.filter(cachedBackground.getSubimage(vx, vy, vw, vh), result); - g.drawImage(result, vx, vy, null); + int stringHeight = fm.getAscent() - fm.getDescent(); + int width = stringWidth; + int height = (int) (stringHeight * 1.2); + + BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + // commenting this out for now... always causing an exception on startup. will fix in the + // upcoming refactoring + // filter20.filter(cachedBackground.getSubimage(x, y, result.getWidth(), + // result.getHeight()), result); + g.drawImage(result, x, y, null); + + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(x, y, width, height); + g.drawRect(x, y, width, height); + + g.setColor(Color.white); + g.setFont(font); + g.drawString( + text, x + (width - stringWidth) / 2, y + stringHeight + (height - stringHeight) / 2); + } + + private void drawWinrateGraphContainer(Graphics g, int statx, int staty, int statw, int stath) { + if (g == null || statw <= 0 || stath <= 0) return; + + BufferedImage result = new BufferedImage(statw, stath + statw, BufferedImage.TYPE_INT_ARGB); + filter20.filter( + cachedBackground.getSubimage(statx, staty, result.getWidth(), result.getHeight()), result); + g.drawImage(result, statx, staty, null); + } + + private GaussianFilter filter20 = new GaussianFilter(20); + private GaussianFilter filter10 = new GaussianFilter(10); + + /** Display the controls */ + void drawControls() { + userAlreadyKnowsAboutCommandString = true; + + cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); + + // redraw background + createBackground(); + + List commandsToShow = new ArrayList<>(Arrays.asList(commands)); + if (Lizzie.leelaz.getDynamicKomi() != null) { + commandsToShow.add(resourceBundle.getString("LizzieFrame.commands.keyD")); } - private void drawPonderingState(Graphics2D g, String text, int x, int y, double size) { - Font font = new Font(systemDefaultFontName, Font.PLAIN, (int)(Math.max(getWidth(), getHeight()) * size)); - FontMetrics fm = g.getFontMetrics(font); - int stringWidth = fm.stringWidth(text); - int stringHeight = fm.getAscent() - fm.getDescent(); - int width = stringWidth; - int height = (int)(stringHeight * 1.2); - - BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - // commenting this out for now... always causing an exception on startup. will fix in the upcoming refactoring -// filter20.filter(cachedBackground.getSubimage(x, y, result.getWidth(), result.getHeight()), result); - g.drawImage(result, x, y, null); - - g.setColor(new Color(0,0,0,130)); - g.fillRect(x, y, width, height); - g.drawRect(x, y, width, height); - - g.setColor(Color.white); - g.setFont(font); - g.drawString(text, x + (width - stringWidth)/2, y + stringHeight + (height - stringHeight)/2); + Graphics2D g = cachedImage.createGraphics(); + + int maxSize = Math.min(getWidth(), getHeight()); + Font font = new Font(systemDefaultFontName, Font.PLAIN, (int) (maxSize * 0.034)); + g.setFont(font); + + FontMetrics metrics = g.getFontMetrics(font); + int maxCmdWidth = commandsToShow.stream().mapToInt(c -> metrics.stringWidth(c)).max().orElse(0); + int lineHeight = (int) (font.getSize() * 1.15); + + int boxWidth = Util.clamp((int) (maxCmdWidth * 1.4), 0, getWidth()); + int boxHeight = Util.clamp(commandsToShow.size() * lineHeight, 0, getHeight()); + + int commandsX = Util.clamp(getWidth() / 2 - boxWidth / 2, 0, getWidth()); + int commandsY = Util.clamp(getHeight() / 2 - boxHeight / 2, 0, getHeight()); + + BufferedImage result = new BufferedImage(boxWidth, boxHeight, BufferedImage.TYPE_INT_ARGB); + filter10.filter( + cachedBackground.getSubimage(commandsX, commandsY, boxWidth, boxHeight), result); + g.drawImage(result, commandsX, commandsY, null); + + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(commandsX, commandsY, boxWidth, boxHeight); + int strokeRadius = 2; + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.setColor(new Color(0, 0, 0, 60)); + g.drawRect( + commandsX + strokeRadius, + commandsY + strokeRadius, + boxWidth - 2 * strokeRadius, + boxHeight - 2 * strokeRadius); + + int verticalLineX = (int) (commandsX + boxWidth * 0.3); + g.setColor(new Color(0, 0, 0, 60)); + g.drawLine( + verticalLineX, + commandsY + 2 * strokeRadius, + verticalLineX, + commandsY + boxHeight - 2 * strokeRadius); + + g.setStroke(new BasicStroke(1)); + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g.setColor(Color.WHITE); + int lineOffset = commandsY; + for (String command : commandsToShow) { + String[] split = command.split("\\|"); + g.drawString( + split[0], + verticalLineX - metrics.stringWidth(split[0]) - strokeRadius * 4, + font.getSize() + lineOffset); + g.drawString(split[1], verticalLineX + strokeRadius * 4, font.getSize() + lineOffset); + lineOffset += lineHeight; } - private void drawWinrateGraphContainer(Graphics g, int statx, int staty, int statw, int stath) { - if (g == null || statw <= 0 || stath <= 0) - return; - - BufferedImage result = new BufferedImage(statw, stath + statw, BufferedImage.TYPE_INT_ARGB); - filter20.filter(cachedBackground.getSubimage(statx, staty, result.getWidth(), result.getHeight()), result); - g.drawImage(result, statx, staty, null); + refreshBackground(); + } + + private boolean userAlreadyKnowsAboutCommandString = false; + + private void drawCommandString(Graphics2D g) { + if (userAlreadyKnowsAboutCommandString) return; + + int maxSize = (int) (Math.min(getWidth(), getHeight()) * 0.98); + + Font font = new Font(systemDefaultFontName, Font.PLAIN, (int) (maxSize * 0.03)); + String commandString = resourceBundle.getString("LizzieFrame.prompt.showControlsHint"); + int strokeRadius = 2; + + int showCommandsHeight = (int) (font.getSize() * 1.1); + int showCommandsWidth = g.getFontMetrics(font).stringWidth(commandString) + 4 * strokeRadius; + int showCommandsX = this.getInsets().left; + int showCommandsY = getHeight() - showCommandsHeight - this.getInsets().bottom; + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(showCommandsX, showCommandsY, showCommandsWidth, showCommandsHeight); + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.setColor(new Color(0, 0, 0, 60)); + g.drawRect( + showCommandsX + strokeRadius, + showCommandsY + strokeRadius, + showCommandsWidth - 2 * strokeRadius, + showCommandsHeight - 2 * strokeRadius); + g.setStroke(new BasicStroke(1)); + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(Color.WHITE); + g.setFont(font); + g.drawString(commandString, showCommandsX + 2 * strokeRadius, showCommandsY + font.getSize()); + } + + private void drawMoveStatistics(Graphics2D g, int posX, int posY, int width, int height) { + if (width < 0 || height < 0) return; // we don't have enough space + + double lastWR = 50; // winrate the previous move + boolean validLastWinrate = false; // whether it was actually calculated + BoardData lastNode = Lizzie.board.getHistory().getPrevious(); + if (lastNode != null && lastNode.playouts > 0) { + lastWR = lastNode.winrate; + validLastWinrate = true; } - private GaussianFilter filter20 = new GaussianFilter(20); - private GaussianFilter filter10 = new GaussianFilter(10); - - /** - * Display the controls - */ - void drawControls() { - userAlreadyKnowsAboutCommandString = true; + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + double curWR = stats.maxWinrate; // winrate on this move + boolean validWinrate = (stats.totalPlayouts > 0); // and whether it was actually calculated + if (isPlayingAgainstLeelaz && playerIsBlack == !Lizzie.board.getHistory().getData().blackToPlay) + validWinrate = false; - cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); - - // redraw background - createBackground(); - - Graphics2D g = cachedImage.createGraphics(); - - int maxSize = Math.min(getWidth(), getHeight()); - Font font = new Font(systemDefaultFontName, Font.PLAIN, (int) (maxSize * 0.034)); - g.setFont(font); - FontMetrics metrics = g.getFontMetrics(font); - int maxCommandWidth = Arrays.asList(commands).stream().reduce(0, (Integer i, String command) -> Math.max(i, metrics.stringWidth(command)), (Integer a, Integer b) -> Math.max(a, b)); - int lineHeight = (int) (font.getSize() * 1.15); - - int boxWidth = Util.clamp((int) (maxCommandWidth * 1.4), 0, getWidth()); - int boxHeight = Util.clamp(commands.length * lineHeight, 0, getHeight()); - - int commandsX = Util.clamp(getWidth() / 2 - boxWidth / 2, 0, getWidth()); - int commandsY = Util.clamp(getHeight() / 2 - boxHeight / 2, 0, getHeight()); - - BufferedImage result = new BufferedImage(boxWidth, boxHeight, BufferedImage.TYPE_INT_ARGB); - filter10.filter(cachedBackground.getSubimage(commandsX, commandsY, boxWidth, boxHeight), result); - g.drawImage(result, commandsX, commandsY, null); - - g.setColor(new Color(0, 0, 0, 130)); - g.fillRect(commandsX, commandsY, boxWidth, boxHeight); - int strokeRadius = 2; - g.setStroke(new BasicStroke(2 * strokeRadius)); - g.setColor(new Color(0, 0, 0, 60)); - g.drawRect(commandsX + strokeRadius, commandsY + strokeRadius, boxWidth - 2 * strokeRadius, boxHeight - 2 * strokeRadius); - - int verticalLineX = (int) (commandsX + boxWidth * 0.3); - g.setColor(new Color(0, 0, 0, 60)); - g.drawLine(verticalLineX, commandsY + 2 * strokeRadius, verticalLineX, commandsY + boxHeight - 2 * strokeRadius); - - - g.setStroke(new BasicStroke(1)); - - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - g.setColor(Color.WHITE); - for (int i = 0; i < commands.length; i++) { - String[] split = commands[i].split("\\|"); - g.drawString(split[0], verticalLineX - metrics.stringWidth(split[0]) - strokeRadius * 4, font.getSize() + (commandsY + i * lineHeight)); - g.drawString(split[1], verticalLineX + strokeRadius * 4, font.getSize() + (commandsY + i * lineHeight)); - } - - refreshBackground(); + if (!validWinrate) { + curWR = 100 - lastWR; // display last move's winrate for now (with color difference) } - - private boolean userAlreadyKnowsAboutCommandString = false; - - private void drawCommandString(Graphics2D g) { - if (userAlreadyKnowsAboutCommandString) - return; - - int maxSize = (int) (Math.min(getWidth(), getHeight()) * 0.98); - - Font font = new Font(systemDefaultFontName, Font.PLAIN, (int) (maxSize * 0.03)); - String commandString = resourceBundle.getString("LizzieFrame.prompt.showControlsHint"); - int strokeRadius = 2; - - int showCommandsHeight = (int) (font.getSize() * 1.1); - int showCommandsWidth = g.getFontMetrics(font).stringWidth(commandString) + 4 * strokeRadius; - int showCommandsX = this.getInsets().left; - int showCommandsY = getHeight() - showCommandsHeight - this.getInsets().bottom; - g.setColor(new Color(0, 0, 0, 130)); - g.fillRect(showCommandsX, showCommandsY, showCommandsWidth, showCommandsHeight); - g.setStroke(new BasicStroke(2 * strokeRadius)); - g.setColor(new Color(0, 0, 0, 60)); - g.drawRect(showCommandsX + strokeRadius, showCommandsY + strokeRadius, showCommandsWidth - 2 * strokeRadius, showCommandsHeight - 2 * strokeRadius); - g.setStroke(new BasicStroke(1)); - - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g.setColor(Color.WHITE); - g.setFont(font); - g.drawString(commandString, showCommandsX + 2 * strokeRadius, showCommandsY + font.getSize()); + double whiteWR, blackWR; + if (Lizzie.board.getData().blackToPlay) { + blackWR = curWR; + } else { + blackWR = 100 - curWR; } - private void drawMoveStatistics(Graphics2D g, int posX, int posY, int width, int height) { - if (width < 0 || height < 0) - return; // we don't have enough space - - double lastWR = 50; // winrate the previous move - boolean validLastWinrate = false; // whether it was actually calculated - BoardData lastNode = Lizzie.board.getHistory().getPrevious(); - if (lastNode != null && lastNode.playouts > 0) { - lastWR = lastNode.winrate; - validLastWinrate = true; - } - - Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); - double curWR = stats.maxWinrate; // winrate on this move - boolean validWinrate = (stats.totalPlayouts > 0); // and whether it was actually calculated - if (isPlayingAgainstLeelaz && playerIsBlack == !Lizzie.board.getHistory().getData().blackToPlay) - validWinrate = false; - - if (!validWinrate) { - curWR = 100 - lastWR; // display last move's winrate for now (with color difference) - } - double whiteWR, blackWR; - if (Lizzie.board.getData().blackToPlay) { - blackWR = curWR; - } else { - blackWR = 100 - curWR; - } - - whiteWR = 100 - blackWR; - - // Background rectangle - g.setColor(new Color(0, 0, 0, 130)); - g.fillRect(posX, posY, width, height); - - // border. does not include bottom edge - int strokeRadius = 3; - g.setStroke(new BasicStroke(2 * strokeRadius)); - g.drawLine(posX + strokeRadius, posY + strokeRadius, - posX - strokeRadius + width, posY + strokeRadius); - g.drawLine(posX + strokeRadius, posY + 3 * strokeRadius, - posX + strokeRadius, posY - strokeRadius + height); - g.drawLine(posX - strokeRadius + width, posY + 3 * strokeRadius, - posX - strokeRadius + width, posY - strokeRadius + height); - - // resize the box now so it's inside the border - posX += 2 * strokeRadius; - posY += 2 * strokeRadius; - width -= 4 * strokeRadius; - height -= 4 * strokeRadius; - - // Title - strokeRadius = 2; - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g.setColor(Color.WHITE); - setPanelFont(g, (int) (Math.min(width, height) * 0.2)); - - // Last move - if (validLastWinrate && validWinrate) { - String text; - if(Lizzie.config.handicapInsteadOfWinrate) { - text=String.format(": %.2f", Lizzie.leelaz.winrateToHandicap(100-curWR) - Lizzie.leelaz.winrateToHandicap(lastWR)); - } else { - text=String.format(": %.1f%%", 100 - lastWR - curWR); - } - - g.drawString(resourceBundle.getString("LizzieFrame.display.lastMove") + - text, - posX + 2 * strokeRadius, - posY + height - 2 * strokeRadius); // - font.getSize()); - } else { - // I think it's more elegant to just not display anything when we don't have - // valid data --dfannius - // g.drawString(resourceBundle.getString("LizzieFrame.display.lastMove") + ": ?%", - // posX + 2 * strokeRadius, posY + height - 2 * strokeRadius); - } - - if (validWinrate || validLastWinrate) { - int maxBarwidth = (int) (width); - int barWidthB = (int) (blackWR * maxBarwidth / 100); - int barWidthW = (int) (whiteWR * maxBarwidth / 100); - int barPosY = posY + height / 3; - int barPosxB = (int) (posX); - int barPosxW = barPosxB + barWidthB; - int barHeight = height / 3; - - // Draw winrate bars - g.fillRect(barPosxW, barPosY, barWidthW, barHeight); - g.setColor(Color.BLACK); - g.fillRect(barPosxB, barPosY, barWidthB, barHeight); - - // Show percentage above bars - g.setColor(Color.WHITE); - g.drawString(String.format("%.1f%%", blackWR), - barPosxB + 2 * strokeRadius, - posY + barHeight - 2 * strokeRadius); - String winString = String.format("%.1f%%", whiteWR); - int sw = g.getFontMetrics().stringWidth(winString); - g.drawString(winString, - barPosxB + maxBarwidth - sw - 2 * strokeRadius, - posY + barHeight - 2 * strokeRadius); - - g.setColor(Color.GRAY); - Stroke oldstroke = g.getStroke(); - Stroke dashed = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, - new float[]{4}, 0); - g.setStroke(dashed); - - for (int i = 1; i <= winRateGridLines; i++) { - int x = barPosxB + (int) (i * (maxBarwidth / (winRateGridLines + 1))); - g.drawLine(x, barPosY, x, barPosY + barHeight); - } - g.setStroke(oldstroke); - } + whiteWR = 100 - blackWR; + + // Background rectangle + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(posX, posY, width, height); + + // border. does not include bottom edge + int strokeRadius = 3; + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.drawLine( + posX + strokeRadius, posY + strokeRadius, + posX - strokeRadius + width, posY + strokeRadius); + g.drawLine( + posX + strokeRadius, posY + 3 * strokeRadius, + posX + strokeRadius, posY - strokeRadius + height); + g.drawLine( + posX - strokeRadius + width, posY + 3 * strokeRadius, + posX - strokeRadius + width, posY - strokeRadius + height); + + // resize the box now so it's inside the border + posX += 2 * strokeRadius; + posY += 2 * strokeRadius; + width -= 4 * strokeRadius; + height -= 4 * strokeRadius; + + // Title + strokeRadius = 2; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(Color.WHITE); + setPanelFont(g, (int) (Math.min(width, height) * 0.2)); + + // Last move + if (validLastWinrate && validWinrate) { + String text; + if (Lizzie.config.handicapInsteadOfWinrate) { + double currHandicapedWR = Lizzie.leelaz.winrateToHandicap(100 - curWR); + double lastHandicapedWR = Lizzie.leelaz.winrateToHandicap(lastWR); + text = String.format(": %.2f", currHandicapedWR - lastHandicapedWR); + } else { + text = String.format(": %.1f%%", 100 - lastWR - curWR); + } + + g.drawString( + resourceBundle.getString("LizzieFrame.display.lastMove") + text, + posX + 2 * strokeRadius, + posY + height - 2 * strokeRadius); // - font.getSize()); + } else { + // I think it's more elegant to just not display anything when we don't have + // valid data --dfannius + // g.drawString(resourceBundle.getString("LizzieFrame.display.lastMove") + ": ?%", + // posX + 2 * strokeRadius, posY + height - 2 * strokeRadius); } - private void drawCaptured(Graphics2D g, int posX, int posY, int width, int height) { - // Draw border - g.setColor(new Color(0, 0, 0, 130)); - g.fillRect(posX, posY, width, height); - - // border. does not include bottom edge - int strokeRadius = 3; - g.setStroke(new BasicStroke(2 * strokeRadius)); - g.drawLine(posX + strokeRadius, posY + strokeRadius, - posX - strokeRadius + width, posY + strokeRadius); - g.drawLine(posX + strokeRadius, posY + 3 * strokeRadius, - posX + strokeRadius, posY - strokeRadius + height); - g.drawLine(posX - strokeRadius + width, posY + 3 * strokeRadius, - posX - strokeRadius + width, posY - strokeRadius + height); - - // Draw middle line - g.drawLine(posX - strokeRadius + width/2, posY + 3 * strokeRadius, - posX - strokeRadius + width/2, posY - strokeRadius + height); - g.setColor(Color.white); - - // Draw black and white "stone" - int diam = height / 3; - int smallDiam = diam / 2; - int bdiam = diam, wdiam = diam; - if (Lizzie.board.inScoreMode()) { - // do nothing - } else if (Lizzie.board.getHistory().isBlacksTurn()) { - wdiam = smallDiam; - } else { - bdiam = smallDiam; - } - g.setColor(Color.black); - g.fillOval(posX + width/4 - bdiam/2, posY + height*3/8 + (diam - bdiam)/2, bdiam, bdiam); - - g.setColor(Color.WHITE); - g.fillOval(posX + width*3/4 - wdiam/2, posY + height*3/8 + (diam - wdiam)/2, wdiam, wdiam); - - // Draw captures - String bval, wval; - setPanelFont(g, (float) (width * 0.06)); - if (Lizzie.board.inScoreMode()) - { - double score[] = Lizzie.board.getScore(Lizzie.board.scoreStones()); - bval = String.format("%.0f", score[0]); - wval = String.format("%.1f", score[1]); - } else { - bval = String.format("%d", Lizzie.board.getData().blackCaptures); - wval = String.format("%d", Lizzie.board.getData().whiteCaptures); - } - - g.setColor(Color.WHITE); - int bw = g.getFontMetrics().stringWidth(bval); - int ww = g.getFontMetrics().stringWidth(wval); - boolean largeSubBoard = Lizzie.config.showLargeSubBoard(); - int bx = (largeSubBoard ? diam : - bw/2); - int wx = (largeSubBoard ? bx : - ww/2); - - g.drawString(bval, - posX + width/4 + bx, - posY + height*7/8); - g.drawString(wval, - posX + width*3/4 + wx, - posY + height*7/8); + if (validWinrate || validLastWinrate) { + int maxBarwidth = (int) (width); + int barWidthB = (int) (blackWR * maxBarwidth / 100); + int barWidthW = (int) (whiteWR * maxBarwidth / 100); + int barPosY = posY + height / 3; + int barPosxB = (int) (posX); + int barPosxW = barPosxB + barWidthB; + int barHeight = height / 3; + + // Draw winrate bars + g.fillRect(barPosxW, barPosY, barWidthW, barHeight); + g.setColor(Color.BLACK); + g.fillRect(barPosxB, barPosY, barWidthB, barHeight); + + // Show percentage above bars + g.setColor(Color.WHITE); + g.drawString( + String.format("%.1f%%", blackWR), + barPosxB + 2 * strokeRadius, + posY + barHeight - 2 * strokeRadius); + String winString = String.format("%.1f%%", whiteWR); + int sw = g.getFontMetrics().stringWidth(winString); + g.drawString( + winString, + barPosxB + maxBarwidth - sw - 2 * strokeRadius, + posY + barHeight - 2 * strokeRadius); + + g.setColor(Color.GRAY); + Stroke oldstroke = g.getStroke(); + Stroke dashed = + new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] {4}, 0); + g.setStroke(dashed); + + for (int i = 1; i <= winRateGridLines; i++) { + int x = barPosxB + (int) (i * (maxBarwidth / (winRateGridLines + 1))); + g.drawLine(x, barPosY, x, barPosY + barHeight); + } + g.setStroke(oldstroke); } - - private void setPanelFont(Graphics2D g, float size) { - Font font = OpenSansRegularBase.deriveFont(Font.PLAIN, size); - g.setFont(font); + } + + private void drawCaptured(Graphics2D g, int posX, int posY, int width, int height) { + // Draw border + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(posX, posY, width, height); + + // border. does not include bottom edge + int strokeRadius = 3; + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.drawLine( + posX + strokeRadius, posY + strokeRadius, posX - strokeRadius + width, posY + strokeRadius); + g.drawLine( + posX + strokeRadius, + posY + 3 * strokeRadius, + posX + strokeRadius, + posY - strokeRadius + height); + g.drawLine( + posX - strokeRadius + width, + posY + 3 * strokeRadius, + posX - strokeRadius + width, + posY - strokeRadius + height); + + // Draw middle line + g.drawLine( + posX - strokeRadius + width / 2, + posY + 3 * strokeRadius, + posX - strokeRadius + width / 2, + posY - strokeRadius + height); + g.setColor(Color.white); + + // Draw black and white "stone" + int diam = height / 3; + int smallDiam = diam / 2; + int bdiam = diam, wdiam = diam; + if (Lizzie.board.inScoreMode()) { + // do nothing + } else if (Lizzie.board.getHistory().isBlacksTurn()) { + wdiam = smallDiam; + } else { + bdiam = smallDiam; } - - /** - * Checks whether or not something was clicked and performs the appropriate action - * - * @param x x coordinate - * @param y y coordinate - */ - public void onClicked(int x, int y) { - // check for board click - int[] boardCoordinates = boardRenderer.convertScreenToCoordinates(x, y); - int moveNumber = winrateGraph.moveNumber(x, y); - - if (boardCoordinates != null) { - if (Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); - if (!isPlayingAgainstLeelaz || (playerIsBlack == Lizzie.board.getData().blackToPlay)) - Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); - } - if (Lizzie.config.showWinrate && moveNumber >= 0) { - isPlayingAgainstLeelaz = false; - Lizzie.board.goToMoveNumberBeyondBranch(moveNumber); - } - if (Lizzie.config.showSubBoard && subBoardRenderer.isInside(x, y)) { - Lizzie.config.toggleLargeSubBoard(); - } - repaint(); + g.setColor(Color.black); + g.fillOval( + posX + width / 4 - bdiam / 2, posY + height * 3 / 8 + (diam - bdiam) / 2, bdiam, bdiam); + + g.setColor(Color.WHITE); + g.fillOval( + posX + width * 3 / 4 - wdiam / 2, posY + height * 3 / 8 + (diam - wdiam) / 2, wdiam, wdiam); + + // Draw captures + String bval, wval; + setPanelFont(g, (float) (width * 0.06)); + if (Lizzie.board.inScoreMode()) { + double score[] = Lizzie.board.getScore(Lizzie.board.scoreStones()); + bval = String.format("%.0f", score[0]); + wval = String.format("%.1f", score[1]); + } else { + bval = String.format("%d", Lizzie.board.getData().blackCaptures); + wval = String.format("%d", Lizzie.board.getData().whiteCaptures); } - public boolean playCurrentVariation() { - List variation = boardRenderer.variation; - boolean onVariation = (variation != null); - if (onVariation) { - for (int i = 0; i < variation.size(); i++) { - int[] boardCoordinates = Board.convertNameToCoordinates(variation.get(i)); - if (boardCoordinates != null) - Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); - } - } - return onVariation; + g.setColor(Color.WHITE); + int bw = g.getFontMetrics().stringWidth(bval); + int ww = g.getFontMetrics().stringWidth(wval); + boolean largeSubBoard = Lizzie.config.showLargeSubBoard(); + int bx = (largeSubBoard ? diam : -bw / 2); + int wx = (largeSubBoard ? bx : -ww / 2); + + g.drawString(bval, posX + width / 4 + bx, posY + height * 7 / 8); + g.drawString(wval, posX + width * 3 / 4 + wx, posY + height * 7 / 8); + } + + private void setPanelFont(Graphics2D g, float size) { + Font font = OpenSansRegularBase.deriveFont(Font.PLAIN, size); + g.setFont(font); + } + + /** + * Checks whether or not something was clicked and performs the appropriate action + * + * @param x x coordinate + * @param y y coordinate + */ + public void onClicked(int x, int y) { + // check for board click + int[] boardCoordinates = boardRenderer.convertScreenToCoordinates(x, y); + int moveNumber = winrateGraph.moveNumber(x, y); + + if (boardCoordinates != null) { + if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); + if (!isPlayingAgainstLeelaz || (playerIsBlack == Lizzie.board.getData().blackToPlay)) + Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); } - - public void playBestMove() { - String bestCoordinateName = boardRenderer.bestMoveCoordinateName(); - if (bestCoordinateName == null) - return; - int[] boardCoordinates = Board.convertNameToCoordinates(bestCoordinateName); - if (boardCoordinates != null) { - Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); - } + if (Lizzie.config.showWinrate && moveNumber >= 0) { + isPlayingAgainstLeelaz = false; + Lizzie.board.goToMoveNumberBeyondBranch(moveNumber); } - - public void onMouseMoved(int x, int y) { - int[] newMouseHoverCoordinate = boardRenderer.convertScreenToCoordinates(x, y); - if (mouseHoverCoordinate != null && newMouseHoverCoordinate != null && (mouseHoverCoordinate[0] != newMouseHoverCoordinate[0] || mouseHoverCoordinate[1] != newMouseHoverCoordinate[1])) { - mouseHoverCoordinate = newMouseHoverCoordinate; - repaint(); - } else { - mouseHoverCoordinate = newMouseHoverCoordinate; - } + if (Lizzie.config.showSubBoard && subBoardRenderer.isInside(x, y)) { + Lizzie.config.toggleLargeSubBoard(); } - - public void onMouseDragged(int x, int y) { - int moveNumber = winrateGraph.moveNumber(x, y); - if (Lizzie.config.showWinrate && moveNumber >= 0) { - if (Lizzie.board.goToMoveNumberWithinBranch(moveNumber)) { - repaint(); - } - } + repaint(); + } + + public boolean playCurrentVariation() { + List variation = boardRenderer.variation; + boolean onVariation = (variation != null); + if (onVariation) { + for (int i = 0; i < variation.size(); i++) { + int[] boardCoordinates = Board.convertNameToCoordinates(variation.get(i)); + if (boardCoordinates != null) Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); + } } - - public void toggleCoordinates() { - showCoordinates = !showCoordinates; + return onVariation; + } + + public void playBestMove() { + String bestCoordinateName = boardRenderer.bestMoveCoordinateName(); + if (bestCoordinateName == null) return; + int[] boardCoordinates = Board.convertNameToCoordinates(bestCoordinateName); + if (boardCoordinates != null) { + Lizzie.board.place(boardCoordinates[0], boardCoordinates[1]); } + } - public void setPlayers(String whitePlayer, String blackPlayer) { - setTitle(String.format("%s (%s [W] vs %s [B])", DEFAULT_TITLE, - whitePlayer, blackPlayer)); + public void onMouseMoved(int x, int y) { + int[] c = boardRenderer.convertScreenToCoordinates(x, y); + if (c != null && !isMouseOver(c[0], c[1])) { + repaint(); } - - private void setDisplayedBranchLength(int n) { - boardRenderer.setDisplayedBranchLength(n); + mouseOverCoordinate = c; + } + + public boolean isMouseOver(int x, int y) { + return mouseOverCoordinate != null + && mouseOverCoordinate[0] == x + && mouseOverCoordinate[1] == x; + } + + public void onMouseDragged(int x, int y) { + int moveNumber = winrateGraph.moveNumber(x, y); + if (Lizzie.config.showWinrate && moveNumber >= 0) { + if (Lizzie.board.goToMoveNumberWithinBranch(moveNumber)) { + repaint(); + } } - - public void startRawBoard() { - boolean onBranch = boardRenderer.isShowingBranch(); - int n = (onBranch ? 1 : BoardRenderer.SHOW_RAW_BOARD); - boardRenderer.setDisplayedBranchLength(n); + } + + /** + * Process Comment Mouse Wheel Moved + * + * @return true when the scroll event was processed by this method + */ + public boolean processCommentMouseWheelMoved(MouseWheelEvent e) { + if (Lizzie.config.showComment + && commentRect != null + && commentRect.contains(e.getX(), e.getY())) { + scrollPane.dispatchEvent(e); + createCommentImage(true, 0, 0); + getGraphics() + .drawImage( + commentImage, + commentRect.x, + commentRect.y, + commentRect.width, + commentRect.height, + null); + return true; + } else { + return false; } - - public void stopRawBoard() { - boardRenderer.setDisplayedBranchLength(BoardRenderer.SHOW_NORMAL_BOARD); + } + + /** + * Create comment cached image + * + * @param forceRefresh + * @param w + * @param h + */ + public void createCommentImage(boolean forceRefresh, int w, int h) { + if (forceRefresh + || commentImage == null + || scrollPane.getWidth() != w + || scrollPane.getHeight() != h) { + if (w > 0 && h > 0) { + scrollPane.setSize(w, h); + } + commentImage = + new BufferedImage( + scrollPane.getWidth(), scrollPane.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = commentImage.createGraphics(); + scrollPane.doLayout(); + scrollPane.addNotify(); + scrollPane.validate(); + scrollPane.printAll(g2); + g2.dispose(); } - - public boolean incrementDisplayedBranchLength(int n) { - return boardRenderer.incrementDisplayedBranchLength(n); + } + + private void autosaveMaybe() { + int interval = + Lizzie.config.config.getJSONObject("ui").getInt("autosave-interval-seconds") * 1000; + long currentTime = System.currentTimeMillis(); + if (interval > 0 && currentTime - lastAutosaveTime >= interval) { + Lizzie.board.autosave(); + lastAutosaveTime = currentTime; } - - public void resetTitle() { - setTitle(DEFAULT_TITLE); + } + + public void toggleCoordinates() { + showCoordinates = !showCoordinates; + } + + public void setPlayers(String whitePlayer, String blackPlayer) { + this.playerTitle = String.format("(%s [W] vs %s [B])", whitePlayer, blackPlayer); + this.updateTitle(); + } + + public void updateTitle() { + StringBuilder sb = new StringBuilder(DEFAULT_TITLE); + sb.append(this.playerTitle != null ? " " + this.playerTitle.trim() : ""); + sb.append( + Lizzie.leelaz.engineCommand() != null ? " [" + Lizzie.leelaz.engineCommand() + "]" : ""); + setTitle(sb.toString()); + } + + private void setDisplayedBranchLength(int n) { + boardRenderer.setDisplayedBranchLength(n); + } + + public void startRawBoard() { + boolean onBranch = boardRenderer.isShowingBranch(); + int n = (onBranch ? 1 : BoardRenderer.SHOW_RAW_BOARD); + boardRenderer.setDisplayedBranchLength(n); + } + + public void stopRawBoard() { + boardRenderer.setDisplayedBranchLength(BoardRenderer.SHOW_NORMAL_BOARD); + } + + public boolean incrementDisplayedBranchLength(int n) { + return boardRenderer.incrementDisplayedBranchLength(n); + } + + public void resetTitle() { + this.playerTitle = null; + this.updateTitle(); + } + + public void copySgf() { + try { + // Get sgf content from game + String sgfContent = SGFParser.saveToString(); + + // Save to clipboard + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable transferableString = new StringSelection(sgfContent); + clipboard.setContents(transferableString, null); + } catch (Exception e) { + e.printStackTrace(); } - - public void copySgf() { - try { - // Get sgf content from game - String sgfContent = SGFParser.saveToString(); - - // Save to clipboard - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - Transferable transferableString = new StringSelection(sgfContent); - clipboard.setContents(transferableString, null); - } catch (Exception e) { - e.printStackTrace(); + } + + public void pasteSgf() { + try { + String sgfContent = null; + // Get string from clipboard + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable clipboardContents = clipboard.getContents(null); + if (clipboardContents != null) { + if (clipboardContents.isDataFlavorSupported(DataFlavor.stringFlavor)) { + sgfContent = (String) clipboardContents.getTransferData(DataFlavor.stringFlavor); } + } + + // load game contents from sgf string + if (sgfContent != null && !sgfContent.isEmpty()) { + SGFParser.loadFromString(sgfContent); + } + } catch (Exception e) { + e.printStackTrace(); } - - public void pasteSgf() { - try { - String sgfContent = null; - // Get string from clipboard - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - Transferable clipboardContents = clipboard.getContents(null); - if (clipboardContents != null) { - if (clipboardContents.isDataFlavorSupported(DataFlavor.stringFlavor)) { - sgfContent = (String) clipboardContents.getTransferData(DataFlavor.stringFlavor); - } - } - - // load game contents from sgf string - if (sgfContent != null && !sgfContent.isEmpty()) { - SGFParser.loadFromString(sgfContent); - } - } catch (Exception e) { - e.printStackTrace(); - } + } + + public void increaseMaxAlpha(int k) { + boardRenderer.increaseMaxAlpha(k); + } + + /** + * Draw the Comment of the Sgf file + * + * @param g + * @param x + * @param y + * @param w + * @param h + * @param full + * @return + */ + private int drawComment(Graphics2D g, int x, int y, int w, int h, boolean full) { + String comment = + (Lizzie.board.getHistory().getData() != null + && Lizzie.board.getHistory().getData().comment != null) + ? Lizzie.board.getHistory().getData().comment + : ""; + int cHeight = full ? h : (int) (h * 0.5); + int fontSize = (int) (Math.min(getWidth(), getHeight()) * 0.0294); + if (Lizzie.config.commentFontSize > 0) { + fontSize = Lizzie.config.commentFontSize; + } else if (fontSize < 16) { + fontSize = 16; } + Font font = new Font(systemDefaultFontName, Font.PLAIN, fontSize); + commentPane.setFont(font); + commentPane.setText(comment); + commentPane.setSize(w, cHeight); + createCommentImage(comment != null && !comment.equals(this.cachedComment), w, cHeight); + commentRect = + new Rectangle(x, y + (h - cHeight), scrollPane.getWidth(), scrollPane.getHeight()); + g.drawImage( + commentImage, commentRect.x, commentRect.y, commentRect.width, commentRect.height, null); + cachedComment = comment; + return cHeight; + } } diff --git a/src/main/java/featurecat/lizzie/gui/NewGameDialog.java b/src/main/java/featurecat/lizzie/gui/NewGameDialog.java index 1d3a8389a..668c2a6ed 100644 --- a/src/main/java/featurecat/lizzie/gui/NewGameDialog.java +++ b/src/main/java/featurecat/lizzie/gui/NewGameDialog.java @@ -5,185 +5,185 @@ package featurecat.lizzie.gui; import featurecat.lizzie.analysis.GameInfo; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; import java.awt.*; import java.text.DecimalFormat; import java.text.ParseException; +import javax.swing.*; +import javax.swing.border.EmptyBorder; -/** - * @author unknown - */ +/** @author unknown */ public class NewGameDialog extends JDialog { - // create formatters - public static final DecimalFormat FORMAT_KOMI = new DecimalFormat("#0.0"); - public static final DecimalFormat FORMAT_HANDICAP = new DecimalFormat("0"); - public static final JLabel PLACEHOLDER = new JLabel(""); - - static { - FORMAT_HANDICAP.setMaximumIntegerDigits(1); + // create formatters + public static final DecimalFormat FORMAT_KOMI = new DecimalFormat("#0.0"); + public static final DecimalFormat FORMAT_HANDICAP = new DecimalFormat("0"); + public static final JLabel PLACEHOLDER = new JLabel(""); + + static { + FORMAT_HANDICAP.setMaximumIntegerDigits(1); + } + + private JPanel dialogPane = new JPanel(); + private JPanel contentPanel = new JPanel(); + private JPanel buttonBar = new JPanel(); + private JButton okButton = new JButton(); + + private JCheckBox checkBoxPlayerIsBlack; + private JTextField textFieldBlack; + private JTextField textFieldWhite; + private JTextField textFieldKomi; + private JTextField textFieldHandicap; + + private boolean cancelled = true; + private GameInfo gameInfo; + + public NewGameDialog() { + initComponents(); + } + + private void initComponents() { + setMinimumSize(new Dimension(100, 100)); + setResizable(false); + setTitle("New Game"); + setModal(true); + + Container contentPane = getContentPane(); + contentPane.setLayout(new BorderLayout()); + + initDialogPane(contentPane); + + pack(); + setLocationRelativeTo(getOwner()); + } + + private void initDialogPane(Container contentPane) { + dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); + dialogPane.setLayout(new BorderLayout()); + + initContentPanel(); + initButtonBar(); + + contentPane.add(dialogPane, BorderLayout.CENTER); + } + + private void initContentPanel() { + GridLayout gridLayout = new GridLayout(5, 2, 4, 4); + contentPanel.setLayout(gridLayout); + + checkBoxPlayerIsBlack = new JCheckBox("Play black?", true); + checkBoxPlayerIsBlack.addChangeListener(evt -> togglePlayerIsBlack()); + textFieldWhite = new JTextField(); + textFieldBlack = new JTextField(); + textFieldKomi = new JFormattedTextField(FORMAT_KOMI); + textFieldHandicap = new JFormattedTextField(FORMAT_HANDICAP); + textFieldHandicap.addPropertyChangeListener(evt -> modifyHandicap()); + + contentPanel.add(checkBoxPlayerIsBlack); + contentPanel.add(PLACEHOLDER); + contentPanel.add(new JLabel("Black")); + contentPanel.add(textFieldBlack); + contentPanel.add(new JLabel("White")); + contentPanel.add(textFieldWhite); + contentPanel.add(new JLabel("Komi")); + contentPanel.add(textFieldKomi); + contentPanel.add(new JLabel("Handicap")); + contentPanel.add(textFieldHandicap); + + textFieldKomi.setEnabled(false); + + dialogPane.add(contentPanel, BorderLayout.CENTER); + } + + private void togglePlayerIsBlack() { + JTextField humanTextField = playerIsBlack() ? textFieldBlack : textFieldWhite; + JTextField computerTextField = playerIsBlack() ? textFieldWhite : textFieldBlack; + + humanTextField.setEnabled(true); + humanTextField.setText(GameInfo.DEFAULT_NAME_HUMAN_PLAYER); + computerTextField.setEnabled(false); + computerTextField.setText(GameInfo.DEFAULT_NAME_CPU_PLAYER); + } + + private void modifyHandicap() { + try { + int handicap = FORMAT_HANDICAP.parse(textFieldHandicap.getText()).intValue(); + if (handicap < 0) throw new IllegalArgumentException(); + + textFieldKomi.setText(FORMAT_KOMI.format(GameInfo.DEFAULT_KOMI)); + } catch (ParseException | RuntimeException e) { + // do not correct user mistakes } - - private JPanel dialogPane = new JPanel(); - private JPanel contentPanel = new JPanel(); - private JPanel buttonBar = new JPanel(); - private JButton okButton = new JButton(); - - private JCheckBox checkBoxPlayerIsBlack; - private JTextField textFieldBlack; - private JTextField textFieldWhite; - private JTextField textFieldKomi; - private JTextField textFieldHandicap; - - private boolean cancelled = true; - private GameInfo gameInfo; - - public NewGameDialog() { - initComponents(); + } + + private void initButtonBar() { + buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); + buttonBar.setLayout(new GridBagLayout()); + ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[] {0, 80}; + ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[] {1.0, 0.0}; + + // ---- okButton ---- + okButton.setText("OK"); + okButton.addActionListener(e -> apply()); + + int center = GridBagConstraints.CENTER; + int both = GridBagConstraints.BOTH; + buttonBar.add( + okButton, + new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, center, both, new Insets(0, 0, 0, 0), 0, 0)); + + dialogPane.add(buttonBar, BorderLayout.SOUTH); + } + + public void apply() { + try { + // validate data + String playerBlack = textFieldBlack.getText(); + String playerWhite = textFieldWhite.getText(); + double komi = FORMAT_KOMI.parse(textFieldKomi.getText()).doubleValue(); + int handicap = FORMAT_HANDICAP.parse(textFieldHandicap.getText()).intValue(); + + // apply new values + gameInfo.setPlayerBlack(playerBlack); + gameInfo.setPlayerWhite(playerWhite); + gameInfo.setKomi(komi); + gameInfo.setHandicap(handicap); + + // close window + cancelled = false; + setVisible(false); + } catch (ParseException e) { + // hide input mistakes. } - - private void initComponents() { - setMinimumSize(new Dimension(100, 100)); - setResizable(false); - setTitle("New Game"); - setModal(true); - - Container contentPane = getContentPane(); - contentPane.setLayout(new BorderLayout()); - - initDialogPane(contentPane); - - pack(); - setLocationRelativeTo(getOwner()); - } - - private void initDialogPane(Container contentPane) { - dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12)); - dialogPane.setLayout(new BorderLayout()); - - initContentPanel(); - initButtonBar(); - - contentPane.add(dialogPane, BorderLayout.CENTER); - } - - private void initContentPanel() { - GridLayout gridLayout = new GridLayout(5, 2, 4, 4); - contentPanel.setLayout(gridLayout); - - checkBoxPlayerIsBlack = new JCheckBox("Play black?", true); - checkBoxPlayerIsBlack.addChangeListener(evt -> togglePlayerIsBlack()); - textFieldWhite = new JTextField(); - textFieldBlack = new JTextField(); - textFieldKomi = new JFormattedTextField(FORMAT_KOMI); - textFieldHandicap = new JFormattedTextField(FORMAT_HANDICAP); - textFieldHandicap.addPropertyChangeListener(evt -> modifyHandicap()); - - contentPanel.add(checkBoxPlayerIsBlack); - contentPanel.add(PLACEHOLDER); - contentPanel.add(new JLabel("Black")); - contentPanel.add(textFieldBlack); - contentPanel.add(new JLabel("White")); - contentPanel.add(textFieldWhite); - contentPanel.add(new JLabel("Komi")); - contentPanel.add(textFieldKomi); - contentPanel.add(new JLabel("Handicap")); - contentPanel.add(textFieldHandicap); - - textFieldKomi.setEnabled(false); - - dialogPane.add(contentPanel, BorderLayout.CENTER); - } - - private void togglePlayerIsBlack() { - JTextField humanTextField = playerIsBlack() ? textFieldBlack : textFieldWhite; - JTextField computerTextField = playerIsBlack() ? textFieldWhite : textFieldBlack; - - humanTextField.setEnabled(true); - humanTextField.setText(GameInfo.DEFAULT_NAME_HUMAN_PLAYER); - computerTextField.setEnabled(false); - computerTextField.setText(GameInfo.DEFAULT_NAME_CPU_PLAYER); - } - - private void modifyHandicap() { - try { - int handicap = FORMAT_HANDICAP.parse(textFieldHandicap.getText()).intValue(); - if (handicap < 0) throw new IllegalArgumentException(); - - textFieldKomi.setText(FORMAT_KOMI.format(GameInfo.DEFAULT_KOMI)); - } catch (ParseException | RuntimeException e) { - // do not correct user mistakes - } - } - - private void initButtonBar() { - buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0)); - buttonBar.setLayout(new GridBagLayout()); - ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[]{0, 80}; - ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[]{1.0, 0.0}; - - //---- okButton ---- - okButton.setText("OK"); - okButton.addActionListener(e -> apply()); - - buttonBar.add(okButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), 0, 0)); - - dialogPane.add(buttonBar, BorderLayout.SOUTH); - } - - public void apply() { - try { - // validate data - String playerBlack = textFieldBlack.getText(); - String playerWhite = textFieldWhite.getText(); - double komi = FORMAT_KOMI.parse(textFieldKomi.getText()).doubleValue(); - int handicap = FORMAT_HANDICAP.parse(textFieldHandicap.getText()).intValue(); - - // apply new values - gameInfo.setPlayerBlack(playerBlack); - gameInfo.setPlayerWhite(playerWhite); - gameInfo.setKomi(komi); - gameInfo.setHandicap(handicap); - - // close window - cancelled = false; - setVisible(false); - } catch (ParseException e) { - // hide input mistakes. - } - } - - public void setGameInfo(GameInfo gameInfo) { - this.gameInfo = gameInfo; - - textFieldBlack.setText(gameInfo.getPlayerBlack()); - textFieldWhite.setText(gameInfo.getPlayerWhite()); - textFieldHandicap.setText(FORMAT_HANDICAP.format(gameInfo.getHandicap())); - textFieldKomi.setText(FORMAT_KOMI.format(gameInfo.getKomi())); - - // update player names - togglePlayerIsBlack(); - } - - public boolean playerIsBlack() { - return checkBoxPlayerIsBlack.isSelected(); - } - - public boolean isCancelled() { - return cancelled; - } - - public static void main(String[] args) { - EventQueue.invokeLater(() -> { - try { - NewGameDialog window = new NewGameDialog(); - window.setVisible(true); - } catch (Exception e) { - e.printStackTrace(); - } + } + + public void setGameInfo(GameInfo gameInfo) { + this.gameInfo = gameInfo; + + textFieldBlack.setText(gameInfo.getPlayerBlack()); + textFieldWhite.setText(gameInfo.getPlayerWhite()); + textFieldHandicap.setText(FORMAT_HANDICAP.format(gameInfo.getHandicap())); + textFieldKomi.setText(FORMAT_KOMI.format(gameInfo.getKomi())); + + // update player names + togglePlayerIsBlack(); + } + + public boolean playerIsBlack() { + return checkBoxPlayerIsBlack.isSelected(); + } + + public boolean isCancelled() { + return cancelled; + } + + public static void main(String[] args) { + EventQueue.invokeLater( + () -> { + try { + NewGameDialog window = new NewGameDialog(); + window.setVisible(true); + } catch (Exception e) { + e.printStackTrace(); + } }); - } + } } diff --git a/src/main/java/featurecat/lizzie/gui/VariationTree.java b/src/main/java/featurecat/lizzie/gui/VariationTree.java index 6c873dc0e..aef810e2b 100644 --- a/src/main/java/featurecat/lizzie/gui/VariationTree.java +++ b/src/main/java/featurecat/lizzie/gui/VariationTree.java @@ -3,143 +3,166 @@ import featurecat.lizzie.Lizzie; import featurecat.lizzie.rules.BoardHistoryList; import featurecat.lizzie.rules.BoardHistoryNode; - import java.awt.*; import java.util.ArrayList; public class VariationTree { - private int YSPACING; - private int XSPACING; - private int DOT_DIAM = 11; // Should be odd number - - private ArrayList laneUsageList; - private BoardHistoryNode curMove; + private int YSPACING; + private int XSPACING; + private int DOT_DIAM = 11; // Should be odd number + + private ArrayList laneUsageList; + private BoardHistoryNode curMove; + + public VariationTree() { + laneUsageList = new ArrayList(); + } + + public void drawTree( + Graphics2D g, + int posx, + int posy, + int startLane, + int maxposy, + BoardHistoryNode startNode, + int variationNumber, + boolean isMain) { + if (isMain) g.setColor(Color.white); + else g.setColor(Color.gray.brighter()); + + // Finds depth on leftmost variation of this tree + int depth = BoardHistoryList.getDepth(startNode) + 1; + int lane = startLane; + // Figures out how far out too the right (which lane) we have to go not to collide with other + // variations + while (lane < laneUsageList.size() + && laneUsageList.get(lane) <= startNode.getData().moveNumber + depth) { + // laneUsageList keeps a list of how far down it is to a variation in the different "lanes" + laneUsageList.set(lane, startNode.getData().moveNumber - 1); + lane++; + } + if (lane >= laneUsageList.size()) { + laneUsageList.add(0); + } + if (variationNumber > 1) laneUsageList.set(lane - 1, startNode.getData().moveNumber - 1); + laneUsageList.set(lane, startNode.getData().moveNumber); + + // At this point, lane contains the lane we should use (the main branch is in lane 0) + + BoardHistoryNode cur = startNode; + int curposx = posx + lane * XSPACING; + int dotoffset = DOT_DIAM / 2; + + // Draw line back to main branch + if (lane > 0) { + if (lane - startLane > 0 || variationNumber > 1) { + // Need a horizontal and an angled line + g.drawLine( + curposx + dotoffset, + posy + dotoffset, + curposx + dotoffset - XSPACING, + posy + dotoffset - YSPACING); + g.drawLine( + posx + (startLane - variationNumber) * XSPACING + 2 * dotoffset, + posy - YSPACING + dotoffset, + curposx + dotoffset - XSPACING, + posy + dotoffset - YSPACING); + } else { + // Just an angled line + g.drawLine( + curposx + dotoffset, + posy + dotoffset, + curposx + 2 * dotoffset - XSPACING, + posy + 2 * dotoffset - YSPACING); + } + } - public VariationTree() - { - laneUsageList = new ArrayList(); + // Draw all the nodes and lines in this lane (not variations) + Color curcolor = g.getColor(); + if (startNode == curMove) { + g.setColor(Color.green.brighter().brighter()); + } + if (startNode.previous() != null) { + g.fillOval(curposx, posy, DOT_DIAM, DOT_DIAM); + g.setColor(Color.BLACK); + g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); + } else { + g.fillRect(curposx, posy, DOT_DIAM, DOT_DIAM); + g.setColor(Color.BLACK); + g.drawRect(curposx, posy, DOT_DIAM, DOT_DIAM); + } + g.setColor(curcolor); + + // Draw main line + while (cur.next() != null && posy + YSPACING < maxposy) { + posy += YSPACING; + cur = cur.next(); + if (cur == curMove) { + g.setColor(Color.green.brighter().brighter()); + } + g.fillOval(curposx, posy, DOT_DIAM, DOT_DIAM); + g.setColor(Color.BLACK); + g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); + g.setColor(curcolor); + g.drawLine( + curposx + dotoffset, posy - 1, curposx + dotoffset, posy - YSPACING + 2 * dotoffset + 2); + } + // Now we have drawn all the nodes in this variation, and has reached the bottom of this + // variation + // Move back up, and for each, draw any variations we find + while (cur.previous() != null && cur != startNode) { + cur = cur.previous(); + int curwidth = lane; + // Draw each variation, uses recursion + for (int i = 1; i < cur.numberOfChildren(); i++) { + curwidth++; + // Recursion, depth of recursion will normally not be very deep (one recursion level for + // every variation that has a variation (sort of)) + drawTree(g, posx, posy, curwidth, maxposy, cur.getVariation(i), i, false); + } + posy -= YSPACING; } + } - public void drawTree(Graphics2D g, int posx, int posy, int startLane, int maxposy, BoardHistoryNode startNode, int variationNumber, boolean isMain) - { - if (isMain) g.setColor(Color.white); - else g.setColor(Color.gray.brighter()); - - - // Finds depth on leftmost variation of this tree - int depth = BoardHistoryList.getDepth(startNode) + 1; - int lane = startLane; - // Figures out how far out too the right (which lane) we have to go not to collide with other variations - while (lane < laneUsageList.size() && laneUsageList.get(lane) <= startNode.getData().moveNumber + depth) { - // laneUsageList keeps a list of how far down it is to a variation in the different "lanes" - laneUsageList.set(lane, startNode.getData().moveNumber - 1); - lane++; - } - if (lane >= laneUsageList.size()) - { - laneUsageList.add(0); - } - if (variationNumber > 1) - laneUsageList.set(lane - 1, startNode.getData().moveNumber - 1); - laneUsageList.set(lane, startNode.getData().moveNumber); - - // At this point, lane contains the lane we should use (the main branch is in lane 0) - - BoardHistoryNode cur = startNode; - int curposx = posx + lane*XSPACING; - int dotoffset = DOT_DIAM/2; - - // Draw line back to main branch - if (lane > 0) { - if (lane - startLane > 0 || variationNumber > 1) { - // Need a horizontal and an angled line - g.drawLine(curposx + dotoffset, posy + dotoffset, curposx + dotoffset - XSPACING, posy + dotoffset - YSPACING); - g.drawLine(posx + (startLane - variationNumber )*XSPACING + 2*dotoffset, posy - YSPACING + dotoffset, curposx + dotoffset - XSPACING, posy + dotoffset - YSPACING); - } else { - // Just an angled line - g.drawLine(curposx + dotoffset, posy + dotoffset, curposx + 2*dotoffset - XSPACING, posy + 2*dotoffset - YSPACING); - } - } - - // Draw all the nodes and lines in this lane (not variations) - Color curcolor = g.getColor(); - if (startNode == curMove) { - g.setColor(Color.green.brighter().brighter()); - } - if (startNode.previous() != null) { - g.fillOval(curposx, posy, DOT_DIAM, DOT_DIAM); - g.setColor(Color.BLACK); - g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); - } else { - g.fillRect(curposx, posy, DOT_DIAM, DOT_DIAM); - g.setColor(Color.BLACK); - g.drawRect(curposx, posy, DOT_DIAM, DOT_DIAM); - } - g.setColor(curcolor); - - // Draw main line - while (cur.next() != null && posy + YSPACING < maxposy) { - posy += YSPACING; - cur = cur.next(); - if (cur == curMove) { - g.setColor(Color.green.brighter().brighter()); - } - g.fillOval(curposx , posy, DOT_DIAM, DOT_DIAM); - g.setColor(Color.BLACK); - g.drawOval(curposx, posy, DOT_DIAM, DOT_DIAM); - g.setColor(curcolor); - g.drawLine(curposx + dotoffset, posy-1, curposx + dotoffset , posy - YSPACING + 2*dotoffset+2); - } - // Now we have drawn all the nodes in this variation, and has reached the bottom of this variation - // Move back up, and for each, draw any variations we find - while (cur.previous() != null && cur != startNode) { - cur = cur.previous(); - int curwidth = lane; - // Draw each variation, uses recursion - for (int i = 1; i < cur.numberOfChildren(); i++) { - curwidth++; - // Recursion, depth of recursion will normally not be very deep (one recursion level for every variation that has a variation (sort of)) - drawTree(g, posx, posy, curwidth, maxposy, cur.getVariation(i), i,false); - } - posy -= YSPACING; - } + public void draw(Graphics2D g, int posx, int posy, int width, int height) { + if (width <= 0 || height <= 0) { + return; // we don't have enough space } - public void draw(Graphics2D g, int posx, int posy, int width, int height) { - if (width <= 0 || height <= 0) - return; // we don't have enough space - - // Use dense tree for saving space if large-subboard - YSPACING = (Lizzie.config.showLargeSubBoard() ? 20 : 30); - XSPACING = YSPACING; - - // Draw background - g.setColor(new Color(0, 0, 0, 60)); - g.fillRect(posx, posy, width, height); - - // draw edge of panel - int strokeRadius = 2; - g.setStroke(new BasicStroke(2 * strokeRadius)); - g.drawLine(posx+strokeRadius, posy+strokeRadius, posx+strokeRadius, posy-strokeRadius+height); - g.setStroke(new BasicStroke(1)); - - - int middleY = posy + height/2; - int xoffset = 30; - laneUsageList.clear(); - - curMove = Lizzie.board.getHistory().getCurrentHistoryNode(); - - // Is current move a variation? If so, find top of variation - BoardHistoryNode top = BoardHistoryList.findTop(curMove); - int curposy = middleY - YSPACING*(curMove.getData().moveNumber - top.getData().moveNumber); - // Go to very top of tree (visible in assigned area) - BoardHistoryNode node = top; - while (curposy > posy + YSPACING && node.previous() != null) { - node = node.previous(); - curposy -= YSPACING; - } - drawTree(g, posx + xoffset, curposy, 0, posy + height, node, 0,true); + // Use dense tree for saving space if large-subboard + YSPACING = (Lizzie.config.showLargeSubBoard() ? 20 : 30); + XSPACING = YSPACING; + + // Draw background + g.setColor(new Color(0, 0, 0, 60)); + g.fillRect(posx, posy, width, height); + + // draw edge of panel + int strokeRadius = 2; + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.drawLine( + posx + strokeRadius, + posy + strokeRadius, + posx + strokeRadius, + posy - strokeRadius + height); + g.setStroke(new BasicStroke(1)); + + int middleY = posy + height / 2; + int xoffset = 30; + laneUsageList.clear(); + + curMove = Lizzie.board.getHistory().getCurrentHistoryNode(); + + // Is current move a variation? If so, find top of variation + BoardHistoryNode top = BoardHistoryList.findTop(curMove); + int curposy = middleY - YSPACING * (curMove.getData().moveNumber - top.getData().moveNumber); + // Go to very top of tree (visible in assigned area) + BoardHistoryNode node = top; + while (curposy > posy + YSPACING && node.previous() != null) { + node = node.previous(); + curposy -= YSPACING; } + drawTree(g, posx + xoffset, curposy, 0, posy + height, node, 0, true); + } } diff --git a/src/main/java/featurecat/lizzie/gui/WinrateGraph.java b/src/main/java/featurecat/lizzie/gui/WinrateGraph.java index 8f6074a56..3b4915c96 100644 --- a/src/main/java/featurecat/lizzie/gui/WinrateGraph.java +++ b/src/main/java/featurecat/lizzie/gui/WinrateGraph.java @@ -4,223 +4,225 @@ import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.rules.BoardHistoryList; import featurecat.lizzie.rules.BoardHistoryNode; - import java.awt.*; import java.awt.geom.Point2D; public class WinrateGraph { - private int DOT_RADIUS = 6; - private int[] origParams = {0, 0, 0, 0}; - private int[] params = {0, 0, 0, 0, 0}; - - public void draw(Graphics2D g, int posx, int posy, int width, int height) - { - BoardHistoryNode curMove = Lizzie.board.getHistory().getCurrentHistoryNode(); - BoardHistoryNode node = curMove; - - // draw background rectangle - final Paint gradient = new GradientPaint(new Point2D.Float(posx, posy), new Color(0, 0, 0, 150), new Point2D.Float(posx, posy+height), new Color(255, 255, 255, 150)); - final Paint borderGradient = new GradientPaint(new Point2D.Float(posx, posy), new Color(0, 0, 0, 150), new Point2D.Float(posx, posy+height), new Color(255, 255, 255, 150)); - - Paint original = g.getPaint(); - g.setPaint(gradient); - - g.fillRect(posx, posy, width, height); - - // draw border - int strokeRadius = 3; - g.setStroke(new BasicStroke(2 * strokeRadius)); - g.setPaint(borderGradient); - g.drawRect(posx+ strokeRadius, posy + strokeRadius, width - 2 * strokeRadius, height- 2 * strokeRadius); - - g.setPaint(original); + private int DOT_RADIUS = 6; + private int[] origParams = {0, 0, 0, 0}; + private int[] params = {0, 0, 0, 0, 0}; + + public void draw(Graphics2D g, int posx, int posy, int width, int height) { + BoardHistoryNode curMove = Lizzie.board.getHistory().getCurrentHistoryNode(); + BoardHistoryNode node = curMove; + + // draw background rectangle + final Paint gradient = + new GradientPaint( + new Point2D.Float(posx, posy), + new Color(0, 0, 0, 150), + new Point2D.Float(posx, posy + height), + new Color(255, 255, 255, 150)); + final Paint borderGradient = + new GradientPaint( + new Point2D.Float(posx, posy), + new Color(0, 0, 0, 150), + new Point2D.Float(posx, posy + height), + new Color(255, 255, 255, 150)); + + Paint original = g.getPaint(); + g.setPaint(gradient); + + g.fillRect(posx, posy, width, height); + + // draw border + int strokeRadius = 3; + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.setPaint(borderGradient); + g.drawRect( + posx + strokeRadius, + posy + strokeRadius, + width - 2 * strokeRadius, + height - 2 * strokeRadius); + + g.setPaint(original); + + // record parameters (before resizing) for calculating moveNumber + origParams[0] = posx; + origParams[1] = posy; + origParams[2] = width; + origParams[3] = height; + + // resize the box now so it's inside the border + posx += 2 * strokeRadius; + posy += 2 * strokeRadius; + width -= 4 * strokeRadius; + height -= 4 * strokeRadius; + + // draw lines marking 50% 60% 70% etc. + Stroke dashed = + new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] {4}, 0); + g.setStroke(dashed); + + g.setColor(Color.white); + int winRateGridLines = Lizzie.frame.winRateGridLines; + for (int i = 1; i <= winRateGridLines; i++) { + double percent = i * 100.0 / (winRateGridLines + 1); + int y = posy + height - (int) (height * convertWinrate(percent) / 100); + g.drawLine(posx, y, posx + width, y); + } - // record parameters (before resizing) for calculating moveNumber - origParams[0] = posx; - origParams[1] = posy; - origParams[2] = width; - origParams[3] = height; + g.setColor(Color.green); + g.setStroke(new BasicStroke(3)); + + BoardHistoryNode topOfVariation = null; + int numMoves = 0; + if (!BoardHistoryList.isMainTrunk(curMove)) { + // We're in a variation, need to draw both main trunk and variation + // Find top of variation + topOfVariation = BoardHistoryList.findTop(curMove); + // Find depth of main trunk, need this for plot scaling + numMoves = + BoardHistoryList.getDepth(topOfVariation) + topOfVariation.getData().moveNumber - 1; + g.setStroke(dashed); + } - // resize the box now so it's inside the border - posx += 2*strokeRadius; - posy += 2*strokeRadius; - width -= 4*strokeRadius; - height -= 4*strokeRadius; + // Go to end of variation and work our way backwards to the root + while (node.next() != null) node = node.next(); + if (numMoves < node.getData().moveNumber - 1) { + numMoves = node.getData().moveNumber - 1; + } - // draw lines marking 50% 60% 70% etc. - Stroke dashed = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, - new float[]{4}, 0); + if (numMoves < 1) return; + + // Plot + width = (int) (width * 0.95); // Leave some space after last move + double lastWr = 50; + boolean lastNodeOk = false; + boolean inFirstPath = true; + int movenum = node.getData().moveNumber - 1; + int lastOkMove = -1; + + while (node.previous() != null) { + double wr = node.getData().winrate; + int playouts = node.getData().playouts; + if (node == curMove) { + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + double bwr = stats.maxWinrate; + if (bwr >= 0 && stats.totalPlayouts > playouts) { + wr = bwr; + playouts = stats.totalPlayouts; + } + // Draw a vertical line at the current move + Stroke previousStroke = g.getStroke(); + int x = posx + (movenum * width / numMoves); g.setStroke(dashed); - g.setColor(Color.white); - int winRateGridLines = Lizzie.frame.winRateGridLines; - for (int i = 1; i <= winRateGridLines; i++) { - double percent = i * 100.0 / (winRateGridLines + 1); - int y = posy + height - (int) (height * convertWinrate(percent) / 100); - g.drawLine(posx, y, posx + width, y); + g.drawLine(x, posy, x, posy + height); + // Show move number + String moveNumString = "" + node.getData().moveNumber; + int mw = g.getFontMetrics().stringWidth(moveNumString); + int margin = strokeRadius; + int mx = x - posx < width / 2 ? x + margin : x - mw - margin; + g.drawString(moveNumString, mx, posy + height - margin); + g.setStroke(previousStroke); + } + if (playouts > 0) { + if (wr < 0) { + wr = 100 - lastWr; + } else if (!node.getData().blackToPlay) { + wr = 100 - wr; } - - g.setColor(Color.green); - g.setStroke(new BasicStroke(3)); - - BoardHistoryNode topOfVariation = null; - int numMoves = 0; - if (!BoardHistoryList.isMainTrunk(curMove)) - { - // We're in a variation, need to draw both main trunk and variation - // Find top of variation - topOfVariation = BoardHistoryList.findTop(curMove); - // Find depth of main trunk, need this for plot scaling - numMoves = BoardHistoryList.getDepth(topOfVariation) + topOfVariation.getData().moveNumber - 1; - g.setStroke(dashed); + if (Lizzie.frame.isPlayingAgainstLeelaz + && Lizzie.frame.playerIsBlack == !node.getData().blackToPlay) { + wr = lastWr; } - // Go to end of variation and work our way backwards to the root - while (node.next() != null) node = node.next(); - if (numMoves < node.getData().moveNumber-1) { - numMoves = node.getData().moveNumber - 1; - } + if (lastNodeOk) g.setColor(Color.green); + else g.setColor(Color.blue.darker()); - if (numMoves < 1) return; - - // Plot - width = (int)(width*0.95); // Leave some space after last move - double lastWr = 50; - boolean lastNodeOk = false; - boolean inFirstPath = true; - int movenum = node.getData().moveNumber - 1; - int lastOkMove = -1; - - while (node.previous() != null) - { - double wr = node.getData().winrate; - int playouts = node.getData().playouts; - if (node == curMove) - { - Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); - double bwr = stats.maxWinrate; - if (bwr >= 0 && stats.totalPlayouts > playouts) { - wr = bwr; - playouts = stats.totalPlayouts; - } - { - // Draw a vertical line at the current move - Stroke previousStroke = g.getStroke(); - int x = posx + (movenum*width/numMoves); - g.setStroke(dashed); - g.setColor(Color.white); - g.drawLine(x, posy, x, posy + height); - // Show move number - String moveNumString = "" + node.getData().moveNumber; - int mw = g.getFontMetrics().stringWidth(moveNumString); - int margin = strokeRadius; - int mx = x - posx < width / 2 ? x + margin : x - mw - margin; - g.drawString(moveNumString, mx, posy + height - margin); - g.setStroke(previousStroke); - } - } - if (playouts > 0) { - if (wr < 0) - { - wr = 100 - lastWr; - } - else if (!node.getData().blackToPlay) - { - wr = 100 - wr; - } - if (Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack == !node.getData().blackToPlay) { - wr = lastWr; - } - - if (lastNodeOk) - g.setColor(Color.green); - else - g.setColor(Color.blue.darker()); - - if (lastOkMove > 0) { - g.drawLine(posx + (lastOkMove * width / numMoves), - posy + height - (int) (convertWinrate(lastWr) * height / 100), - posx + (movenum * width / numMoves), - posy + height - (int) (convertWinrate(wr) * height / 100)); - } - - if (node == curMove) - { - g.setColor(Color.green); - g.fillOval(posx + (movenum*width/numMoves) - DOT_RADIUS, - posy + height - (int)(convertWinrate(wr)*height/100) - DOT_RADIUS, - DOT_RADIUS*2, - DOT_RADIUS*2); - } - lastWr = wr; - lastNodeOk = true; - // Check if we were in a variation and has reached the main trunk - if (node == topOfVariation) { - // Reached top of variation, go to end of main trunk before continuing - while (node.next() != null) node = node.next(); - movenum = node.getData().moveNumber - 1; - lastWr = node.getData().winrate; - if (!node.getData().blackToPlay) - lastWr = 100 - lastWr; - g.setStroke(new BasicStroke(3)); - topOfVariation = null; - if (node.getData().playouts == 0) { - lastNodeOk = false; - } - inFirstPath = false; - } - lastOkMove = lastNodeOk ? movenum : -1; - } else { - lastNodeOk = false; - } - - node = node.previous(); - movenum--; + if (lastOkMove > 0) { + g.drawLine( + posx + (lastOkMove * width / numMoves), + posy + height - (int) (convertWinrate(lastWr) * height / 100), + posx + (movenum * width / numMoves), + posy + height - (int) (convertWinrate(wr) * height / 100)); } - g.setStroke(new BasicStroke(1)); + if (node == curMove) { + g.setColor(Color.green); + g.fillOval( + posx + (movenum * width / numMoves) - DOT_RADIUS, + posy + height - (int) (convertWinrate(wr) * height / 100) - DOT_RADIUS, + DOT_RADIUS * 2, + DOT_RADIUS * 2); + } + lastWr = wr; + lastNodeOk = true; + // Check if we were in a variation and has reached the main trunk + if (node == topOfVariation) { + // Reached top of variation, go to end of main trunk before continuing + while (node.next() != null) node = node.next(); + movenum = node.getData().moveNumber - 1; + lastWr = node.getData().winrate; + if (!node.getData().blackToPlay) lastWr = 100 - lastWr; + g.setStroke(new BasicStroke(3)); + topOfVariation = null; + if (node.getData().playouts == 0) { + lastNodeOk = false; + } + inFirstPath = false; + } + lastOkMove = lastNodeOk ? movenum : -1; + } else { + lastNodeOk = false; + } - // record parameters for calculating moveNumber - params[0] = posx; - params[1] = posy; - params[2] = width; - params[3] = height; - params[4] = numMoves; + node = node.previous(); + movenum--; } - private double convertWinrate(double winrate) { - double maxHandicap = 10; - if (Lizzie.config.handicapInsteadOfWinrate) { - double handicap = Lizzie.leelaz.winrateToHandicap(winrate); - // handicap == + maxHandicap => r == 1.0 - // handicap == - maxHandicap => r == 0.0 - double r = 0.5 + handicap / (2 * maxHandicap); - return Math.max(0, Math.min(r, 1)) * 100; - } else { - return winrate; - } + g.setStroke(new BasicStroke(1)); + + // record parameters for calculating moveNumber + params[0] = posx; + params[1] = posy; + params[2] = width; + params[3] = height; + params[4] = numMoves; + } + + private double convertWinrate(double winrate) { + double maxHandicap = 10; + if (Lizzie.config.handicapInsteadOfWinrate) { + double handicap = Lizzie.leelaz.winrateToHandicap(winrate); + // handicap == + maxHandicap => r == 1.0 + // handicap == - maxHandicap => r == 0.0 + double r = 0.5 + handicap / (2 * maxHandicap); + return Math.max(0, Math.min(r, 1)) * 100; + } else { + return winrate; } - - public int moveNumber(int x, int y) - { - int origPosx = origParams[0]; - int origPosy = origParams[1]; - int origWidth = origParams[2]; - int origHeight = origParams[3]; - int posx = params[0]; - int posy = params[1]; - int width = params[2]; - int height = params[3]; - int numMoves = params[4]; - if (origPosx <= x && x < origPosx + origWidth && - origPosy <= y && y < origPosy + origHeight) { - // x == posx + (movenum * width / numMoves) ==> movenum = ... - int movenum = Math.round((x - posx) * numMoves / (float) width); - // movenum == moveNumber - 1 ==> moveNumber = ... - return movenum + 1; - } else { - return -1; - } + } + + public int moveNumber(int x, int y) { + int origPosx = origParams[0]; + int origPosy = origParams[1]; + int origWidth = origParams[2]; + int origHeight = origParams[3]; + int posx = params[0]; + int posy = params[1]; + int width = params[2]; + int height = params[3]; + int numMoves = params[4]; + if (origPosx <= x && x < origPosx + origWidth && origPosy <= y && y < origPosy + origHeight) { + // x == posx + (movenum * width / numMoves) ==> movenum = ... + int movenum = Math.round((x - posx) * numMoves / (float) width); + // movenum == moveNumber - 1 ==> moveNumber = ... + return movenum + 1; + } else { + return -1; } + } } diff --git a/src/main/java/featurecat/lizzie/plugin/IPlugin.java b/src/main/java/featurecat/lizzie/plugin/IPlugin.java deleted file mode 100644 index 190d4b54d..000000000 --- a/src/main/java/featurecat/lizzie/plugin/IPlugin.java +++ /dev/null @@ -1,62 +0,0 @@ -package featurecat.lizzie.plugin; - - -import java.awt.Graphics2D; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.net.URLClassLoader; - -public interface IPlugin { - - public static IPlugin load(String uri) throws Exception { - URLClassLoader loader = new URLClassLoader(new URL[] {new File(uri).toURI().toURL()}); - IPlugin plugin = (IPlugin) loader.loadClass("plugin.Plugin").newInstance(); - plugin.onInit(); - - loader.close(); - - return plugin; - } - - public default void onInit() throws IOException { - - } - - public default void onMousePressed(MouseEvent e) { - - } - - public default void onMouseReleased(MouseEvent e) { - - } - - public default void onMouseMoved(MouseEvent e) { - - } - - public default void onKeyPressed(KeyEvent e) { - - } - - public default void onKeyReleased(KeyEvent e) { - - } - - public default boolean onDraw(Graphics2D g) { - return false; - } - - public default void onShutdown() throws IOException { - - } - - public default void onSgfLoaded() { - - } - - public String getName(); - public String getVersion(); -} diff --git a/src/main/java/featurecat/lizzie/plugin/PluginManager.java b/src/main/java/featurecat/lizzie/plugin/PluginManager.java deleted file mode 100644 index 426a36a77..000000000 --- a/src/main/java/featurecat/lizzie/plugin/PluginManager.java +++ /dev/null @@ -1,100 +0,0 @@ -package featurecat.lizzie.plugin; - -import java.awt.Graphics2D; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; -import java.io.File; -import java.util.HashSet; -import java.io.IOException; - -import featurecat.lizzie.Lizzie; - -public class PluginManager { - public static HashSet plugins; - - public static void loadPlugins() throws IOException { - if (plugins != null) { - for (IPlugin plugin : plugins) { - plugin.onShutdown(); - } - } - plugins = new HashSet(); - File path = new File("./plugin/"); - path.mkdirs(); - - for (File jarFile : path.listFiles()) { - if (jarFile.isDirectory()) { - continue; - } - try { - plugins.add(IPlugin.load(jarFile.getPath())); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - public static void onMousePressed(MouseEvent e) { - for (IPlugin plugin : plugins) { - plugin.onMousePressed(e); - } - } - - public static void onMouseReleased(MouseEvent e) { - for (IPlugin plugin : plugins) { - plugin.onMousePressed(e); - } - } - - public static void onMouseMoved(MouseEvent e) { - for (IPlugin plugin : plugins) { - plugin.onMousePressed(e); - } - } - - public static void onKeyPressed(KeyEvent e) { - for (IPlugin plugin : plugins) { - plugin.onKeyPressed(e); - } - } - - public static void onKeyReleased(KeyEvent e) { - for (IPlugin plugin : plugins) { - plugin.onKeyReleased(e); - } - } - - public static void onShutdown(){ - - for (IPlugin plugin : plugins) { - try {plugin.onShutdown(); - } catch(IOException e) { - e.printStackTrace(); - } - } - } - - public static void onSgfLoaded() { - for (IPlugin plugin : plugins) { - plugin.onSgfLoaded(); - } - } - - public static void onDraw(Graphics2D g0) { - int width = Lizzie.frame.getWidth(); - int height = Lizzie.frame.getHeight(); - BufferedImage cachedImageParent = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = cachedImageParent.createGraphics(); - for (IPlugin plugin : plugins) { - BufferedImage cachedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = cachedImage.createGraphics(); - if (plugin.onDraw(graphics)) { - g.drawImage(cachedImage, 0, 0, null); - } - graphics.dispose(); - } - g0.drawImage(cachedImageParent, 0, 0, null); - g.dispose(); - } -} diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index fcac07cc7..2de079d57 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -4,1049 +4,1204 @@ import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.analysis.LeelazListener; import featurecat.lizzie.analysis.MoveData; - -import javax.swing.*; +import java.io.IOException; import java.util.ArrayDeque; import java.util.Deque; -import java.util.Queue; import java.util.List; +import java.util.Queue; +import javax.swing.*; +import org.json.JSONException; public class Board implements LeelazListener { - public static final int BOARD_SIZE = Lizzie.config.config.getJSONObject("ui").optInt("board-size", 19); - private final static String alphabet = "ABCDEFGHJKLMNOPQRST"; - - private BoardHistoryList history; - private Stone[] capturedStones; - - private boolean scoreMode; - - private boolean analysisMode = false; - private int playoutsAnalysis = 100; - + public static final int BOARD_SIZE = + Lizzie.config.config.getJSONObject("ui").optInt("board-size", 19); + private static final String alphabet = "ABCDEFGHJKLMNOPQRST"; - public Board() { - initialize(); - } + private BoardHistoryList history; + private Stone[] capturedStones; - /** - * Initialize the board completely - */ - private void initialize() { - Stone[] stones = new Stone[BOARD_SIZE * BOARD_SIZE]; - for (int i = 0; i < stones.length; i++) - stones[i] = Stone.EMPTY; + private boolean scoreMode; - boolean blackToPlay = true; - int[] lastMove = null; + private boolean analysisMode = false; + private int playoutsAnalysis = 100; - capturedStones = null; - scoreMode = false; + // Save the node for restore move when in the branch + private BoardHistoryNode saveNode = null; - history = new BoardHistoryList(new BoardData(stones, lastMove, Stone.EMPTY, blackToPlay, - new Zobrist(), 0, new int[BOARD_SIZE * BOARD_SIZE], 0, 0, 50, 0)); - } + public Board() { + initialize(); + } - /** - * Calculates the array index of a stone stored at (x, y) - * - * @param x the x coordinate - * @param y the y coordinate - * @return the array index - */ - public static int getIndex(int x, int y) { - return x * Board.BOARD_SIZE + y; + /** Initialize the board completely */ + private void initialize() { + Stone[] stones = new Stone[BOARD_SIZE * BOARD_SIZE]; + for (int i = 0; i < stones.length; i++) { + stones[i] = Stone.EMPTY; } - /** - * Converts a named coordinate eg C16, T5, K10, etc to an x and y coordinate - * - * @param namedCoordinate a capitalized version of the named coordinate. Must be a valid 19x19 Go coordinate, without I - * @return an array containing x, followed by y - */ - public static int[] convertNameToCoordinates(String namedCoordinate) { - namedCoordinate = namedCoordinate.trim(); - if (namedCoordinate.equalsIgnoreCase("pass")) { - return null; - } - // coordinates take the form C16 A19 Q5 K10 etc. I is not used. - int x = alphabet.indexOf(namedCoordinate.charAt(0)); - int y = Integer.parseInt(namedCoordinate.substring(1)) - 1; - return new int[]{x, y}; + capturedStones = null; + scoreMode = false; + + int[] boardArray = new int[BOARD_SIZE * BOARD_SIZE]; + BoardData boardData = + new BoardData(stones, null, Stone.EMPTY, true, new Zobrist(), 0, boardArray, 0, 0, 50, 0); + history = new BoardHistoryList(boardData); + } + + /** + * Calculates the array index of a stone stored at (x, y) + * + * @param x the x coordinate + * @param y the y coordinate + * @return the array index + */ + public static int getIndex(int x, int y) { + return x * Board.BOARD_SIZE + y; + } + + /** + * Converts a named coordinate eg C16, T5, K10, etc to an x and y coordinate + * + * @param namedCoordinate a capitalized version of the named coordinate. Must be a valid 19x19 Go + * coordinate, without I + * @return an array containing x, followed by y + */ + public static int[] convertNameToCoordinates(String namedCoordinate) { + namedCoordinate = namedCoordinate.trim(); + if (namedCoordinate.equalsIgnoreCase("pass")) { + return null; } - - /** - * Converts a x and y coordinate to a named coordinate eg C16, T5, K10, etc - * - * @param x x coordinate -- must be valid - * @param y y coordinate -- must be valid - * @return a string representing the coordinate - */ - public static String convertCoordinatesToName(int x, int y) { - // coordinates take the form C16 A19 Q5 K10 etc. I is not used. - return alphabet.charAt(x) + "" + (y + 1); + // coordinates take the form C16 A19 Q5 K10 etc. I is not used. + int x = alphabet.indexOf(namedCoordinate.charAt(0)); + int y = BOARD_SIZE - Integer.parseInt(namedCoordinate.substring(1)); + return new int[] {x, y}; + } + + /** + * Converts a x and y coordinate to a named coordinate eg C16, T5, K10, etc + * + * @param x x coordinate -- must be valid + * @param y y coordinate -- must be valid + * @return a string representing the coordinate + */ + public static String convertCoordinatesToName(int x, int y) { + // coordinates take the form C16 A19 Q5 K10 etc. I is not used. + return alphabet.charAt(x) + "" + (BOARD_SIZE - y); + } + + /** + * Checks if a coordinate is valid + * + * @param x x coordinate + * @param y y coordinate + * @return whether or not this coordinate is part of the board + */ + public static boolean isValid(int x, int y) { + return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE; + } + + /** + * The comment. Thread safe + * + * @param comment the comment of stone + */ + public void comment(String comment) { + synchronized (this) { + if (history.getData() != null) { + history.getData().comment = comment; + } } - - /** - * Checks if a coordinate is valid - * - * @param x x coordinate - * @param y y coordinate - * @return whether or not this coordinate is part of the board - */ - public static boolean isValid(int x, int y) { - return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE; + } + + /** + * The pass. Thread safe + * + * @param color the type of pass + */ + public void pass(Stone color) { + synchronized (this) { + + // check to see if this move is being replayed in history + BoardData next = history.getNext(); + if (next != null && next.lastMove == null) { + // this is the next move in history. Just increment history so that we don't erase the + // redo's + history.next(); + Lizzie.leelaz.playMove(color, "pass"); + if (Lizzie.frame.isPlayingAgainstLeelaz) + Lizzie.leelaz.genmove((history.isBlacksTurn() ? "B" : "W")); + + return; + } + + Stone[] stones = history.getStones().clone(); + Zobrist zobrist = history.getZobrist(); + int moveNumber = history.getMoveNumber() + 1; + int[] moveNumberList = history.getMoveNumberList().clone(); + + // build the new game state + BoardData newState = + new BoardData( + stones, + null, + color, + color.equals(Stone.WHITE), + zobrist, + moveNumber, + moveNumberList, + history.getData().blackCaptures, + history.getData().whiteCaptures, + 0, + 0); + + // update leelaz with pass + Lizzie.leelaz.playMove(color, "pass"); + if (Lizzie.frame.isPlayingAgainstLeelaz) + Lizzie.leelaz.genmove((history.isBlacksTurn() ? "W" : "B")); + + // update history with pass + history.addOrGoto(newState); + + Lizzie.frame.repaint(); } - - /** - * The pass. Thread safe - * - * @param color the type of pass - */ - public void pass(Stone color) { - synchronized (this) { - - // check to see if this move is being replayed in history - BoardData next = history.getNext(); - if (next != null && next.lastMove == null) { - // this is the next move in history. Just increment history so that we don't erase the redo's - history.next(); - Lizzie.leelaz.playMove(color, "pass"); - if (Lizzie.frame.isPlayingAgainstLeelaz) - Lizzie.leelaz.genmove((history.isBlacksTurn()? "B" : "W")); - - return; - } - - Stone[] stones = history.getStones().clone(); - Zobrist zobrist = history.getZobrist(); - int moveNumber = history.getMoveNumber() + 1; - int[] moveNumberList = history.getMoveNumberList().clone(); - - // build the new game state - BoardData newState = new BoardData(stones, null, color, color.equals(Stone.WHITE), - zobrist, moveNumber, moveNumberList, history.getData().blackCaptures, history.getData().whiteCaptures, 0, 0); - - // update leelaz with pass - Lizzie.leelaz.playMove(color, "pass"); - if (Lizzie.frame.isPlayingAgainstLeelaz) - Lizzie.leelaz.genmove((history.isBlacksTurn()? "W" : "B")); - - // update history with pass - history.addOrGoto(newState); - - Lizzie.frame.repaint(); + } + + /** overloaded method for pass(), chooses color in an alternating pattern */ + public void pass() { + pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); + } + + /** + * Places a stone onto the board representation. Thread safe + * + * @param x x coordinate + * @param y y coordinate + * @param color the type of stone to place + */ + public void place(int x, int y, Stone color) { + synchronized (this) { + if (scoreMode) { + // Mark clicked stone as dead + Stone[] stones = history.getStones(); + toggleLiveStatus(capturedStones, x, y); + return; + } + + if (!isValid(x, y) || history.getStones()[getIndex(x, y)] != Stone.EMPTY) return; + + // Update winrate + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + + if (stats.maxWinrate >= 0 && stats.totalPlayouts > history.getData().playouts) { + history.getData().winrate = stats.maxWinrate; + history.getData().playouts = stats.totalPlayouts; + } + double nextWinrate = -100; + if (history.getData().winrate >= 0) nextWinrate = 100 - history.getData().winrate; + + // check to see if this coordinate is being replayed in history + BoardData next = history.getNext(); + if (next != null && next.lastMove != null && next.lastMove[0] == x && next.lastMove[1] == y) { + // this is the next coordinate in history. Just increment history so that we don't erase the + // redo's + history.next(); + // should be opposite from the bottom case + if (Lizzie.frame.isPlayingAgainstLeelaz + && Lizzie.frame.playerIsBlack != getData().blackToPlay) { + Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); + Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "W" : "B")); + } else if (!Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); } - } - - /** - * overloaded method for pass(), chooses color in an alternating pattern - */ - public void pass() { - pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); - } - - /** - * Places a stone onto the board representation. Thread safe - * - * @param x x coordinate - * @param y y coordinate - * @param color the type of stone to place - */ - public void place(int x, int y, Stone color) { - synchronized (this) { - if (scoreMode) { - // Mark clicked stone as dead - Stone[] stones = history.getStones(); - toggleLiveStatus(capturedStones, x, y); - return; - } - - if (!isValid(x, y) || history.getStones()[getIndex(x, y)] != Stone.EMPTY) - return; - - // Update winrate - Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); - - if (stats.maxWinrate >= 0 && stats.totalPlayouts > history.getData().playouts) - { - history.getData().winrate = stats.maxWinrate; - history.getData().playouts = stats.totalPlayouts; - } - double nextWinrate = -100; - if (history.getData().winrate >= 0) nextWinrate = 100 - history.getData().winrate; - - // check to see if this coordinate is being replayed in history - BoardData next = history.getNext(); - if (next != null && next.lastMove != null && next.lastMove[0] == x && next.lastMove[1] == y) { - // this is the next coordinate in history. Just increment history so that we don't erase the redo's - history.next(); - // should be opposite from the bottom case - if (Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack != getData().blackToPlay) { - Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); - Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "W" : "B")); - } else if (!Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); - } - return; - } - - // load a copy of the data at the current node of history - Stone[] stones = history.getStones().clone(); - Zobrist zobrist = history.getZobrist(); - int[] lastMove = new int[]{x, y}; // keep track of the last played stone - int moveNumber = history.getMoveNumber() + 1; - int[] moveNumberList = history.getMoveNumberList().clone(); - - moveNumberList[Board.getIndex(x, y)] = moveNumber; - - // set the stone at (x, y) to color - stones[getIndex(x, y)] = color; - zobrist.toggleStone(x, y, color); - - // remove enemy stones - int capturedStones = 0; - capturedStones += removeDeadChain(x + 1, y, color.opposite(), stones, zobrist); - capturedStones += removeDeadChain(x, y + 1, color.opposite(), stones, zobrist); - capturedStones += removeDeadChain(x - 1, y, color.opposite(), stones, zobrist); - capturedStones += removeDeadChain(x, y - 1, color.opposite(), stones, zobrist); - - // check to see if the player made a suicidal coordinate - int isSuicidal = removeDeadChain(x, y, color, stones, zobrist); - - for (int i = 0; i < BOARD_SIZE * BOARD_SIZE; i++) { - if (stones[i].equals(Stone.EMPTY)) { - moveNumberList[i] = 0; - } - } - - int bc = history.getData().blackCaptures; - int wc = history.getData().whiteCaptures; - if (color.isBlack()) - bc += capturedStones; - else - wc += capturedStones; - BoardData newState = new BoardData(stones, lastMove, color, color.equals(Stone.WHITE), - zobrist, moveNumber, moveNumberList, bc, wc, nextWinrate, 0); - - // don't make this coordinate if it is suicidal or violates superko - if (isSuicidal > 0|| history.violatesSuperko(newState)) - return; - - // update leelaz with board position - if (Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.frame.playerIsBlack == getData().blackToPlay) { - Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); - Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "W" : "B")); - } else if (!Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); - } - - // update history with this coordinate - history.addOrGoto(newState); - - Lizzie.frame.repaint(); + return; + } + + // load a copy of the data at the current node of history + Stone[] stones = history.getStones().clone(); + Zobrist zobrist = history.getZobrist(); + int[] lastMove = new int[] {x, y}; // keep track of the last played stone + int moveNumber = history.getMoveNumber() + 1; + int[] moveNumberList = history.getMoveNumberList().clone(); + + moveNumberList[Board.getIndex(x, y)] = moveNumber; + + // set the stone at (x, y) to color + stones[getIndex(x, y)] = color; + zobrist.toggleStone(x, y, color); + + // remove enemy stones + int capturedStones = 0; + capturedStones += removeDeadChain(x + 1, y, color.opposite(), stones, zobrist); + capturedStones += removeDeadChain(x, y + 1, color.opposite(), stones, zobrist); + capturedStones += removeDeadChain(x - 1, y, color.opposite(), stones, zobrist); + capturedStones += removeDeadChain(x, y - 1, color.opposite(), stones, zobrist); + + // check to see if the player made a suicidal coordinate + int isSuicidal = removeDeadChain(x, y, color, stones, zobrist); + + for (int i = 0; i < BOARD_SIZE * BOARD_SIZE; i++) { + if (stones[i].equals(Stone.EMPTY)) { + moveNumberList[i] = 0; } + } + + int bc = history.getData().blackCaptures; + int wc = history.getData().whiteCaptures; + if (color.isBlack()) bc += capturedStones; + else wc += capturedStones; + BoardData newState = + new BoardData( + stones, + lastMove, + color, + color.equals(Stone.WHITE), + zobrist, + moveNumber, + moveNumberList, + bc, + wc, + nextWinrate, + 0); + + // don't make this coordinate if it is suicidal or violates superko + if (isSuicidal > 0 || history.violatesSuperko(newState)) return; + + // update leelaz with board position + if (Lizzie.frame.isPlayingAgainstLeelaz + && Lizzie.frame.playerIsBlack == getData().blackToPlay) { + Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); + Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "W" : "B")); + } else if (!Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.leelaz.playMove(color, convertCoordinatesToName(x, y)); + } + + // update history with this coordinate + history.addOrGoto(newState); + + Lizzie.frame.repaint(); } - - /** - * overloaded method for place(), chooses color in an alternating pattern - * - * @param x x coordinate - * @param y y coordinate - */ - public void place(int x, int y) { - place(x, y, history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); + } + + /** + * overloaded method for place(), chooses color in an alternating pattern + * + * @param x x coordinate + * @param y y coordinate + */ + public void place(int x, int y) { + place(x, y, history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); + } + + /** + * overloaded method for place. To be used by the LeelaZ engine. Color is then assumed to be + * alternating + * + * @param namedCoordinate the coordinate to place a stone, + */ + public void place(String namedCoordinate) { + if (namedCoordinate.contains("pass")) { + pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); + return; + } else if (namedCoordinate.contains("resign")) { + pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); + return; } - /** - * overloaded method for place. To be used by the LeelaZ engine. Color is then assumed to be alternating - * - * @param namedCoordinate the coordinate to place a stone, - */ - public void place(String namedCoordinate) { - if (namedCoordinate.contains("pass")) { - pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); - return; - } else if (namedCoordinate.contains("resign")) { - pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); - return; - } - - int[] coordinates = convertNameToCoordinates(namedCoordinate); - - place(coordinates[0], coordinates[1]); + int[] coordinates = convertNameToCoordinates(namedCoordinate); + + place(coordinates[0], coordinates[1]); + } + + /** for handicap */ + public void flatten() { + Stone[] stones = history.getStones(); + boolean blackToPlay = history.isBlacksTurn(); + Zobrist zobrist = history.getZobrist().clone(); + BoardHistoryList oldHistory = history; + history = + new BoardHistoryList( + new BoardData( + stones, + null, + Stone.EMPTY, + blackToPlay, + zobrist, + 0, + new int[BOARD_SIZE * BOARD_SIZE], + 0, + 0, + 0.0, + 0)); + history.setGameInfo(oldHistory.getGameInfo()); + } + + /** + * Removes a chain if it has no liberties + * + * @param x x coordinate -- needn't be valid + * @param y y coordinate -- needn't be valid + * @param color the color of the chain to remove + * @param stones the stones array to modify + * @param zobrist the zobrist object to modify + * @return number of removed stones + */ + private int removeDeadChain(int x, int y, Stone color, Stone[] stones, Zobrist zobrist) { + if (!isValid(x, y) || stones[getIndex(x, y)] != color) return 0; + + boolean hasLiberties = hasLibertiesHelper(x, y, color, stones); + + // either remove stones or reset what hasLibertiesHelper does to the board + return cleanupHasLibertiesHelper(x, y, color.recursed(), stones, zobrist, !hasLiberties); + } + + /** + * Recursively determines if a chain has liberties. Alters the state of stones, so it must be + * counteracted + * + * @param x x coordinate -- needn't be valid + * @param y y coordinate -- needn't be valid + * @param color the color of the chain to be investigated + * @param stones the stones array to modify + * @return whether or not this chain has liberties + */ + private boolean hasLibertiesHelper(int x, int y, Stone color, Stone[] stones) { + if (!isValid(x, y)) return false; + + if (stones[getIndex(x, y)] == Stone.EMPTY) return true; // a liberty was found + else if (stones[getIndex(x, y)] != color) + return false; // we are either neighboring an enemy stone, or one we've already recursed on + + // set this index to be the recursed color to keep track of where we've already searched + stones[getIndex(x, y)] = color.recursed(); + + // set removeDeadChain to true if any recursive calls return true. Recurse in all 4 directions + boolean hasLiberties = + hasLibertiesHelper(x + 1, y, color, stones) + || hasLibertiesHelper(x, y + 1, color, stones) + || hasLibertiesHelper(x - 1, y, color, stones) + || hasLibertiesHelper(x, y - 1, color, stones); + + return hasLiberties; + } + + /** + * cleans up what hasLibertyHelper does to the board state + * + * @param x x coordinate -- needn't be valid + * @param y y coordinate -- needn't be valid + * @param color color to clean up. Must be a recursed stone type + * @param stones the stones array to modify + * @param zobrist the zobrist object to modify + * @param removeStones if true, we will remove all these stones. otherwise, we will set them to + * their unrecursed version + * @return number of removed stones + */ + private int cleanupHasLibertiesHelper( + int x, int y, Stone color, Stone[] stones, Zobrist zobrist, boolean removeStones) { + int removed = 0; + if (!isValid(x, y) || stones[getIndex(x, y)] != color) return 0; + + stones[getIndex(x, y)] = removeStones ? Stone.EMPTY : color.unrecursed(); + if (removeStones) { + zobrist.toggleStone(x, y, color.unrecursed()); + removed = 1; } - /** - * for handicap - */ - public void flatten() { - Stone[] stones = history.getStones(); - boolean blackToPlay = history.isBlacksTurn(); - Zobrist zobrist = history.getZobrist().clone(); - history = new BoardHistoryList(new BoardData(stones, null, Stone.EMPTY, blackToPlay, zobrist, - 0, new int[BOARD_SIZE * BOARD_SIZE], 0, 0, 0.0, 0)); - } - - /** - * Removes a chain if it has no liberties - * - * @param x x coordinate -- needn't be valid - * @param y y coordinate -- needn't be valid - * @param color the color of the chain to remove - * @param stones the stones array to modify - * @param zobrist the zobrist object to modify - * @return number of removed stones - */ - private int removeDeadChain(int x, int y, Stone color, Stone[] stones, Zobrist zobrist) { - if (!isValid(x, y) || stones[getIndex(x, y)] != color) - return 0; - - boolean hasLiberties = hasLibertiesHelper(x, y, color, stones); - - // either remove stones or reset what hasLibertiesHelper does to the board - return cleanupHasLibertiesHelper(x, y, color.recursed(), stones, zobrist, !hasLiberties); + // use the flood fill algorithm to replace all adjacent recursed stones + removed += cleanupHasLibertiesHelper(x + 1, y, color, stones, zobrist, removeStones); + removed += cleanupHasLibertiesHelper(x, y + 1, color, stones, zobrist, removeStones); + removed += cleanupHasLibertiesHelper(x - 1, y, color, stones, zobrist, removeStones); + removed += cleanupHasLibertiesHelper(x, y - 1, color, stones, zobrist, removeStones); + return removed; + } + + /** + * get current board state + * + * @return the stones array corresponding to the current board state + */ + public Stone[] getStones() { + return history.getStones(); + } + + /** + * shows where to mark the last coordinate + * + * @return the last played stone + */ + public int[] getLastMove() { + return history.getLastMove(); + } + + /** + * get the move played in this position + * + * @return the next move, if any + */ + public int[] getNextMove() { + return history.getNextMove(); + } + + /** + * get current board move number + * + * @return the int array corresponding to the current board move number + */ + public int[] getMoveNumberList() { + return history.getMoveNumberList(); + } + + /** Goes to the next coordinate, thread safe */ + public boolean nextMove() { + synchronized (this) { + // Update win rate statistics + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + if (stats.totalPlayouts >= history.getData().playouts) { + history.getData().winrate = stats.maxWinrate; + history.getData().playouts = stats.totalPlayouts; + } + if (history.next() != null) { + // update leelaz board position, before updating to next node + if (history.getData().lastMove == null) { + Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); + } else { + Lizzie.leelaz.playMove( + history.getLastMoveColor(), + convertCoordinatesToName(history.getLastMove()[0], history.getLastMove()[1])); + } + Lizzie.frame.repaint(); + return true; + } + return false; } - - /** - * Recursively determines if a chain has liberties. Alters the state of stones, so it must be counteracted - * - * @param x x coordinate -- needn't be valid - * @param y y coordinate -- needn't be valid - * @param color the color of the chain to be investigated - * @param stones the stones array to modify - * @return whether or not this chain has liberties - */ - private boolean hasLibertiesHelper(int x, int y, Stone color, Stone[] stones) { - if (!isValid(x, y)) - return false; - - if (stones[getIndex(x, y)] == Stone.EMPTY) - return true; // a liberty was found - else if (stones[getIndex(x, y)] != color) - return false; // we are either neighboring an enemy stone, or one we've already recursed on - - // set this index to be the recursed color to keep track of where we've already searched - stones[getIndex(x, y)] = color.recursed(); - - // set removeDeadChain to true if any recursive calls return true. Recurse in all 4 directions - boolean hasLiberties = hasLibertiesHelper(x + 1, y, color, stones) || - hasLibertiesHelper(x, y + 1, color, stones) || - hasLibertiesHelper(x - 1, y, color, stones) || - hasLibertiesHelper(x, y - 1, color, stones); - - return hasLiberties; + } + + /** + * Goes to the next coordinate, thread safe + * + * @param fromBackChildren by back children branch + * @return true when has next variation + */ + public boolean nextMove(int fromBackChildren) { + synchronized (this) { + // Update win rate statistics + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + if (stats.totalPlayouts >= history.getData().playouts) { + history.getData().winrate = stats.maxWinrate; + history.getData().playouts = stats.totalPlayouts; + } + return nextVariation(fromBackChildren); } - - /** - * cleans up what hasLibertyHelper does to the board state - * - * @param x x coordinate -- needn't be valid - * @param y y coordinate -- needn't be valid - * @param color color to clean up. Must be a recursed stone type - * @param stones the stones array to modify - * @param zobrist the zobrist object to modify - * @param removeStones if true, we will remove all these stones. otherwise, we will set them to their unrecursed version - * @return number of removed stones - */ - private int cleanupHasLibertiesHelper(int x, int y, Stone color, Stone[] stones, Zobrist zobrist, boolean removeStones) { - int removed = 0; - if (!isValid(x, y) || stones[getIndex(x, y)] != color) - return 0; - - stones[getIndex(x, y)] = removeStones ? Stone.EMPTY : color.unrecursed(); - if (removeStones) { - zobrist.toggleStone(x, y, color.unrecursed()); - removed = 1; - } - - // use the flood fill algorithm to replace all adjacent recursed stones - removed += cleanupHasLibertiesHelper(x + 1, y, color, stones, zobrist, removeStones); - removed += cleanupHasLibertiesHelper(x, y + 1, color, stones, zobrist, removeStones); - removed += cleanupHasLibertiesHelper(x - 1, y, color, stones, zobrist, removeStones); - removed += cleanupHasLibertiesHelper(x, y - 1, color, stones, zobrist, removeStones); - return removed; + } + + /** Save the move number for restore If in the branch, save the back routing from children */ + public void saveMoveNumber() { + BoardHistoryNode curNode = history.getCurrentHistoryNode(); + int curMoveNum = curNode.getData().moveNumber; + if (curMoveNum > 0) { + if (!BoardHistoryList.isMainTrunk(curNode)) { + // If in branch, save the back routing from children + saveBackRouting(curNode); + } + goToMoveNumber(0); } - - /** - * get current board state - * - * @return the stones array corresponding to the current board state - */ - public Stone[] getStones() { - return history.getStones(); + saveNode = curNode; + } + + /** Save the back routing from children */ + public void saveBackRouting(BoardHistoryNode node) { + if (node != null && node.previous() != null) { + node.previous().setFromBackChildren(node.previous().getNexts().indexOf(node)); + saveBackRouting(node.previous()); } + } - /** - * shows where to mark the last coordinate - * - * @return the last played stone - */ - public int[] getLastMove() { - return history.getLastMove(); - } + /** Restore move number by saved node */ + public void restoreMoveNumber() { + restoreMoveNumber(saveNode); + } - /** - * get the move played in this position - * - * @return the next move, if any - */ - public int[] getNextMove() { - return history.getNextMove(); + /** Restore move number by node */ + public void restoreMoveNumber(BoardHistoryNode node) { + if (node == null) { + return; } - - /** - * get current board move number - * - * @return the int array corresponding to the current board move number - */ - public int[] getMoveNumberList() { - return history.getMoveNumberList(); + int moveNumber = node.getData().moveNumber; + if (moveNumber > 0) { + if (BoardHistoryList.isMainTrunk(node)) { + goToMoveNumber(moveNumber); + } else { + // If in Branch, restore by the back routing + goToMoveNumberByBackChildren(moveNumber); + } } - - /** - * Goes to the next coordinate, thread safe - */ - public boolean nextMove() { - synchronized (this) { - // Update win rate statistics - Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); - if (stats.totalPlayouts >= history.getData().playouts) { - history.getData().winrate = stats.maxWinrate; - history.getData().playouts = stats.totalPlayouts; - } - if (history.next() != null) { - // update leelaz board position, before updating to next node - if (history.getData().lastMove == null) { - Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); - } else { - Lizzie.leelaz.playMove(history.getLastMoveColor(), convertCoordinatesToName(history.getLastMove()[0], history.getLastMove()[1])); - } - Lizzie.frame.repaint(); - return true; - } - return false; + } + + /** Go to move number by back routing from children when in branch */ + public void goToMoveNumberByBackChildren(int moveNumber) { + int delta = moveNumber - history.getMoveNumber(); + for (int i = 0; i < Math.abs(delta); i++) { + BoardHistoryNode curNode = history.getCurrentHistoryNode(); + if (curNode.numberOfChildren() > 1 && delta > 0) { + nextMove(curNode.getFromBackChildren()); + } else { + if (!(delta > 0 ? nextMove() : previousMove())) { + break; } + } } + } - public boolean goToMoveNumber(int moveNumber) { - return goToMoveNumberHelper(moveNumber, false); - } + public boolean goToMoveNumber(int moveNumber) { + return goToMoveNumberHelper(moveNumber, false); + } - public boolean goToMoveNumberWithinBranch(int moveNumber) { - return goToMoveNumberHelper(moveNumber, true); - } + public boolean goToMoveNumberWithinBranch(int moveNumber) { + return goToMoveNumberHelper(moveNumber, true); + } - public boolean goToMoveNumberBeyondBranch(int moveNumber) { - // Go to main trunk if current branch is shorter than moveNumber. - if (moveNumber > history.currentBranchLength() && - moveNumber <= history.mainTrunkLength()) { - goToMoveNumber(0); + public boolean goToMoveNumberBeyondBranch(int moveNumber) { + // Go to main trunk if current branch is shorter than moveNumber. + if (moveNumber > history.currentBranchLength() && moveNumber <= history.mainTrunkLength()) { + goToMoveNumber(0); + } + return goToMoveNumber(moveNumber); + } + + public boolean goToMoveNumberHelper(int moveNumber, boolean withinBranch) { + int delta = moveNumber - history.getMoveNumber(); + boolean moved = false; + for (int i = 0; i < Math.abs(delta); i++) { + if (withinBranch && delta < 0) { + BoardHistoryNode curNode = history.getCurrentHistoryNode(); + if (!curNode.isFirstChild()) { + break; } - return goToMoveNumber(moveNumber); + } + if (!(delta > 0 ? nextMove() : previousMove())) { + break; + } + moved = true; } - - public boolean goToMoveNumberHelper(int moveNumber, boolean withinBranch) { - int delta = moveNumber - history.getMoveNumber(); - boolean moved = false; - for (int i = 0; i < Math.abs(delta); i++) { - if (withinBranch && delta < 0) { - BoardHistoryNode curNode = history.getCurrentHistoryNode(); - if (!curNode.isFirstChild()) { - break; - } - } - if (!(delta > 0 ? nextMove() : previousMove())) { - break; - } - moved = true; + return moved; + } + + /** Goes to the next variation, thread safe */ + public boolean nextVariation(int idx) { + synchronized (this) { + // Don't update winrate here as this is usually called when jumping between variations + if (history.nextVariation(idx) != null) { + // update leelaz board position, before updating to next node + if (history.getData().lastMove == null) { + Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); + } else { + Lizzie.leelaz.playMove( + history.getLastMoveColor(), + convertCoordinatesToName(history.getLastMove()[0], history.getLastMove()[1])); } - return moved; + Lizzie.frame.repaint(); + return true; + } + return false; } - - /** - * Goes to the next variation, thread safe - */ - public boolean nextVariation(int idx) { - synchronized (this) { - // Don't update winrate here as this is usually called when jumping between variations - if (history.nextVariation(idx) != null) { - // update leelaz board position, before updating to next node - if (history.getData().lastMove == null) { - Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); - } else { - Lizzie.leelaz.playMove(history.getLastMoveColor(), convertCoordinatesToName(history.getLastMove()[0], history.getLastMove()[1])); - } - Lizzie.frame.repaint(); - return true; + } + + /* + * Moves to next variation (variation to the right) if possible + * To move to another variation, the variation must have a move with the same move number as the current move in it. + * Note: Will only look within variations that start at the same move on the main trunk/branch, and if on trunk + * only in the ones closest + */ + public boolean nextBranch() { + synchronized (this) { + BoardHistoryNode curNode = history.getCurrentHistoryNode(); + int curMoveNum = curNode.getData().moveNumber; + // First check if there is a branch to move to, if not, stay in same place + // Requirement: variaton need to have a move number same as current + if (BoardHistoryList.isMainTrunk(curNode)) { + // Check if there is a variation tree to the right that is deep enough + BoardHistoryNode startVarNode = BoardHistoryList.findChildOfPreviousWithVariation(curNode); + if (startVarNode == null) return false; + startVarNode = startVarNode.previous(); + boolean isDeepEnough = false; + for (int i = 1; i < startVarNode.numberOfChildren(); i++) { + if (BoardHistoryList.hasDepth( + startVarNode.getVariation(i), curMoveNum - startVarNode.getData().moveNumber - 1)) { + isDeepEnough = true; + break; + } + } + if (!isDeepEnough) return false; + } else { + // We are in a variation, is there some variation to the right? + BoardHistoryNode tmpNode = curNode; + while (tmpNode != null) { + // Try to move to the right + BoardHistoryNode prevBranch = BoardHistoryList.findChildOfPreviousWithVariation(tmpNode); + int idx = BoardHistoryList.findIndexOfNode(prevBranch.previous(), prevBranch); + // Check if there are branches to the right, that are deep enough + boolean isDeepEnough = false; + for (int i = idx + 1; i < prevBranch.previous().numberOfChildren(); i++) { + if (BoardHistoryList.hasDepth( + prevBranch.previous().getVariation(i), + curMoveNum - prevBranch.previous().getData().moveNumber - 1)) { + isDeepEnough = true; + break; } + } + if (isDeepEnough) break; + // Did not find a deep enough branch, move up unless we reached main trunk + if (BoardHistoryList.isMainTrunk(prevBranch.previous())) { + // No right hand side branch to move too return false; + } + tmpNode = prevBranch.previous(); } - } - - /* - * Moves to next variation (variation to the right) if possible - * To move to another variation, the variation must have a move with the same move number as the current move in it. - * Note: Will only look within variations that start at the same move on the main trunk/branch, and if on trunk - * only in the ones closest - */ - public boolean nextBranch() { - synchronized (this) { - BoardHistoryNode curNode = history.getCurrentHistoryNode(); - int curMoveNum = curNode.getData().moveNumber; - // First check if there is a branch to move to, if not, stay in same place - // Requirement: variaton need to have a move number same as current - if (BoardHistoryList.isMainTrunk(curNode)) { - // Check if there is a variation tree to the right that is deep enough - BoardHistoryNode startVarNode = BoardHistoryList.findChildOfPreviousWithVariation(curNode); - if (startVarNode == null) return false; - startVarNode = startVarNode.previous(); - boolean isDeepEnough = false; - for (int i = 1; i < startVarNode.numberOfChildren(); i++) - { - if (BoardHistoryList.hasDepth(startVarNode.getVariation(i), curMoveNum - startVarNode.getData().moveNumber - 1)) { - isDeepEnough = true; - break; - } - } - if (!isDeepEnough) return false; - } else { - // We are in a variation, is there some variation to the right? - BoardHistoryNode tmpNode = curNode; - while (tmpNode != null) { - // Try to move to the right - BoardHistoryNode prevBranch = BoardHistoryList.findChildOfPreviousWithVariation(tmpNode); - int idx = BoardHistoryList.findIndexOfNode(prevBranch.previous(), prevBranch); - // Check if there are branches to the right, that are deep enough - boolean isDeepEnough = false; - for (int i = idx + 1; i < prevBranch.previous().numberOfChildren(); i++) { - if (BoardHistoryList.hasDepth(prevBranch.previous().getVariation(i), - curMoveNum - prevBranch.previous().getData().moveNumber - 1)) { - isDeepEnough = true; - break; - } - } - if (isDeepEnough) - break; - // Did not find a deep enough branch, move up unless we reached main trunk - if (BoardHistoryList.isMainTrunk(prevBranch.previous())) { - // No right hand side branch to move too - return false; - } - tmpNode = prevBranch.previous(); - - } + } + + // At this point, we know there is somewhere to move to... Move there, one step at the time + // (because of Leelaz) + // Start moving up the tree until we reach a moves with variations that are deep enough + BoardHistoryNode prevNode; + int startIdx = 0; + while (curNode.previous() != null) { + prevNode = curNode; + previousMove(); + curNode = history.getCurrentHistoryNode(); + startIdx = BoardHistoryList.findIndexOfNode(curNode, prevNode) + 1; + if (curNode.numberOfChildren() > 1 && startIdx <= curNode.numberOfChildren()) { + // Find the variation that is deep enough + boolean isDeepEnough = false; + for (int i = startIdx; i < curNode.numberOfChildren(); i++) { + if (BoardHistoryList.hasDepth( + curNode.getVariation(i), curMoveNum - curNode.getData().moveNumber - 1)) { + isDeepEnough = true; + break; } - - // At this point, we know there is somewhere to move to... Move there, one step at the time (because of Leelaz) - // Start moving up the tree until we reach a moves with variations that are deep enough - BoardHistoryNode prevNode; - int startIdx = 0; - while (curNode.previous() != null) { - prevNode = curNode; - previousMove(); - curNode = history.getCurrentHistoryNode(); - startIdx = BoardHistoryList.findIndexOfNode(curNode, prevNode) + 1; - if (curNode.numberOfChildren() > 1 && startIdx <= curNode.numberOfChildren()) { - // Find the variation that is deep enough - boolean isDeepEnough = false; - for (int i = startIdx; i < curNode.numberOfChildren(); i++) { - if (BoardHistoryList.hasDepth(curNode.getVariation(i), curMoveNum - curNode.getData().moveNumber - 1)) - { - isDeepEnough = true; - break; - } - } - if (isDeepEnough) - break; - } - } - // Now move forward in new branch - while (curNode.getData().moveNumber < curMoveNum) { - if (curNode.numberOfChildren() == 1) { - // One-way street, just move to next - if (!nextVariation(0)) { - // Not supposed to happen, fail-safe - break; - } - } else { - // Has several variations, need to find the closest one that is deep enough - for (int i = startIdx; i < curNode.numberOfChildren(); i++) - { - if (BoardHistoryList.hasDepth(curNode.getVariation(i), curMoveNum - curNode.getData().moveNumber - 1)) { - nextVariation(i); - break; - } - } - } - startIdx = 0; - curNode = history.getCurrentHistoryNode(); + } + if (isDeepEnough) break; + } + } + // Now move forward in new branch + while (curNode.getData().moveNumber < curMoveNum) { + if (curNode.numberOfChildren() == 1) { + // One-way street, just move to next + if (!nextVariation(0)) { + // Not supposed to happen, fail-safe + break; + } + } else { + // Has several variations, need to find the closest one that is deep enough + for (int i = startIdx; i < curNode.numberOfChildren(); i++) { + if (BoardHistoryList.hasDepth( + curNode.getVariation(i), curMoveNum - curNode.getData().moveNumber - 1)) { + nextVariation(i); + break; } - return true; + } } + startIdx = 0; + curNode = history.getCurrentHistoryNode(); + } + return true; } - - /* - * Moves to previous variation (variation to the left) if possible, or back to main trunk - * To move to another variation, the variation must have the same number of moves in it. - * If no variation with sufficient moves exist, move back to main trunk. - * Note: Will always move back to main trunk, even if variation has more moves than main trunk (if that - * is the case it will move to the last move in the trunk) - */ - public boolean previousBranch() { - synchronized (this) { - BoardHistoryNode curNode = history.getCurrentHistoryNode(); - BoardHistoryNode prevNode; - int curMoveNum = curNode.getData().moveNumber; - - if (BoardHistoryList.isMainTrunk(curNode)) { - // Not possible to move further to the left, so just return - return false; - } - // We already know we can move back (back to main trunk if necessary), so just start moving - // Move backwards first - int depth = 0; - int startIdx = 0; - boolean foundBranch = false; - while (!BoardHistoryList.isMainTrunk(curNode)) { - prevNode = curNode; - // Move back - previousMove(); - curNode = history.getCurrentHistoryNode(); - depth++; - startIdx = BoardHistoryList.findIndexOfNode(curNode, prevNode); - // If current move has children, check if any of those are deep enough (starting at the one closest) - if (curNode.numberOfChildren() > 1 && startIdx != 0) { - foundBranch = false; - for (int i = startIdx - 1; i >= 0; i--) { - if (BoardHistoryList.hasDepth(curNode.getVariation(i), depth-1)) { - foundBranch = true; - startIdx = i; - break; - } - } - if (foundBranch) break; // Found a variation (or main trunk) and it is deep enough - } + } + + /* + * Moves to previous variation (variation to the left) if possible, or back to main trunk + * To move to another variation, the variation must have the same number of moves in it. + * If no variation with sufficient moves exist, move back to main trunk. + * Note: Will always move back to main trunk, even if variation has more moves than main trunk (if that + * is the case it will move to the last move in the trunk) + */ + public boolean previousBranch() { + synchronized (this) { + BoardHistoryNode curNode = history.getCurrentHistoryNode(); + BoardHistoryNode prevNode; + int curMoveNum = curNode.getData().moveNumber; + + if (BoardHistoryList.isMainTrunk(curNode)) { + // Not possible to move further to the left, so just return + return false; + } + // We already know we can move back (back to main trunk if necessary), so just start moving + // Move backwards first + int depth = 0; + int startIdx = 0; + boolean foundBranch = false; + while (!BoardHistoryList.isMainTrunk(curNode)) { + prevNode = curNode; + // Move back + previousMove(); + curNode = history.getCurrentHistoryNode(); + depth++; + startIdx = BoardHistoryList.findIndexOfNode(curNode, prevNode); + // If current move has children, check if any of those are deep enough (starting at the one + // closest) + if (curNode.numberOfChildren() > 1 && startIdx != 0) { + foundBranch = false; + for (int i = startIdx - 1; i >= 0; i--) { + if (BoardHistoryList.hasDepth(curNode.getVariation(i), depth - 1)) { + foundBranch = true; + startIdx = i; + break; } + } + if (foundBranch) break; // Found a variation (or main trunk) and it is deep enough + } + } - if (!foundBranch) - { - // Back at main trunk, and it is not long enough, move forward until we reach the end.. - while (nextVariation(0)) { - - }; - return true; - } + if (!foundBranch) { + // Back at main trunk, and it is not long enough, move forward until we reach the end.. + while (nextVariation(0)) {} - // At this point, we are either back at the main trunk, or on top of variation we know is long enough - // Move forward - while (curNode.getData().moveNumber < curMoveNum && curNode.next() != null) { - if (curNode.numberOfChildren() == 1) { - // One-way street, just move to next - if (!nextVariation(0)) { - // Not supposed to happen... - break; - } - } else { - foundBranch = false; - // Several variations to choose between, make sure we select the one closest that is deep enough (if any) - for (int i = startIdx; i >= 0; i--) - { - if (BoardHistoryList.hasDepth(curNode.getVariation(i), curMoveNum - curNode.getData().moveNumber - 1)) { - nextVariation(i); - foundBranch = true; - break; - } - } - if (!foundBranch) { - // Not supposed to happen, fail-safe - nextVariation(0); - } - } - // We have now moved one step further down - curNode = history.getCurrentHistoryNode(); - startIdx = curNode.numberOfChildren() - 1; + ; + return true; + } + + // At this point, we are either back at the main trunk, or on top of variation we know is long + // enough + // Move forward + while (curNode.getData().moveNumber < curMoveNum && curNode.next() != null) { + if (curNode.numberOfChildren() == 1) { + // One-way street, just move to next + if (!nextVariation(0)) { + // Not supposed to happen... + break; + } + } else { + foundBranch = false; + // Several variations to choose between, make sure we select the one closest that is deep + // enough (if any) + for (int i = startIdx; i >= 0; i--) { + if (BoardHistoryList.hasDepth( + curNode.getVariation(i), curMoveNum - curNode.getData().moveNumber - 1)) { + nextVariation(i); + foundBranch = true; + break; } - return true; + } + if (!foundBranch) { + // Not supposed to happen, fail-safe + nextVariation(0); + } } + // We have now moved one step further down + curNode = history.getCurrentHistoryNode(); + startIdx = curNode.numberOfChildren() - 1; + } + return true; } + } - public void moveBranchUp() { - synchronized (this) { - history.getCurrentHistoryNode().topOfBranch().moveUp(); - } - } - - public void moveBranchDown() { - synchronized (this) { - history.getCurrentHistoryNode().topOfBranch().moveDown(); - } + public void moveBranchUp() { + synchronized (this) { + history.getCurrentHistoryNode().topOfBranch().moveUp(); } + } - public void deleteMove() { - synchronized (this) { - BoardHistoryNode curNode = history.getCurrentHistoryNode(); - if (curNode.next() != null) { - // Will delete more than one move, ask for confirmation - int ret = JOptionPane.showConfirmDialog(null, "This will delete all moves and branches after this move", "Delete", JOptionPane.OK_CANCEL_OPTION); - if (ret != JOptionPane.OK_OPTION) { - return; - } - - } - // Clear the board if we're at the top - if (curNode.previous() == null) { - clear(); - return; - } - previousMove(); - int idx = BoardHistoryList.findIndexOfNode(curNode.previous(), curNode); - curNode.previous().deleteChild(idx); - } + public void moveBranchDown() { + synchronized (this) { + history.getCurrentHistoryNode().topOfBranch().moveDown(); } - - public void deleteBranch() { - int originalMoveNumber = history.getMoveNumber(); - undoToChildOfPreviousWithVariation(); - int moveNumberBeforeOperation = history.getMoveNumber(); - deleteMove(); - boolean canceled = (history.getMoveNumber() == moveNumberBeforeOperation); - if (canceled) { - goToMoveNumber(originalMoveNumber); + } + + public void deleteMove() { + synchronized (this) { + BoardHistoryNode curNode = history.getCurrentHistoryNode(); + if (curNode.next() != null) { + // Will delete more than one move, ask for confirmation + int ret = + JOptionPane.showConfirmDialog( + null, + "This will delete all moves and branches after this move", + "Delete", + JOptionPane.OK_CANCEL_OPTION); + if (ret != JOptionPane.OK_OPTION) { + return; } + } + // Clear the board if we're at the top + if (curNode.previous() == null) { + clear(); + return; + } + previousMove(); + int idx = BoardHistoryList.findIndexOfNode(curNode.previous(), curNode); + curNode.previous().deleteChild(idx); } - - public BoardData getData() { - return history.getData(); + } + + public void deleteBranch() { + int originalMoveNumber = history.getMoveNumber(); + undoToChildOfPreviousWithVariation(); + int moveNumberBeforeOperation = history.getMoveNumber(); + deleteMove(); + boolean canceled = (history.getMoveNumber() == moveNumberBeforeOperation); + if (canceled) { + goToMoveNumber(originalMoveNumber); } - - public BoardHistoryList getHistory() { - return history; + } + + public BoardData getData() { + return history.getData(); + } + + public BoardHistoryList getHistory() { + return history; + } + + /** Clears all history and starts over from empty board. */ + public void clear() { + Lizzie.leelaz.sendCommand("clear_board"); + Lizzie.frame.resetTitle(); + initialize(); + } + + /** Goes to the previous coordinate, thread safe */ + public boolean previousMove() { + synchronized (this) { + if (inScoreMode()) setScoreMode(false); + // Update win rate statistics + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + + if (stats.totalPlayouts >= history.getData().playouts) { + history.getData().winrate = stats.maxWinrate; + history.getData().playouts = stats.totalPlayouts; + } + if (history.previous() != null) { + Lizzie.leelaz.undo(); + Lizzie.frame.repaint(); + return true; + } + return false; } - - /** - * Clears all history and starts over from empty board. - */ - public void clear() { - Lizzie.leelaz.sendCommand("clear_board"); - Lizzie.frame.resetTitle(); - initialize(); + } + + public boolean undoToChildOfPreviousWithVariation() { + BoardHistoryNode start = history.getCurrentHistoryNode(); + BoardHistoryNode goal = history.findChildOfPreviousWithVariation(start); + if (start == goal) return false; + while ((history.getCurrentHistoryNode() != goal) && previousMove()) ; + return true; + } + + public void setScoreMode(boolean on) { + if (on) { + // load a copy of the data at the current node of history + capturedStones = history.getStones().clone(); + } else { + capturedStones = null; } - - /** - * Goes to the previous coordinate, thread safe - */ - public boolean previousMove() { - synchronized (this) { - if (inScoreMode()) setScoreMode(false); - // Update win rate statistics - Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); - if (stats.totalPlayouts >= history.getData().playouts) { - history.getData().winrate = stats.maxWinrate; - history.getData().playouts = stats.totalPlayouts; - } - if (history.previous() != null) { - Lizzie.leelaz.undo(); - Lizzie.frame.repaint(); - return true; - } - return false; - } + scoreMode = on; + } + + /* + * Starting at position stonex, stoney, remove all stones with same color within an area bordered by stones + * of opposite color (AKA captured stones) + */ + private void toggleLiveStatus(Stone[] stones, int stonex, int stoney) { + Stone[] shdwstones = stones.clone(); + Stone toggle = stones[getIndex(stonex, stoney)]; + Stone toggleToo; + switch (toggle) { + case BLACK: + toggleToo = Stone.BLACK_CAPTURED; + break; + case BLACK_CAPTURED: + toggleToo = Stone.BLACK; + break; + case WHITE: + toggleToo = Stone.WHITE_CAPTURED; + break; + case WHITE_CAPTURED: + toggleToo = Stone.WHITE; + break; + default: + return; } - - public boolean undoToChildOfPreviousWithVariation() { - BoardHistoryNode start = history.getCurrentHistoryNode(); - BoardHistoryNode goal = history.findChildOfPreviousWithVariation(start); - if (start == goal) - return false; - while ((history.getCurrentHistoryNode() != goal) && previousMove()) ; - return true; - } - - public void setScoreMode(boolean on) { - if (on) { - // load a copy of the data at the current node of history - capturedStones = history.getStones().clone(); + boolean lastup, lastdown; + // This is using a flood fill algorithm that uses a Q instead of being recursive + Queue visitQ = new ArrayDeque<>(); + visitQ.add(new int[] {stonex, stoney}); + while (!visitQ.isEmpty()) { + int[] curpos = visitQ.remove(); + int x = curpos[0]; + int y = curpos[1]; + + // Move all the way left + while (x > 0 + && (stones[getIndex(x - 1, y)] == Stone.EMPTY || stones[getIndex(x - 1, y)] == toggle)) { + x--; + } + + lastup = lastdown = false; + // Find all stones within empty area line by line (new lines added to Q) + while (x < BOARD_SIZE) { + if (shdwstones[getIndex(x, y)] == Stone.EMPTY) { + shdwstones[getIndex(x, y)] = Stone.DAME; // Too mark that it has been visited + } else if (stones[getIndex(x, y)] == toggle) { + stones[getIndex(x, y)] = toggleToo; } else { - capturedStones = null; + break; } - scoreMode = on; - } - - /* - * Starting at position stonex, stoney, remove all stones with same color within an area bordered by stones - * of opposite color (AKA captured stones) - */ - private void toggleLiveStatus(Stone[] stones, int stonex, int stoney) - { - Stone[] shdwstones = stones.clone(); - Stone toggle = stones[getIndex(stonex, stoney)]; - Stone toggleToo; - switch (toggle) { - case BLACK: - toggleToo = Stone.BLACK_CAPTURED; - break; - case BLACK_CAPTURED: - toggleToo = Stone.BLACK; - break; - case WHITE: - toggleToo = Stone.WHITE_CAPTURED; - break; - case WHITE_CAPTURED: - toggleToo = Stone.WHITE; - break; - default: - return; + // Check above + if (y - 1 >= 0 + && (shdwstones[getIndex(x, y - 1)] == Stone.EMPTY + || stones[getIndex(x, y - 1)] == toggle)) { + if (!lastup) visitQ.add(new int[] {x, y - 1}); + lastup = true; + } else { + lastup = false; } - boolean lastup, lastdown; - // This is using a flood fill algorithm that uses a Q instead of being recursive - Queue visitQ = new ArrayDeque<>(); - visitQ.add(new int[]{stonex, stoney}); - while (!visitQ.isEmpty()) - { - int[] curpos = visitQ.remove(); - int x = curpos[0]; - int y = curpos[1]; - - // Move all the way left - while (x > 0 && (stones[getIndex(x-1, y)] == Stone.EMPTY || stones[getIndex(x-1, y)] == toggle)) - { - x--; - } - - lastup = lastdown = false; - // Find all stones within empty area line by line (new lines added to Q) - while (x < BOARD_SIZE ) { - if (shdwstones[getIndex(x, y)] == Stone.EMPTY) { - shdwstones[getIndex(x, y)] = Stone.DAME; // Too mark that it has been visited - } else if (stones[getIndex(x, y)] == toggle) { - stones[getIndex(x, y)] = toggleToo; - } else { - break; - } - // Check above - if (y - 1 >= 0 && (shdwstones[getIndex(x, y - 1)] == Stone.EMPTY || stones[getIndex(x, y - 1)] == toggle)) { - if (!lastup) - visitQ.add(new int[]{x, y - 1}); - lastup = true; - } else { - lastup = false; - } - // Check below - if (y + 1 < BOARD_SIZE && (shdwstones[getIndex(x, y + 1)] == Stone.EMPTY || stones[getIndex(x, y + 1)] == toggle)) { - if (!lastdown) - visitQ.add(new int[]{x, y + 1}); - lastdown = true; - } else { - lastdown = false; - } - x++; - } + // Check below + if (y + 1 < BOARD_SIZE + && (shdwstones[getIndex(x, y + 1)] == Stone.EMPTY + || stones[getIndex(x, y + 1)] == toggle)) { + if (!lastdown) visitQ.add(new int[] {x, y + 1}); + lastdown = true; + } else { + lastdown = false; } - Lizzie.frame.repaint(); + x++; + } } - - /* - * Check if a point on the board is empty or contains a captured stone - */ - private boolean emptyOrCaptured(Stone[] stones, int x, int y) { - int curidx = getIndex(x, y); - if (stones[curidx] == Stone.EMPTY || stones[curidx] == Stone.BLACK_CAPTURED || stones[curidx] == Stone.WHITE_CAPTURED) - return true; - return false; - } - - /* - * Starting from startx, starty, mark all empty points within area as either white, black or dame. - * If two stones of opposite color (neither marked as captured) is encountered, the area is dame. - * - * @return A stone with color white, black or dame - */ - private Stone markEmptyArea(Stone[] stones, int startx, int starty) { - Stone[] shdwstones = stones.clone(); - Stone found = Stone.EMPTY; // Found will either be black or white, or dame if both are found in area - boolean lastup, lastdown; - Queue visitQ = new ArrayDeque<>(); - visitQ.add(new int[]{startx, starty}); - Deque allPoints = new ArrayDeque<>(); - // Check one line at the time, new lines added to visitQ - while (!visitQ.isEmpty()) - { - int[] curpos = visitQ.remove(); - int x = curpos[0]; - int y = curpos[1]; - if (!emptyOrCaptured(shdwstones, x, y)) { - continue; + Lizzie.frame.repaint(); + } + + /* + * Check if a point on the board is empty or contains a captured stone + */ + private boolean emptyOrCaptured(Stone[] stones, int x, int y) { + int curidx = getIndex(x, y); + if (stones[curidx] == Stone.EMPTY + || stones[curidx] == Stone.BLACK_CAPTURED + || stones[curidx] == Stone.WHITE_CAPTURED) return true; + return false; + } + + /* + * Starting from startx, starty, mark all empty points within area as either white, black or dame. + * If two stones of opposite color (neither marked as captured) is encountered, the area is dame. + * + * @return A stone with color white, black or dame + */ + private Stone markEmptyArea(Stone[] stones, int startx, int starty) { + Stone[] shdwstones = stones.clone(); + Stone found = + Stone.EMPTY; // Found will either be black or white, or dame if both are found in area + boolean lastup, lastdown; + Queue visitQ = new ArrayDeque<>(); + visitQ.add(new int[] {startx, starty}); + Deque allPoints = new ArrayDeque<>(); + // Check one line at the time, new lines added to visitQ + while (!visitQ.isEmpty()) { + int[] curpos = visitQ.remove(); + int x = curpos[0]; + int y = curpos[1]; + if (!emptyOrCaptured(shdwstones, x, y)) { + continue; + } + // Move all the way left + while (x > 0 && emptyOrCaptured(shdwstones, x - 1, y)) { + x--; + } + // Are we on the border, or do we have a stone to the left? + if (x > 0 && shdwstones[getIndex(x - 1, y)] != found) { + if (found == Stone.EMPTY) found = shdwstones[getIndex(x - 1, y)]; + else found = Stone.DAME; + } + + lastup = lastdown = false; + while (x < BOARD_SIZE && emptyOrCaptured(shdwstones, x, y)) { + // Check above + if (y - 1 >= 0 && shdwstones[getIndex(x, y - 1)] != Stone.DAME) { + if (emptyOrCaptured(shdwstones, x, y - 1)) { + if (!lastup) visitQ.add(new int[] {x, y - 1}); + lastup = true; + } else { + lastup = false; + if (found != shdwstones[getIndex(x, y - 1)]) { + if (found == Stone.EMPTY) { + found = shdwstones[getIndex(x, y - 1)]; + } else { + found = Stone.DAME; + } } - // Move all the way left - while (x > 0 && emptyOrCaptured(shdwstones, x - 1, y)) - { - x--; - } - // Are we on the border, or do we have a stone to the left? - if (x > 0 && shdwstones[getIndex(x - 1, y)] != found) { - if (found == Stone.EMPTY) found = shdwstones[getIndex(x - 1, y)]; - else found = Stone.DAME; - } - - lastup = lastdown = false; - while (x < BOARD_SIZE && emptyOrCaptured(shdwstones, x, y)) { - // Check above - if (y - 1 >= 0 && shdwstones[getIndex(x, y - 1)] != Stone.DAME) { - if (emptyOrCaptured(shdwstones, x, y - 1)) { - if (!lastup) - visitQ.add(new int[]{x, y - 1}); - lastup = true; - } else { - lastup = false; - if (found != shdwstones[getIndex(x, y - 1)]) { - if (found == Stone.EMPTY) { - found = shdwstones[getIndex(x, y - 1)]; - } - else { - found = Stone.DAME; - } - } - } - } - // Check below - if (y + 1 < BOARD_SIZE && shdwstones[getIndex(x, y + 1)] != Stone.DAME) { - if (emptyOrCaptured(shdwstones, x, y + 1)) { - if (!lastdown) { - visitQ.add(new int[]{x, y + 1}); - } - lastdown = true; - } else { - lastdown = false; - if (found != shdwstones[getIndex(x, y + 1)]) { - if (found == Stone.EMPTY) { - found = shdwstones[getIndex(x, y + 1)]; - } - else { - found = Stone.DAME; - } - } - } - } - // Add current stone to empty area and mark as visited - if (shdwstones[getIndex(x,y)] == Stone.EMPTY) - allPoints.add(getIndex(x,y)); - - // Use dame stone to mark as visited - shdwstones[getIndex(x, y)] = Stone.DAME; - x++; + } + } + // Check below + if (y + 1 < BOARD_SIZE && shdwstones[getIndex(x, y + 1)] != Stone.DAME) { + if (emptyOrCaptured(shdwstones, x, y + 1)) { + if (!lastdown) { + visitQ.add(new int[] {x, y + 1}); } - // At this point x is at the edge of the board or on a stone - if (x < BOARD_SIZE && shdwstones[getIndex(x, y)] != found) { - if (found == Stone.EMPTY) found = shdwstones[getIndex(x, y )]; - else found = Stone.DAME; + lastdown = true; + } else { + lastdown = false; + if (found != shdwstones[getIndex(x, y + 1)]) { + if (found == Stone.EMPTY) { + found = shdwstones[getIndex(x, y + 1)]; + } else { + found = Stone.DAME; + } } + } } - // Finally mark all points as black or white captured if they were surronded by white or black - if (found == Stone.WHITE) found = Stone.WHITE_POINT; - else if (found == Stone.BLACK) found = Stone.BLACK_POINT; - // else found == DAME and will be set as this. - while (!allPoints.isEmpty()) { - int idx = allPoints.remove(); - stones[idx] = found; - } - return found; + // Add current stone to empty area and mark as visited + if (shdwstones[getIndex(x, y)] == Stone.EMPTY) allPoints.add(getIndex(x, y)); + + // Use dame stone to mark as visited + shdwstones[getIndex(x, y)] = Stone.DAME; + x++; + } + // At this point x is at the edge of the board or on a stone + if (x < BOARD_SIZE && shdwstones[getIndex(x, y)] != found) { + if (found == Stone.EMPTY) found = shdwstones[getIndex(x, y)]; + else found = Stone.DAME; + } + } + // Finally mark all points as black or white captured if they were surronded by white or black + if (found == Stone.WHITE) found = Stone.WHITE_POINT; + else if (found == Stone.BLACK) found = Stone.BLACK_POINT; + // else found == DAME and will be set as this. + while (!allPoints.isEmpty()) { + int idx = allPoints.remove(); + stones[idx] = found; } + return found; + } - /* - * Mark all empty points on board as black point, white point or dame - */ - public Stone[] scoreStones() { + /* + * Mark all empty points on board as black point, white point or dame + */ + public Stone[] scoreStones() { - Stone[] scoreStones = capturedStones.clone(); + Stone[] scoreStones = capturedStones.clone(); - for (int i = 0; i < BOARD_SIZE; i++) { - for (int j = 0; j < BOARD_SIZE; j++) { - if (scoreStones[getIndex(i, j)] == Stone.EMPTY) { - markEmptyArea(scoreStones, i, j); - } - } + for (int i = 0; i < BOARD_SIZE; i++) { + for (int j = 0; j < BOARD_SIZE; j++) { + if (scoreStones[getIndex(i, j)] == Stone.EMPTY) { + markEmptyArea(scoreStones, i, j); } - return scoreStones; + } } - - /* - * Count score for whole board, including komi and captured stones - */ - public double[] getScore(Stone[] scoreStones) { - double score[] = new double[] {getData().blackCaptures, getData().whiteCaptures + getHistory().getGameInfo().getKomi()}; - for (int i = 0; i < BOARD_SIZE; i++) { - for (int j = 0; j < BOARD_SIZE; j++) { - switch (scoreStones[getIndex(i, j)]) { - case BLACK_POINT: - score[0]++; - break; - case BLACK_CAPTURED: - score[1] += 2; - break; - - case WHITE_POINT: - score[1]++; - break; - case WHITE_CAPTURED: - score[0] += 2; - break; - - } - } + return scoreStones; + } + + /* + * Count score for whole board, including komi and captured stones + */ + public double[] getScore(Stone[] scoreStones) { + double score[] = + new double[] { + getData().blackCaptures, getData().whiteCaptures + getHistory().getGameInfo().getKomi() + }; + for (int i = 0; i < BOARD_SIZE; i++) { + for (int j = 0; j < BOARD_SIZE; j++) { + switch (scoreStones[getIndex(i, j)]) { + case BLACK_POINT: + score[0]++; + break; + case BLACK_CAPTURED: + score[1] += 2; + break; + + case WHITE_POINT: + score[1]++; + break; + case WHITE_CAPTURED: + score[0] += 2; + break; } - return score; + } } - - public boolean inAnalysisMode() { - return analysisMode; - } - - public boolean inScoreMode() { - return scoreMode; + return score; + } + + public boolean inAnalysisMode() { + return analysisMode; + } + + public boolean inScoreMode() { + return scoreMode; + } + + public void toggleAnalysis() { + if (analysisMode) { + Lizzie.leelaz.removeListener(this); + analysisMode = false; + } else { + if (getNextMove() == null) return; + String answer = + JOptionPane.showInputDialog( + "# playouts for analysis (e.g. 100 (fast) or 50000 (slow)): "); + if (answer == null) return; + try { + playoutsAnalysis = Integer.parseInt(answer); + } catch (NumberFormatException err) { + System.out.println("Not a valid number"); + return; + } + Lizzie.leelaz.addListener(this); + analysisMode = true; + if (!Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); } - - public void toggleAnalysis() { - if (analysisMode) { - Lizzie.leelaz.removeListener(this); - analysisMode = false; - } else { - if (getNextMove() == null) return; - String answer = JOptionPane.showInputDialog("# playouts for analysis (e.g. 100 (fast) or 50000 (slow)): "); - if (answer == null) return; - try { - playoutsAnalysis = Integer.parseInt(answer); - } catch (NumberFormatException err) { - System.out.println("Not a valid number"); - return; - } - Lizzie.leelaz.addListener(this); - analysisMode = true; - if (!Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); + } + + public void bestMoveNotification(List bestMoves) { + if (analysisMode) { + boolean isSuccessivePass = + (history.getPrevious() != null + && history.getPrevious().lastMove == null + && getLastMove() == null); + // Note: We cannot replace this history.getNext() with getNextMove() + // because the latter returns null if the next move is "pass". + if (history.getNext() == null || isSuccessivePass) { + // Reached the end... + toggleAnalysis(); + } else if (bestMoves == null || bestMoves.size() == 0) { + // If we get empty list, something strange happened, ignore notification + } else { + // sum the playouts to proceed like leelaz's --visits option. + int sum = 0; + for (MoveData move : bestMoves) { + sum += move.playouts; } + if (sum >= playoutsAnalysis) { + nextMove(); + } + } + } + } + + public void autosave() { + if (autosaveToMemory()) { + try { + Lizzie.config.persist(); + } catch (IOException err) { + } } + } - public void bestMoveNotification(List bestMoves) { - if (analysisMode) { - boolean isSuccessivePass = (history.getPrevious() != null && - history.getPrevious().lastMove == null && - getLastMove() == null); - // Note: We cannot replace this history.getNext() with getNextMove() - // because the latter returns null if the next move is "pass". - if (history.getNext() == null || isSuccessivePass) { - // Reached the end... - toggleAnalysis(); - } else if (bestMoves == null || bestMoves.size() == 0) { - // If we get empty list, something strange happened, ignore notification - } else { - // sum the playouts to proceed like leelaz's --visits option. - int sum = 0; - for (MoveData move : bestMoves) { - sum += move.playouts; - } - if (sum >= playoutsAnalysis) { - nextMove(); - } - } - } + public boolean autosaveToMemory() { + try { + String sgf = SGFParser.saveToString(); + if (sgf.equals(Lizzie.config.persisted.getString("autosave"))) { + return false; + } + Lizzie.config.persisted.put("autosave", sgf); + } catch (Exception err) { // IOException or JSONException + return false; + } + return true; + } + + public void resumePreviousGame() { + try { + SGFParser.loadFromString(Lizzie.config.persisted.getString("autosave")); + while (nextMove()) ; + } catch (JSONException err) { } + } } diff --git a/src/main/java/featurecat/lizzie/rules/BoardData.java b/src/main/java/featurecat/lizzie/rules/BoardData.java index 1fce99366..9b266ceef 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardData.java +++ b/src/main/java/featurecat/lizzie/rules/BoardData.java @@ -1,37 +1,49 @@ package featurecat.lizzie.rules; public class BoardData { - public int moveNumber; - public int[] lastMove; - public int[] moveNumberList; - public boolean blackToPlay; - - public Stone lastMoveColor; - public Stone[] stones; - public Zobrist zobrist; - - public boolean verify; - - public double winrate; - public int playouts; - - public int blackCaptures; - public int whiteCaptures; - - public BoardData(Stone[] stones, int[] lastMove, Stone lastMoveColor, boolean blackToPlay, Zobrist zobrist, int moveNumber, int[] moveNumberList, int blackCaptures, int whiteCaptures, double winrate, int playouts) { - this.moveNumber = moveNumber; - this.lastMove = lastMove; - this.moveNumberList = moveNumberList; - this.blackToPlay = blackToPlay; - - this.lastMoveColor = lastMoveColor; - this.stones = stones; - this.zobrist = zobrist; - this.verify = false; - - this.winrate = winrate; - this.playouts = playouts; - this.blackCaptures = blackCaptures; - this.whiteCaptures = whiteCaptures; - } + public int moveNumber; + public int[] lastMove; + public int[] moveNumberList; + public boolean blackToPlay; + + public Stone lastMoveColor; + public Stone[] stones; + public Zobrist zobrist; + public boolean verify; + + public double winrate; + public int playouts; + public int blackCaptures; + public int whiteCaptures; + + // Comment in the Sgf move + public String comment; + + public BoardData( + Stone[] stones, + int[] lastMove, + Stone lastMoveColor, + boolean blackToPlay, + Zobrist zobrist, + int moveNumber, + int[] moveNumberList, + int blackCaptures, + int whiteCaptures, + double winrate, + int playouts) { + this.moveNumber = moveNumber; + this.lastMove = lastMove; + this.moveNumberList = moveNumberList; + this.blackToPlay = blackToPlay; + + this.lastMoveColor = lastMoveColor; + this.stones = stones; + this.zobrist = zobrist; + this.verify = false; + + this.winrate = winrate; + this.playouts = playouts; + this.blackCaptures = blackCaptures; + this.whiteCaptures = whiteCaptures; + } } diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index aaf6764d5..3d4027a16 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -1,341 +1,319 @@ package featurecat.lizzie.rules; -import java.util.List; - import featurecat.lizzie.analysis.GameInfo; +import java.util.List; -/** - * Linked list data structure to store board history - */ +/** Linked list data structure to store board history */ public class BoardHistoryList { - private GameInfo gameInfo; - private BoardHistoryNode head; - - /** - * Initialize a new board history list, whose first node is data - * - * @param data the data to be stored for the first entry - */ - public BoardHistoryList(BoardData data) { - head = new BoardHistoryNode(data); - gameInfo = new GameInfo(); - } - - public GameInfo getGameInfo() { - return gameInfo; - } - - /** - * Clear history. - */ - public void clear() { - head.clear(); - } - - /** - * Add new data after head. Overwrites any data that may have been stored after head. - * - * @param data the data to add - */ - public void add(BoardData data) { - head = head.add(new BoardHistoryNode(data)); - } - - public void addOrGoto(BoardData data) { - head = head.addOrGoto(data); - } - - /** - * moves the pointer to the left, returns the data stored there - * - * @return data of previous node, null if there is no previous node - */ - public BoardData previous() { - if (head.previous() == null) - return null; - else - head = head.previous(); - - return head.getData(); - } - - public void toStart() { - while (previous() != null); - } - - /** - * moves the pointer to the right, returns the data stored there - * - * @return the data of next node, null if there is no next node - */ - public BoardData next() { - if (head.next() == null) - return null; - else - head = head.next(); - - return head.getData(); - } - - /** - * moves the pointer to the variation number idx, returns the data stored there - * - * @return the data of next node, null if there is no variaton with index - */ - public BoardData nextVariation(int idx) { - if (head.getVariation(idx) == null) - return null; - else - head = head.getVariation(idx); - - return head.getData(); - } - - /** - * Does not change the pointer position - * - * @return the data stored at the next index. null if not present - */ - public BoardData getNext() { - if (head.next() == null) - return null; - else - return head.next().getData(); - } - - /** - * @return nexts for display - */ - public List getNexts() { - return head.getNexts(); - } - - /** - * Does not change the pointer position - * - * @return the data stored at the previous index. null if not present - */ - public BoardData getPrevious() { - if (head.previous() == null) - return null; - else - return head.previous().getData(); - } - - /** - * @return the data of the current node - */ - public BoardData getData() { - return head.getData(); - } - - public void setStone(int[] coordinates, Stone stone) { - int index = Board.getIndex(coordinates[0], coordinates[1]); - head.getData().stones[index] = stone; - head.getData().zobrist.toggleStone(coordinates[0], coordinates[1], stone); - } - - public Stone[] getStones() { - return head.getData().stones; - } - - public int[] getLastMove() { - return head.getData().lastMove; - } - - public int[] getNextMove() { - BoardData next = getNext(); - if (next == null) - return null; - else - return next.lastMove; - } - - public Stone getLastMoveColor() { - return head.getData().lastMoveColor; - } - - public boolean isBlacksTurn() { - return head.getData().blackToPlay; - } - - public Zobrist getZobrist() { - return head.getData().zobrist.clone(); - } - - public int getMoveNumber() { - return head.getData().moveNumber; - } - - public int[] getMoveNumberList() { - return head.getData().moveNumberList; - } - - public BoardHistoryNode getCurrentHistoryNode() { - return head; - } - - /** - * @param data the board position to check against superko - * @return whether or not the given position violates the superko rule at the head's state - */ - public boolean violatesSuperko(BoardData data) { - BoardHistoryNode head = this.head; - - // check to see if this position has occurred before - while (head.previous() != null) { - // if two zobrist hashes are equal, and it's the same player to coordinate, they are the same position - if (data.zobrist.equals(head.getData().zobrist) && data.blackToPlay == head.getData().blackToPlay) - return true; - - head = head.previous(); + private GameInfo gameInfo; + private BoardHistoryNode head; + + /** + * Initialize a new board history list, whose first node is data + * + * @param data the data to be stored for the first entry + */ + public BoardHistoryList(BoardData data) { + head = new BoardHistoryNode(data); + gameInfo = new GameInfo(); + } + + public GameInfo getGameInfo() { + return gameInfo; + } + + public void setGameInfo(GameInfo gameInfo) { + this.gameInfo = gameInfo; + } + + public BoardHistoryList shallowCopy() { + BoardHistoryList copy = new BoardHistoryList(null); + copy.head = head; + copy.gameInfo = gameInfo; + return copy; + } + + /** Clear history. */ + public void clear() { + head.clear(); + } + + /** + * Add new data after head. Overwrites any data that may have been stored after head. + * + * @param data the data to add + */ + public void add(BoardData data) { + head = head.add(new BoardHistoryNode(data)); + } + + public void addOrGoto(BoardData data) { + head = head.addOrGoto(data); + } + + /** + * moves the pointer to the left, returns the data stored there + * + * @return data of previous node, null if there is no previous node + */ + public BoardData previous() { + if (head.previous() == null) return null; + else head = head.previous(); + + return head.getData(); + } + + public void toStart() { + while (previous() != null) ; + } + + /** + * moves the pointer to the right, returns the data stored there + * + * @return the data of next node, null if there is no next node + */ + public BoardData next() { + if (head.next() == null) return null; + else head = head.next(); + + return head.getData(); + } + + /** + * moves the pointer to the variation number idx, returns the data stored there + * + * @return the data of next node, null if there is no variaton with index + */ + public BoardData nextVariation(int idx) { + if (head.getVariation(idx) == null) return null; + else head = head.getVariation(idx); + + return head.getData(); + } + + /** + * Does not change the pointer position + * + * @return the data stored at the next index. null if not present + */ + public BoardData getNext() { + if (head.next() == null) return null; + else return head.next().getData(); + } + + /** @return nexts for display */ + public List getNexts() { + return head.getNexts(); + } + + /** + * Does not change the pointer position + * + * @return the data stored at the previous index. null if not present + */ + public BoardData getPrevious() { + if (head.previous() == null) return null; + else return head.previous().getData(); + } + + /** @return the data of the current node */ + public BoardData getData() { + return head.getData(); + } + + public void setStone(int[] coordinates, Stone stone) { + int index = Board.getIndex(coordinates[0], coordinates[1]); + head.getData().stones[index] = stone; + head.getData().zobrist.toggleStone(coordinates[0], coordinates[1], stone); + } + + public Stone[] getStones() { + return head.getData().stones; + } + + public int[] getLastMove() { + return head.getData().lastMove; + } + + public int[] getNextMove() { + BoardData next = getNext(); + if (next == null) return null; + else return next.lastMove; + } + + public Stone getLastMoveColor() { + return head.getData().lastMoveColor; + } + + public boolean isBlacksTurn() { + return head.getData().blackToPlay; + } + + public Zobrist getZobrist() { + return head.getData().zobrist.clone(); + } + + public int getMoveNumber() { + return head.getData().moveNumber; + } + + public int[] getMoveNumberList() { + return head.getData().moveNumberList; + } + + public BoardHistoryNode getCurrentHistoryNode() { + return head; + } + + /** + * @param data the board position to check against superko + * @return whether or not the given position violates the superko rule at the head's state + */ + public boolean violatesSuperko(BoardData data) { + BoardHistoryNode head = this.head; + + // check to see if this position has occurred before + while (head.previous() != null) { + // if two zobrist hashes are equal, and it's the same player to coordinate, they are the same + // position + if (data.zobrist.equals(head.getData().zobrist) + && data.blackToPlay == head.getData().blackToPlay) return true; + + head = head.previous(); + } + + // no position matched this position, so it's valid + return false; + } + + /** + * Returns the root node + * + * @return root node + */ + public BoardHistoryNode root() { + BoardHistoryNode top = head; + while (top.previous() != null) { + top = top.previous(); + } + return top; + } + + /** + * Returns the length of current branch + * + * @return length of current branch + */ + public int currentBranchLength() { + return getMoveNumber() + BoardHistoryList.getDepth(head); + } + + /** + * Returns the length of main trunk + * + * @return length of main trunk + */ + public int mainTrunkLength() { + return BoardHistoryList.getDepth(root()); + } + + /* + * Static helper methods + */ + + /** + * Returns the number of moves in a tree (only the left-most (trunk) variation) + * + * @return number of moves in a tree + */ + public static int getDepth(BoardHistoryNode node) { + int c = 0; + while (node.next() != null) { + c++; + node = node.next(); + } + return c; + } + + /** + * Check if there is a branch that is at least depth deep (at least depth moves) + * + * @return true if it is deep enough, false otherwise + */ + public static boolean hasDepth(BoardHistoryNode node, int depth) { + int c = 0; + if (depth <= 0) return true; + while (node.next() != null) { + if (node.numberOfChildren() > 1) { + for (int i = 0; i < node.numberOfChildren(); i++) { + if (hasDepth(node.getVariation(i), depth - c - 1)) return true; } - - // no position matched this position, so it's valid return false; - } - - /** - * Returns the root node - * - * @return root node - */ - public BoardHistoryNode root() { - BoardHistoryNode top = head; - while (top.previous() != null) { - top = top.previous(); - } - return top; - } - - /** - * Returns the length of current branch - * - * @return length of current branch - */ - public int currentBranchLength() { - return getMoveNumber() + BoardHistoryList.getDepth(head); - } - - /** - * Returns the length of main trunk - * - * @return length of main trunk - */ - public int mainTrunkLength() { - return BoardHistoryList.getDepth(root()); - } - - /* - * Static helper methods - */ - - /** - * Returns the number of moves in a tree (only the left-most (trunk) variation) - * - * @return number of moves in a tree - */ - static public int getDepth(BoardHistoryNode node) - { - int c = 0; - while (node.next() != null) - { - c++; - node = node.next(); - } - return c; - } - - /** - * Check if there is a branch that is at least depth deep (at least depth moves) - * - * @return true if it is deep enough, false otherwise - */ - static public boolean hasDepth(BoardHistoryNode node, int depth) { - int c = 0; - if (depth <= 0) return true; - while (node.next() != null) { - if (node.numberOfChildren() > 1) { - for (int i = 0; i < node.numberOfChildren(); i++) { - if (hasDepth(node.getVariation(i), depth - c - 1)) - return true; - } - return false; - } else { - node = node.next(); - c++; - if (c >= depth) return true; - } - } + } else { + node = node.next(); + c++; + if (c >= depth) return true; + } + } + return false; + } + + /** + * Find top of variation (the first move that is on the main trunk) + * + * @return top of variaton, if on main trunk, return start move + */ + public static BoardHistoryNode findTop(BoardHistoryNode start) { + BoardHistoryNode top = start; + while (start.previous() != null) { + if (start.previous().next() != start) { + top = start.previous(); + } + start = start.previous(); + } + return top; + } + + /** + * Find first move with variations in tree above node + * + * @return The child (in the current variation) of the first node with variations + */ + public static BoardHistoryNode findChildOfPreviousWithVariation(BoardHistoryNode node) { + while (node.previous() != null) { + if (node.previous().numberOfChildren() > 1) { + return node; + } + node = node.previous(); + } + return null; + } + + /** + * Given a parent node and a child node, find the index of the child node + * + * @return index of child node, -1 if child node not a child of parent + */ + public static int findIndexOfNode(BoardHistoryNode parentNode, BoardHistoryNode childNode) { + if (parentNode.next() == null) return -1; + for (int i = 0; i < parentNode.numberOfChildren(); i++) { + if (parentNode.getVariation(i) == childNode) return i; + } + return -1; + } + + /** + * Check if node is part of the main trunk (rightmost branch) + * + * @return true if node is part of main trunk, false otherwise + */ + public static boolean isMainTrunk(BoardHistoryNode node) { + while (node.previous() != null) { + if (node.previous().next() != node) { return false; + } + node = node.previous(); } - - /** - * Find top of variation (the first move that is on the main trunk) - * - * @return top of variaton, if on main trunk, return start move - */ - static public BoardHistoryNode findTop(BoardHistoryNode start) - { - BoardHistoryNode top = start; - while (start.previous() != null) - { - if (start.previous().next() != start) - { - top = start.previous(); - } - start = start.previous(); - } - return top; - } - - /** - * Find first move with variations in tree above node - * - * @return The child (in the current variation) of the first node with variations - */ - static public BoardHistoryNode findChildOfPreviousWithVariation(BoardHistoryNode node) - { - while (node.previous() != null) - { - if (node.previous().numberOfChildren() > 1) - { - return node; - } - node = node.previous(); - } - return null; - } - - /** - * Given a parent node and a child node, find the index of the child node - * - * @return index of child node, -1 if child node not a child of parent - */ - static public int findIndexOfNode(BoardHistoryNode parentNode, BoardHistoryNode childNode) - { - if (parentNode.next() == null) return -1; - for (int i = 0; i < parentNode.numberOfChildren(); i++) - { - if (parentNode.getVariation(i) == childNode) return i; - } - return -1; - } - - /** - * Check if node is part of the main trunk (rightmost branch) - * - * @return true if node is part of main trunk, false otherwise - */ - static public boolean isMainTrunk(BoardHistoryNode node) - { - while (node.previous() != null) { - if (node.previous().next() != node) { - return false; - } - node = node.previous(); - } - return true; - } - + return true; + } } diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java index 95cf436f0..01ac257af 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java @@ -1,176 +1,183 @@ package featurecat.lizzie.rules; + import java.util.ArrayList; import java.util.List; -/** - * Node structure for the board history / sgf tree - */ +/** Node structure for the board history / sgf tree */ public class BoardHistoryNode { - private BoardHistoryNode previous; - private ArrayList nexts; - - private BoardData data; - - /** - * Initializes a new list node - */ - public BoardHistoryNode(BoardData data) { - previous = null; - nexts = new ArrayList(); - this.data = data; - } - - /** - * Remove all subsequent nodes. - */ - public void clear() { - nexts.clear(); - } - - /** - * Sets up for a new node. Overwrites future history. - * - * @param node the node following this one - * @return the node that was just set - */ - public BoardHistoryNode add(BoardHistoryNode node) { - nexts.clear(); - nexts.add(node); - node.previous = this; - - return node; - } - - /** - * If we already have a next node with the same BoardData, move to it, - * otherwise add it and move to it. - * - * @param data the node following this one - * @return the node that was just set - */ - public BoardHistoryNode addOrGoto(BoardData data) { - // If you play a hand and immediately return it, it is most likely that you have made a mistake. Ask whether to delete the previous node. -// if (!nexts.isEmpty() && !nexts.get(0).data.zobrist.equals(data.zobrist)) { -// // You may just mark this hand, so it's not necessarily wrong. Answer when the first query is wrong or it will not ask whether the move is wrong. -// if (!nexts.get(0).data.verify) { -// int ret = JOptionPane.showConfirmDialog(null, "Do you want undo?", "Undo", JOptionPane.OK_CANCEL_OPTION); -// if (ret == JOptionPane.OK_OPTION) { -// nexts.remove(0); -// } else { -// nexts.get(0).data.verify = true; -// } -// } -// } - for (int i = 0; i < nexts.size(); i++) { - if (nexts.get(i).data.zobrist.equals(data.zobrist)) { -// if (i != 0) { -// // Swap selected next to foremost -// BoardHistoryNode currentNext = nexts.get(i); -// nexts.set(i, nexts.get(0)); -// nexts.set(0, currentNext); -// } - return nexts.get(i); - } - } - BoardHistoryNode node = new BoardHistoryNode(data); - // Add node - nexts.add(node); - node.previous = this; - - return node; - } - - /** - * @return data stored on this node - */ - public BoardData getData() { - return data; - } - - /** - * @return nexts for display - */ - public List getNexts() { - return nexts; - } - - public BoardHistoryNode previous() { - return previous; - } - - public BoardHistoryNode next() { - if (nexts.size() == 0) { - return null; - } else { - return nexts.get(0); - } - } - - public BoardHistoryNode topOfBranch() { - BoardHistoryNode top = this; - while (top.previous != null && top.previous.nexts.size() == 1) { - top = top.previous; - } - return top; - } - - public int numberOfChildren() { - if (nexts == null) { - return 0; - } else { - return nexts.size(); - } - } - - public boolean isFirstChild() { - return (previous != null) && previous.next() == this; - } - - public BoardHistoryNode getVariation(int idx) { - if (nexts.size() <= idx) { - return null; - } else { - return nexts.get(idx); - } - } - - public void moveUp() { - if (previous != null) { - previous.moveChildUp(this); - } - } - - public void moveDown() { - if (previous != null) { - previous.moveChildDown(this); - } - } - - public void moveChildUp(BoardHistoryNode child) { - for (int i = 1; i < nexts.size(); i++) { - if (nexts.get(i).data.zobrist.equals(child.data.zobrist)) { - BoardHistoryNode tmp = nexts.get(i-1); - nexts.set(i-1, child); - nexts.set(i, tmp); - return; - } - } - } - - public void moveChildDown(BoardHistoryNode child) { - for (int i = 0; i < nexts.size() - 1; i++) { - if (nexts.get(i).data.zobrist.equals(child.data.zobrist)) { - BoardHistoryNode tmp = nexts.get(i+1); - nexts.set(i+1, child); - nexts.set(i, tmp); - return; - } - } - } - - public void deleteChild(int idx) { - if (idx < numberOfChildren()) { - nexts.remove(idx); - } - } + private BoardHistoryNode previous; + private ArrayList nexts; + + private BoardData data; + + // Save the children for restore to branch + private int fromBackChildren; + + /** Initializes a new list node */ + public BoardHistoryNode(BoardData data) { + previous = null; + nexts = new ArrayList(); + this.data = data; + } + + /** Remove all subsequent nodes. */ + public void clear() { + nexts.clear(); + } + + /** + * Sets up for a new node. Overwrites future history. + * + * @param node the node following this one + * @return the node that was just set + */ + public BoardHistoryNode add(BoardHistoryNode node) { + nexts.clear(); + nexts.add(node); + node.previous = this; + + return node; + } + + /** + * If we already have a next node with the same BoardData, move to it, otherwise add it and move + * to it. + * + * @param data the node following this one + * @return the node that was just set + */ + public BoardHistoryNode addOrGoto(BoardData data) { + // If you play a hand and immediately return it, it is most likely that you have made a mistake. + // Ask whether to delete the previous node. + // if (!nexts.isEmpty() && !nexts.get(0).data.zobrist.equals(data.zobrist)) { + // // You may just mark this hand, so it's not necessarily wrong. Answer when the + // first query is wrong or it will not ask whether the move is wrong. + // if (!nexts.get(0).data.verify) { + // int ret = JOptionPane.showConfirmDialog(null, "Do you want undo?", "Undo", + // JOptionPane.OK_CANCEL_OPTION); + // if (ret == JOptionPane.OK_OPTION) { + // nexts.remove(0); + // } else { + // nexts.get(0).data.verify = true; + // } + // } + // } + for (int i = 0; i < nexts.size(); i++) { + if (nexts.get(i).data.zobrist.equals(data.zobrist)) { + // if (i != 0) { + // // Swap selected next to foremost + // BoardHistoryNode currentNext = nexts.get(i); + // nexts.set(i, nexts.get(0)); + // nexts.set(0, currentNext); + // } + return nexts.get(i); + } + } + BoardHistoryNode node = new BoardHistoryNode(data); + // Add node + nexts.add(node); + node.previous = this; + + return node; + } + + /** @return data stored on this node */ + public BoardData getData() { + return data; + } + + /** @return nexts for display */ + public List getNexts() { + return nexts; + } + + public BoardHistoryNode previous() { + return previous; + } + + public BoardHistoryNode next() { + if (nexts.size() == 0) { + return null; + } else { + return nexts.get(0); + } + } + + public BoardHistoryNode topOfBranch() { + BoardHistoryNode top = this; + while (top.previous != null && top.previous.nexts.size() == 1) { + top = top.previous; + } + return top; + } + + public int numberOfChildren() { + if (nexts == null) { + return 0; + } else { + return nexts.size(); + } + } + + public boolean isFirstChild() { + return (previous != null) && previous.next() == this; + } + + public BoardHistoryNode getVariation(int idx) { + if (nexts.size() <= idx) { + return null; + } else { + return nexts.get(idx); + } + } + + public void moveUp() { + if (previous != null) { + previous.moveChildUp(this); + } + } + + public void moveDown() { + if (previous != null) { + previous.moveChildDown(this); + } + } + + public void moveChildUp(BoardHistoryNode child) { + for (int i = 1; i < nexts.size(); i++) { + if (nexts.get(i).data.zobrist.equals(child.data.zobrist)) { + BoardHistoryNode tmp = nexts.get(i - 1); + nexts.set(i - 1, child); + nexts.set(i, tmp); + return; + } + } + } + + public void moveChildDown(BoardHistoryNode child) { + for (int i = 0; i < nexts.size() - 1; i++) { + if (nexts.get(i).data.zobrist.equals(child.data.zobrist)) { + BoardHistoryNode tmp = nexts.get(i + 1); + nexts.set(i + 1, child); + nexts.set(i, tmp); + return; + } + } + } + + public void deleteChild(int idx) { + if (idx < numberOfChildren()) { + nexts.remove(idx); + } + } + + /** @param fromBackChildren the fromBackChildren to set */ + public void setFromBackChildren(int fromBackChildren) { + this.fromBackChildren = fromBackChildren; + } + + /** @return the fromBackChildren */ + public int getFromBackChildren() { + return fromBackChildren; + } } diff --git a/src/main/java/featurecat/lizzie/rules/GIBParser.java b/src/main/java/featurecat/lizzie/rules/GIBParser.java index 7961a5230..3f5e826ff 100644 --- a/src/main/java/featurecat/lizzie/rules/GIBParser.java +++ b/src/main/java/featurecat/lizzie/rules/GIBParser.java @@ -1,109 +1,106 @@ package featurecat.lizzie.rules; import featurecat.lizzie.Lizzie; -import featurecat.lizzie.plugin.PluginManager; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; public class GIBParser { - private static int[][] handicapPlacement = {{3, 15}, {15,3}, {15, 15}, {3, 3}, {3, 9}, - {15, 9}, {9, 3}, {9, 15}, {9, 9}}; - - public static boolean load(String filename) throws IOException { - // Clear the board - Lizzie.board.clear(); + private static int[][] handicapPlacement = { + {3, 15}, {15, 3}, {15, 15}, {3, 3}, {3, 9}, {15, 9}, {9, 3}, {9, 15}, {9, 9} + }; - File file = new File(filename); - if (!file.exists() || !file.canRead()) { - return false; - } + public static boolean load(String filename) throws IOException { + // Clear the board + Lizzie.board.clear(); - FileInputStream fp = new FileInputStream(file); - InputStreamReader reader = new InputStreamReader(fp); - StringBuilder builder = new StringBuilder(); - while (reader.ready()) { - builder.append((char) reader.read()); - } - reader.close(); - fp.close(); - String value = builder.toString(); - if (value.isEmpty()) { - return false; - } + File file = new File(filename); + if (!file.exists() || !file.canRead()) { + return false; + } - boolean returnValue = parse(value); - PluginManager.onSgfLoaded(); - return returnValue; + FileInputStream fp = new FileInputStream(file); + InputStreamReader reader = new InputStreamReader(fp); + StringBuilder builder = new StringBuilder(); + while (reader.ready()) { + builder.append((char) reader.read()); + } + reader.close(); + fp.close(); + String value = builder.toString(); + if (value.isEmpty()) { + return false; } - private static void placeHandicap(int handi) { - if (handi > 9) { - System.out.println("More than 9 in handicap not supported!"); - handi = 9; - } - if (handi == 5 || handi == 7) { - Lizzie.board.place(9, 9, Stone.BLACK); - handi--; - } - for (int i = 0; i < handi; i++) { - Lizzie.board.place(handicapPlacement[i][0], handicapPlacement[i][1], Stone.BLACK); - } + boolean returnValue = parse(value); + return returnValue; + } + + private static void placeHandicap(int handi) { + if (handi > 9) { + System.out.println("More than 9 in handicap not supported!"); + handi = 9; + } + if (handi == 5 || handi == 7) { + Lizzie.board.place(9, 9, Stone.BLACK); + handi--; } + for (int i = 0; i < handi; i++) { + Lizzie.board.place(handicapPlacement[i][0], handicapPlacement[i][1], Stone.BLACK); + } + } - private static boolean parse(String value) { - String[] lines = value.trim().split("\n"); - String whitePlayer = "Player 1"; - String blackPlayer = "Player 2"; - double komi = 1.5; - int handicap = 0; + private static boolean parse(String value) { + String[] lines = value.trim().split("\n"); + String whitePlayer = "Player 1"; + String blackPlayer = "Player 2"; + double komi = 1.5; + int handicap = 0; - for (String line: lines) { - if (line.startsWith("\\[GAMEINFOMAIN=")) { - // See if komi is included - int sk = line.indexOf("GONGJE:") + 7; - int ek = line.indexOf(',', sk); + for (String line : lines) { + if (line.startsWith("\\[GAMEINFOMAIN=")) { + // See if komi is included + int sk = line.indexOf("GONGJE:") + 7; + int ek = line.indexOf(',', sk); - if (sk > 0) { - komi = Integer.parseInt(line.substring(sk, ek))/10.0; - } - } - // Players names - if (line.startsWith("\\[GAMEBLACKNAME=")) { - blackPlayer = line.substring(16, line.length() - 3); - } - if (line.startsWith("\\[GAMEWHITENAME=")) { - whitePlayer = line.substring(16, line.length()-3); - } - // Handicap info - if (line.startsWith("INI")) { - String[] fields = line.split(" "); - handicap = Integer.parseInt(fields[3]); - if (handicap >= 2) { - placeHandicap(handicap); - } - } - // Actual moves - if (line.startsWith("STO")) { - String[] fields = line.split(" "); - int x = Integer.parseInt(fields[4]); - int y = Integer.parseInt(fields[5]); - Stone s = fields[3].equals("1") ? Stone.BLACK : Stone.WHITE; - Lizzie.board.place(x, y, s); - } - // Pass - if (line.startsWith("SKI")) { - Lizzie.board.pass(); - } + if (sk > 0) { + komi = Integer.parseInt(line.substring(sk, ek)) / 10.0; } - Lizzie.board.getHistory().getGameInfo().setKomi(komi); - Lizzie.frame.setPlayers(whitePlayer, blackPlayer); - // Rewind to game start - while (Lizzie.board.previousMove()) ; - - return false; + } + // Players names + if (line.startsWith("\\[GAMEBLACKNAME=")) { + blackPlayer = line.substring(16, line.length() - 3); + } + if (line.startsWith("\\[GAMEWHITENAME=")) { + whitePlayer = line.substring(16, line.length() - 3); + } + // Handicap info + if (line.startsWith("INI")) { + String[] fields = line.split(" "); + handicap = Integer.parseInt(fields[3]); + if (handicap >= 2) { + placeHandicap(handicap); + } + } + // Actual moves + if (line.startsWith("STO")) { + String[] fields = line.split(" "); + int x = Integer.parseInt(fields[4]); + int y = Integer.parseInt(fields[5]); + Stone s = fields[3].equals("1") ? Stone.BLACK : Stone.WHITE; + Lizzie.board.place(x, y, s); + } + // Pass + if (line.startsWith("SKI")) { + Lizzie.board.pass(); + } } + Lizzie.board.getHistory().getGameInfo().setKomi(komi); + Lizzie.frame.setPlayers(whitePlayer, blackPlayer); + // Rewind to game start + while (Lizzie.board.previousMove()) ; + return false; + } } diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index d496c23a3..bafb37173 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -1,307 +1,370 @@ package featurecat.lizzie.rules; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.GameInfo; -import featurecat.lizzie.plugin.PluginManager; - import java.io.*; import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class SGFParser { - private static final SimpleDateFormat SGF_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); - - public static boolean load(String filename) throws IOException { - // Clear the board - Lizzie.board.clear(); + private static final SimpleDateFormat SGF_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); - File file = new File(filename); - if (!file.exists() || !file.canRead()) { - return false; - } + public static boolean load(String filename) throws IOException { + // Clear the board + Lizzie.board.clear(); - FileInputStream fp = new FileInputStream(file); - InputStreamReader reader = new InputStreamReader(fp); - StringBuilder builder = new StringBuilder(); - while (reader.ready()) { - builder.append((char) reader.read()); - } - reader.close(); - fp.close(); - String value = builder.toString(); - if (value.isEmpty()) { - return false; - } + File file = new File(filename); + if (!file.exists() || !file.canRead()) { + return false; + } - boolean returnValue = parse(value); - PluginManager.onSgfLoaded(); - return returnValue; + FileInputStream fp = new FileInputStream(file); + InputStreamReader reader = new InputStreamReader(fp); + StringBuilder builder = new StringBuilder(); + while (reader.ready()) { + builder.append((char) reader.read()); + } + reader.close(); + fp.close(); + String value = builder.toString(); + if (value.isEmpty()) { + return false; } - public static boolean loadFromString(String sgfString) { - // Clear the board - Lizzie.board.clear(); + boolean returnValue = parse(value); + return returnValue; + } - return parse(sgfString); - } + public static boolean loadFromString(String sgfString) { + // Clear the board + Lizzie.board.clear(); - public static int[] convertSgfPosToCoord(String pos) { - if (pos.equals("tt") || pos.isEmpty()) - return null; - int[] ret = new int[2]; - ret[0] = (int) pos.charAt(0) - 'a'; - ret[1] = (int) pos.charAt(1) - 'a'; - return ret; - } + return parse(sgfString); + } - private static boolean parse(String value) { - // Drop anything outside "(;...)" - final Pattern SGF_PATTERN = Pattern.compile("(?s).*?(\\(\\s*;.*\\)).*?"); - Matcher sgfMatcher = SGF_PATTERN.matcher(value); - if (sgfMatcher.matches()) { - value = sgfMatcher.group(1); - } else { - return false; - } - int subTreeDepth = 0; - boolean inTag = false, isMultiGo = false, escaping = false; - String tag = null; - StringBuilder tagBuilder = new StringBuilder(); - StringBuilder tagContentBuilder = new StringBuilder(); - // MultiGo 's branch: (Main Branch (Main Branch) (Branch) ) - // Other 's branch: (Main Branch (Branch) Main Branch) - if (value.charAt(value.length() - 2) == ')') { - isMultiGo = true; - } + public static int[] convertSgfPosToCoord(String pos) { + if (pos.equals("tt") || pos.isEmpty()) return null; + int[] ret = new int[2]; + ret[0] = (int) pos.charAt(0) - 'a'; + ret[1] = (int) pos.charAt(1) - 'a'; + return ret; + } - String blackPlayer = "", whitePlayer = ""; + private static boolean parse(String value) { + // Drop anything outside "(;...)" + final Pattern SGF_PATTERN = Pattern.compile("(?s).*?(\\(\\s*;.*\\)).*?"); + Matcher sgfMatcher = SGF_PATTERN.matcher(value); + if (sgfMatcher.matches()) { + value = sgfMatcher.group(1); + } else { + return false; + } + int subTreeDepth = 0; + // Save the variation step count + Map subTreeStepMap = new HashMap(); + // Comment of the AW/AB (Add White/Add Black) stone + String awabComment = null; + // Previous Tag + String prevTag = null; + boolean inTag = false, isMultiGo = false, escaping = false; + String tag = null; + StringBuilder tagBuilder = new StringBuilder(); + StringBuilder tagContentBuilder = new StringBuilder(); + // MultiGo 's branch: (Main Branch (Main Branch) (Branch) ) + // Other 's branch: (Main Branch (Branch) Main Branch) + if (value.matches("(?s).*\\)\\s*\\)")) { + isMultiGo = true; + } - PARSE_LOOP: - for (byte b : value.getBytes()) { - // Check unicode charactors (UTF-8) - char c = (char) b; - if (((int) b & 0x80) != 0) { - continue; + String blackPlayer = "", whitePlayer = ""; + + // Support unicode characters (UTF-8) + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (escaping) { + // Any char following "\" is inserted verbatim + // (ref) "3.2. Text" in https://www.red-bean.com/sgf/sgf4.html + tagContentBuilder.append(c); + escaping = false; + continue; + } + switch (c) { + case '(': + if (!inTag) { + subTreeDepth += 1; + // Initialize the step count + subTreeStepMap.put(subTreeDepth, 0); + } else { + if (i > 0) { + // Allow the comment tag includes '(' + tagContentBuilder.append(c); } - if (escaping) { - // Any char following "\" is inserted verbatim - // (ref) "3.2. Text" in https://www.red-bean.com/sgf/sgf4.html - tagContentBuilder.append(c); - escaping = false; - continue; + } + break; + case ')': + if (!inTag) { + if (isMultiGo) { + // Restore to the variation node + int varStep = subTreeStepMap.get(subTreeDepth); + for (int s = 0; s < varStep; s++) { + Lizzie.board.previousMove(); + } } - switch (c) { - case '(': - if (!inTag) { - subTreeDepth += 1; - } - break; - case ')': - if (!inTag) { - subTreeDepth -= 1; - if (isMultiGo) { - break PARSE_LOOP; - } - } - break; - case '[': - if (subTreeDepth > 1 && !isMultiGo) { - break; - } - inTag = true; - String tagTemp = tagBuilder.toString(); - if (!tagTemp.isEmpty()) { - // Ignore small letters in tags for the long format Smart-Go file. - // (ex) "PlayerBlack" ==> "PB" - // It is the default format of mgt, an old SGF tool. - // (Mgt is still supported in Debian and Ubuntu.) - tag = tagTemp.replaceAll("[a-z]", ""); - } - tagContentBuilder = new StringBuilder(); - break; - case ']': - if (subTreeDepth > 1 && !isMultiGo) { - break; - } - inTag = false; - tagBuilder = new StringBuilder(); - String tagContent = tagContentBuilder.toString(); - // We got tag, we can parse this tag now. - if (tag.equals("B")) { - int[] move = convertSgfPosToCoord(tagContent); - if (move == null) { - Lizzie.board.pass(Stone.BLACK); - } else { - Lizzie.board.place(move[0], move[1], Stone.BLACK); - } - } else if (tag.equals("W")) { - int[] move = convertSgfPosToCoord(tagContent); - if (move == null) { - Lizzie.board.pass(Stone.WHITE); - } else { - Lizzie.board.place(move[0], move[1], Stone.WHITE); - } - } else if (tag.equals("AB")) { - int[] move = convertSgfPosToCoord(tagContent); - if (move == null) { - Lizzie.board.pass(Stone.BLACK); - } else { - Lizzie.board.place(move[0], move[1], Stone.BLACK); - } - Lizzie.board.flatten(); - } else if (tag.equals("AW")) { - int[] move = convertSgfPosToCoord(tagContent); - if (move == null) { - Lizzie.board.pass(Stone.WHITE); - } else { - Lizzie.board.place(move[0], move[1], Stone.WHITE); - } - Lizzie.board.flatten(); - } else if (tag.equals("PB")) { - blackPlayer = tagContent; - } else if (tag.equals("PW")) { - whitePlayer = tagContent; - } else if (tag.equals("KM")) { - Lizzie.board.getHistory().getGameInfo().setKomi(Double.parseDouble(tagContent)); - } - break; - case ';': - break; - default: - if (subTreeDepth > 1 && !isMultiGo) { - break; - } - if (inTag) { - if (c == '\\') { - escaping = true; - continue; - } - tagContentBuilder.append(c); - } else { - if (c != '\n' && c != '\r' && c != '\t' && c != ' ') { - tagBuilder.append(c); - } - } + subTreeDepth -= 1; + } else { + // Allow the comment tag includes '(' + tagContentBuilder.append(c); + } + break; + case '[': + if (subTreeDepth > 1 && !isMultiGo) { + break; + } + inTag = true; + String tagTemp = tagBuilder.toString(); + if (!tagTemp.isEmpty()) { + // Ignore small letters in tags for the long format Smart-Go file. + // (ex) "PlayerBlack" ==> "PB" + // It is the default format of mgt, an old SGF tool. + // (Mgt is still supported in Debian and Ubuntu.) + tag = tagTemp.replaceAll("[a-z]", ""); + } + tagContentBuilder = new StringBuilder(); + break; + case ']': + if (subTreeDepth > 1 && !isMultiGo) { + break; + } + inTag = false; + tagBuilder = new StringBuilder(); + String tagContent = tagContentBuilder.toString(); + // We got tag, we can parse this tag now. + if (tag.equals("B")) { + int[] move = convertSgfPosToCoord(tagContent); + if (move == null) { + Lizzie.board.pass(Stone.BLACK); + } else { + // Save the step count + subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1); + Lizzie.board.place(move[0], move[1], Stone.BLACK); } - } + } else if (tag.equals("W")) { + int[] move = convertSgfPosToCoord(tagContent); + if (move == null) { + Lizzie.board.pass(Stone.WHITE); + } else { + // Save the step count + subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1); + Lizzie.board.place(move[0], move[1], Stone.WHITE); + } + } else if (tag.equals("C")) { + // Support comment + if ("AW".equals(prevTag) || "AB".equals(prevTag)) { + awabComment = tagContent; + } else { + Lizzie.board.comment(tagContent); + } + } else if (tag.equals("AB")) { + int[] move = convertSgfPosToCoord(tagContent); + if (move == null) { + Lizzie.board.pass(Stone.BLACK); + } else { + Lizzie.board.place(move[0], move[1], Stone.BLACK); + } + Lizzie.board.flatten(); + } else if (tag.equals("AW")) { + int[] move = convertSgfPosToCoord(tagContent); + if (move == null) { + Lizzie.board.pass(Stone.WHITE); + } else { + Lizzie.board.place(move[0], move[1], Stone.WHITE); + } + Lizzie.board.flatten(); + } else if (tag.equals("PB")) { + blackPlayer = tagContent; + } else if (tag.equals("PW")) { + whitePlayer = tagContent; + } else if (tag.equals("KM")) { + try { + if (tagContent.trim().isEmpty()) { + tagContent = "0.0"; + } + Lizzie.board.getHistory().getGameInfo().setKomi(Double.parseDouble(tagContent)); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + prevTag = tag; + break; + case ';': + break; + default: + if (subTreeDepth > 1 && !isMultiGo) { + break; + } + if (inTag) { + if (c == '\\') { + escaping = true; + continue; + } + tagContentBuilder.append(c); + } else { + if (c != '\n' && c != '\r' && c != '\t' && c != ' ') { + tagBuilder.append(c); + } + } + } + } - Lizzie.frame.setPlayers(whitePlayer, blackPlayer); + Lizzie.frame.setPlayers(whitePlayer, blackPlayer); - // Rewind to game start - while (Lizzie.board.previousMove()) ; + // Rewind to game start + while (Lizzie.board.previousMove()) ; - return true; + // Set AW/AB Comment + if (awabComment != null) { + Lizzie.board.comment(awabComment); } - public static String saveToString() throws IOException { - try (StringWriter writer = new StringWriter()) { - saveToStream(Lizzie.board, writer); - return writer.toString(); - } + return true; + } + + public static String saveToString() throws IOException { + try (StringWriter writer = new StringWriter()) { + saveToStream(Lizzie.board, writer); + return writer.toString(); } + } - public static void save(Board board, String filename) throws IOException { - try (Writer writer = new OutputStreamWriter(new FileOutputStream(filename))) { - saveToStream(board, writer); - } + public static void save(Board board, String filename) throws IOException { + try (Writer writer = new OutputStreamWriter(new FileOutputStream(filename))) { + saveToStream(board, writer); } + } - private static void saveToStream(Board board, Writer writer) throws IOException { - // collect game info - BoardHistoryList history = board.getHistory(); - GameInfo gameInfo = history.getGameInfo(); - String playerBlack = gameInfo.getPlayerBlack(); - String playerWhite = gameInfo.getPlayerWhite(); - Double komi = gameInfo.getKomi(); - Integer handicap = gameInfo.getHandicap(); - String date = SGF_DATE_FORMAT.format(gameInfo.getDate()); - - // add SGF header - StringBuilder builder = new StringBuilder("(;"); - if (handicap != 0) builder.append(String.format("HA[%s]", handicap)); - builder.append(String.format("KM[%s]PW[%s]PB[%s]DT[%s]AP[Lizzie: %s]", - komi, playerWhite, playerBlack, date, Lizzie.lizzieVersion)); - - // move to the first move - history.toStart(); - - // add handicap stones to SGF - if (handicap != 0) { - builder.append("AB"); - Stone[] stones = history.getStones(); - for (int i = 0; i < stones.length; i++) { - Stone stone = stones[i]; - if (stone.isBlack()) { - // i = x * Board.BOARD_SIZE + y; - int corY = i % Board.BOARD_SIZE; - int corX = (i - corY) / Board.BOARD_SIZE; - - char x = (char) (corX + 'a'); - char y = (char) (corY + 'a'); - builder.append(String.format("[%c%c]", x, y)); - } - } + private static void saveToStream(Board board, Writer writer) throws IOException { + // collect game info + BoardHistoryList history = board.getHistory().shallowCopy(); + GameInfo gameInfo = history.getGameInfo(); + String playerB = gameInfo.getPlayerBlack(); + String playerW = gameInfo.getPlayerWhite(); + Double komi = gameInfo.getKomi(); + Integer handicap = gameInfo.getHandicap(); + String date = SGF_DATE_FORMAT.format(gameInfo.getDate()); + + // add SGF header + StringBuilder builder = new StringBuilder("(;"); + if (handicap != 0) builder.append(String.format("HA[%s]", handicap)); + String header = "KM[%s]PW[%s]PB[%s]DT[%s]AP[Lizzie: %s]"; + builder.append(String.format(header, komi, playerW, playerB, date, Lizzie.lizzieVersion)); + + // move to the first move + history.toStart(); + + // add handicap stones to SGF + if (handicap != 0) { + builder.append("AB"); + Stone[] stones = history.getStones(); + for (int i = 0; i < stones.length; i++) { + Stone stone = stones[i]; + if (stone.isBlack()) { + // i = x * Board.BOARD_SIZE + y; + int corY = i % Board.BOARD_SIZE; + int corX = (i - corY) / Board.BOARD_SIZE; + + char x = (char) (corX + 'a'); + char y = (char) (corY + 'a'); + builder.append(String.format("[%c%c]", x, y)); + } + } + } else { + // Process the AW/AB stone + Stone[] stones = history.getStones(); + StringBuilder abStone = new StringBuilder(); + StringBuilder awStone = new StringBuilder(); + for (int i = 0; i < stones.length; i++) { + Stone stone = stones[i]; + if (stone.isBlack() || stone.isWhite()) { + // i = x * Board.BOARD_SIZE + y; + int corY = i % Board.BOARD_SIZE; + int corX = (i - corY) / Board.BOARD_SIZE; + + char x = (char) (corX + 'a'); + char y = (char) (corY + 'a'); + + if (stone.isBlack()) { + abStone.append(String.format("[%c%c]", x, y)); + } else { + awStone.append(String.format("[%c%c]", x, y)); + } } + } + if (abStone.length() > 0) { + builder.append("AB").append(abStone); + } + if (awStone.length() > 0) { + builder.append("AW").append(awStone); + } + } - // replay moves, and convert them to tags. - // * format: ";B[xy]" or ";W[xy]" - // * with 'xy' = coordinates ; or 'tt' for pass. - BoardData data; - - // TODO: this code comes from cngoodboy's plugin PR #65. It looks like it might be useful for handling - // AB/AW commands for sgfs in general -- we can extend it beyond just handicap. TODO integrate it -// data = history.getData(); -// -// // For handicap -// ArrayList abList = new ArrayList(); -// ArrayList awList = new ArrayList(); -// -// for (int i = 0; i < Board.BOARD_SIZE; i++) { -// for (int j = 0; j < Board.BOARD_SIZE; j++) { -// switch (data.stones[Board.getIndex(i, j)]) { -// case BLACK: -// abList.add(new int[]{i, j}); -// break; -// case WHITE: -// awList.add(new int[]{i, j}); -// break; -// default: -// break; -// } -// } -// } -// -// if (!abList.isEmpty()) { -// builder.append(";AB"); -// for (int i = 0; i < abList.size(); i++) { -// builder.append(String.format("[%s]", convertCoordToSgfPos(abList.get(i)))); -// } -// } -// -// if (!awList.isEmpty()) { -// builder.append(";AW"); -// for (int i = 0; i < awList.size(); i++) { -// builder.append(String.format("[%s]", convertCoordToSgfPos(awList.get(i)))); -// } -// } - - while ((data = history.next()) != null) { - - String stone; - if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; - else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; - else continue; - - char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); - char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); - - builder.append(String.format(";%s[%c%c]", stone, x, y)); + // The AW/AB Comment + if (history.getData().comment != null) { + builder.append(String.format("C[%s]", history.getData().comment)); + } + + // replay moves, and convert them to tags. + // * format: ";B[xy]" or ";W[xy]" + // * with 'xy' = coordinates ; or 'tt' for pass. + + // Write variation tree + builder.append(generateNode(board, history.getCurrentHistoryNode())); + + // close file + builder.append(')'); + writer.append(builder.toString()); + } + + /** Generate node with variations */ + private static String generateNode(Board board, BoardHistoryNode node) throws IOException { + StringBuilder builder = new StringBuilder(""); + + if (node != null) { + + BoardData data = node.getData(); + String stone = ""; + if (Stone.BLACK.equals(data.lastMoveColor) || Stone.WHITE.equals(data.lastMoveColor)) { + + if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; + else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; + + char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); + char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + + builder.append(String.format(";%s[%c%c]", stone, x, y)); + + // Write the comment + if (data.comment != null) { + builder.append(String.format("C[%s]", data.comment)); } + } - // close file - builder.append(')'); - writer.append(builder.toString()); + if (node.numberOfChildren() > 1) { + // Variation + for (BoardHistoryNode sub : node.getNexts()) { + builder.append("("); + builder.append(generateNode(board, sub)); + builder.append(")"); + } + } else if (node.numberOfChildren() == 1) { + builder.append(generateNode(board, node.next())); + } else { + return builder.toString(); + } } + + return builder.toString(); + } } diff --git a/src/main/java/featurecat/lizzie/rules/Stone.java b/src/main/java/featurecat/lizzie/rules/Stone.java index 585fd6c10..46e49d7fe 100644 --- a/src/main/java/featurecat/lizzie/rules/Stone.java +++ b/src/main/java/featurecat/lizzie/rules/Stone.java @@ -1,80 +1,87 @@ package featurecat.lizzie.rules; public enum Stone { - BLACK, WHITE, EMPTY, BLACK_RECURSED, WHITE_RECURSED, BLACK_GHOST, WHITE_GHOST, DAME, BLACK_POINT, WHITE_POINT, BLACK_CAPTURED, WHITE_CAPTURED; + BLACK, + WHITE, + EMPTY, + BLACK_RECURSED, + WHITE_RECURSED, + BLACK_GHOST, + WHITE_GHOST, + DAME, + BLACK_POINT, + WHITE_POINT, + BLACK_CAPTURED, + WHITE_CAPTURED; - /** - * used to find the opposite color stone - * - * @return the opposite stone type - */ - public Stone opposite() { - switch (this) { - case BLACK: - return WHITE; - case WHITE: - return BLACK; - default: - return this; - } + /** + * used to find the opposite color stone + * + * @return the opposite stone type + */ + public Stone opposite() { + switch (this) { + case BLACK: + return WHITE; + case WHITE: + return BLACK; + default: + return this; } + } - /** - * used to keep track of which stones were visited during removal of dead stones - * - * @return the recursed version of this stone color - */ - public Stone recursed() { - switch (this) { - case BLACK: - return BLACK_RECURSED; - case WHITE: - return WHITE_RECURSED; - default: - return this; - } + /** + * used to keep track of which stones were visited during removal of dead stones + * + * @return the recursed version of this stone color + */ + public Stone recursed() { + switch (this) { + case BLACK: + return BLACK_RECURSED; + case WHITE: + return WHITE_RECURSED; + default: + return this; } + } - /** - * used to keep track of which stones were visited during removal of dead stones - * - * @return the unrecursed version of this stone color - */ - public Stone unrecursed() { - switch (this) { - case BLACK_RECURSED: - return BLACK; - case WHITE_RECURSED: - return WHITE; - default: - return this; - } + /** + * used to keep track of which stones were visited during removal of dead stones + * + * @return the unrecursed version of this stone color + */ + public Stone unrecursed() { + switch (this) { + case BLACK_RECURSED: + return BLACK; + case WHITE_RECURSED: + return WHITE; + default: + return this; } + } - /** - * @return Whether or not this stone is of the black variants. - */ - public boolean isBlack() { - return this == BLACK || this == BLACK_RECURSED || this == BLACK_GHOST; - } + /** @return Whether or not this stone is of the black variants. */ + public boolean isBlack() { + return this == BLACK || this == BLACK_RECURSED || this == BLACK_GHOST; + } - /** - * @return Whether or not this stone is of the white variants. - */ - public boolean isWhite() { - return this != EMPTY && !this.isBlack(); - } + /** @return Whether or not this stone is of the white variants. */ + public boolean isWhite() { + return this != EMPTY && !this.isBlack(); + } - public Stone unGhosted() { - switch (this) { - case BLACK: - case BLACK_GHOST: - return BLACK; - case WHITE: - case WHITE_GHOST: - return WHITE; - default: - return EMPTY; - } + public Stone unGhosted() { + switch (this) { + case BLACK: + case BLACK_GHOST: + return BLACK; + case WHITE: + case WHITE_GHOST: + return WHITE; + default: + return EMPTY; } + } } diff --git a/src/main/java/featurecat/lizzie/rules/Zobrist.java b/src/main/java/featurecat/lizzie/rules/Zobrist.java index 781df4ca5..508755847 100644 --- a/src/main/java/featurecat/lizzie/rules/Zobrist.java +++ b/src/main/java/featurecat/lizzie/rules/Zobrist.java @@ -2,73 +2,69 @@ import java.util.Random; -/** - * Used to maintain zobrist hashes for ko detection - */ +/** Used to maintain zobrist hashes for ko detection */ public class Zobrist { - private static final long[] blackZobrist, whiteZobrist; + private static final long[] blackZobrist, whiteZobrist; - // initialize zobrist hashing - static { - Random random = new Random(); - blackZobrist = new long[Board.BOARD_SIZE * Board.BOARD_SIZE]; - whiteZobrist = new long[Board.BOARD_SIZE * Board.BOARD_SIZE]; + // initialize zobrist hashing + static { + Random random = new Random(); + blackZobrist = new long[Board.BOARD_SIZE * Board.BOARD_SIZE]; + whiteZobrist = new long[Board.BOARD_SIZE * Board.BOARD_SIZE]; - for (int i = 0; i < blackZobrist.length; i++) { - blackZobrist[i] = random.nextLong(); - whiteZobrist[i] = random.nextLong(); - } + for (int i = 0; i < blackZobrist.length; i++) { + blackZobrist[i] = random.nextLong(); + whiteZobrist[i] = random.nextLong(); } + } - // hash to be used to compare two board states - private long zhash; + // hash to be used to compare two board states + private long zhash; - public Zobrist() { - zhash = 0; - } + public Zobrist() { + zhash = 0; + } - public Zobrist(long zhash) { - this.zhash = zhash; - } + public Zobrist(long zhash) { + this.zhash = zhash; + } - /** - * @return a copy of this zobrist - */ - public Zobrist clone() { - return new Zobrist(zhash); - } + /** @return a copy of this zobrist */ + public Zobrist clone() { + return new Zobrist(zhash); + } - /** - * Call this method to alter the current zobrist hash for this stone - * - * @param x x coordinate -- must be valid - * @param y y coordinate -- must be valid - * @param color color of the stone to alter (for adding or removing a stone color) - */ - public void toggleStone(int x, int y, Stone color) { - switch (color) { - case BLACK: - zhash ^= blackZobrist[Board.getIndex(x, y)]; - break; - case WHITE: - zhash ^= whiteZobrist[Board.getIndex(x, y)]; - break; - default: - } + /** + * Call this method to alter the current zobrist hash for this stone + * + * @param x x coordinate -- must be valid + * @param y y coordinate -- must be valid + * @param color color of the stone to alter (for adding or removing a stone color) + */ + public void toggleStone(int x, int y, Stone color) { + switch (color) { + case BLACK: + zhash ^= blackZobrist[Board.getIndex(x, y)]; + break; + case WHITE: + zhash ^= whiteZobrist[Board.getIndex(x, y)]; + break; + default: } + } - @Override - public boolean equals(Object o) { - return o instanceof Zobrist && (((Zobrist) o).zhash == zhash); - } + @Override + public boolean equals(Object o) { + return o instanceof Zobrist && (((Zobrist) o).zhash == zhash); + } - @Override - public int hashCode() { - return (int) zhash; - } + @Override + public int hashCode() { + return (int) zhash; + } - @Override - public String toString() { - return "" + zhash; - } + @Override + public String toString() { + return "" + zhash; + } } diff --git a/src/main/java/featurecat/lizzie/theme/DefaultTheme.java b/src/main/java/featurecat/lizzie/theme/DefaultTheme.java deleted file mode 100644 index f70bc1fa7..000000000 --- a/src/main/java/featurecat/lizzie/theme/DefaultTheme.java +++ /dev/null @@ -1,66 +0,0 @@ -package featurecat.lizzie.theme; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -import javax.imageio.ImageIO; - - -/** - * DefaultTheme - */ -public class DefaultTheme implements ITheme { - BufferedImage blackStoneCached = null; - BufferedImage whiteStoneCached = null; - BufferedImage boardCached = null; - BufferedImage backgroundCached = null; - - @Override - public BufferedImage getBlackStone(int[] position) { - if (blackStoneCached == null) { - try { - blackStoneCached = ImageIO.read(new File("assets/black0.png")); - } catch (IOException e) { - e.printStackTrace(); - } - } - return blackStoneCached; - } - - @Override - public BufferedImage getWhiteStone(int[] position) { - if (whiteStoneCached == null) { - try { - whiteStoneCached = ImageIO.read(new File("assets/white0.png")); - } catch (IOException e) { - e.printStackTrace(); - } - } - return whiteStoneCached; - } - - @Override - public BufferedImage getBoard() { - if (boardCached == null) { - try { - boardCached = ImageIO.read(new File("assets/board.png")); - } catch (IOException e) { - e.printStackTrace(); - } - } - return boardCached; - } - - @Override - public BufferedImage getBackground() { - if (backgroundCached == null) { - try { - backgroundCached = ImageIO.read(new File("assets/background.jpg")); - } catch (IOException e) { - e.printStackTrace(); - } - } - return backgroundCached; - } -} \ No newline at end of file diff --git a/src/main/java/featurecat/lizzie/theme/ITheme.java b/src/main/java/featurecat/lizzie/theme/ITheme.java deleted file mode 100644 index e820a4e44..000000000 --- a/src/main/java/featurecat/lizzie/theme/ITheme.java +++ /dev/null @@ -1,51 +0,0 @@ -package featurecat.lizzie.theme; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; - -/** - * ITheme - */ -public interface ITheme { - static ITheme loadTheme(String name) { - ITheme ret = _loadTheme(name); - if (ret == null) { - return new DefaultTheme(); - } - return ret; - } - - static ITheme _loadTheme(String name) { - try { - File themes = new File("theme"); - if (!themes.isDirectory()) { - return null; - } - ArrayList jarFileList = new ArrayList(); - for (File file : themes.listFiles()) { - if (file.canRead() && file.getName().endsWith(".jar")) { - jarFileList.add(file.toURI().toURL()); - } - } - URLClassLoader loader = new URLClassLoader(jarFileList.toArray(new URL[jarFileList.size()])); - Class theme = loader.loadClass(name); - loader.close(); - ITheme ret = (ITheme) theme.newInstance(); - return ret; - } catch (Exception e) { - return new DefaultTheme(); - } - } - - // Considering that the theme may implement different pieces for each coordinate, you need to pass in the coordinates. - BufferedImage getBlackStone(int[] position); - - BufferedImage getWhiteStone(int[] position); - - BufferedImage getBoard(); - - BufferedImage getBackground(); -} diff --git a/assets/background.jpg b/src/main/resources/assets/background.jpg similarity index 100% rename from assets/background.jpg rename to src/main/resources/assets/background.jpg diff --git a/assets/black0.png b/src/main/resources/assets/black0.png similarity index 100% rename from assets/black0.png rename to src/main/resources/assets/black0.png diff --git a/assets/board.png b/src/main/resources/assets/board.png similarity index 100% rename from assets/board.png rename to src/main/resources/assets/board.png diff --git a/assets/white0.png b/src/main/resources/assets/white0.png similarity index 100% rename from assets/white0.png rename to src/main/resources/assets/white0.png diff --git a/src/main/resources/l10n/DisplayStrings.properties b/src/main/resources/l10n/DisplayStrings.properties index ca7686f4a..be3e5dce7 100644 --- a/src/main/resources/l10n/DisplayStrings.properties +++ b/src/main/resources/l10n/DisplayStrings.properties @@ -19,11 +19,13 @@ LizzieFrame.commands.keyAltC=ctrl-c|copy SGF to clipboard LizzieFrame.commands.keyAltV=ctrl-v|paste SGF from clipboard LizzieFrame.commands.keyC=c|toggle coordinates LizzieFrame.commands.keyControl=ctrl|undo/redo 10 moves +LizzieFrame.commands.keyD=d|show/hide dynamic komi LizzieFrame.commands.keyDownArrow=down arrow|redo LizzieFrame.commands.keyEnd=end|go to end LizzieFrame.commands.keyEnter=enter|force Leela Zero move LizzieFrame.commands.keyF=f|toggle next move display LizzieFrame.commands.keyG=g|toggle variation graph +LizzieFrame.commands.keyT=t|toggle comment display LizzieFrame.commands.keyHome=home|go to start LizzieFrame.commands.keyI=i|edit game info LizzieFrame.commands.keyA=a|run automatic analysis of game @@ -39,13 +41,17 @@ LizzieFrame.commands.keyW=w|toggle winrate display LizzieFrame.commands.keyPeriod=.|score game LizzieFrame.commands.mouseWheelScroll=scrollwheel|undo/redo LizzieFrame.commands.rightClick=right click|undo -LizzieFrame.prompt.failedToOpenSgf=Failed to open the SGF file. -LizzieFrame.prompt.failedToSaveSgf=Failed to save the SGF file. +LizzieFrame.prompt.failedToOpenFile=Failed to open file. +LizzieFrame.prompt.failedToSaveFile=Failed to save file. LizzieFrame.prompt.sgfExists=The SGF file already exists, do you want to replace it? LizzieFrame.prompt.showControlsHint=hold x = view controls +LizzieFrame.prompt.switching=switching... LizzieFrame.display.lastMove=Last move LizzieFrame.display.pondering=Pondering LizzieFrame.display.on=on LizzieFrame.display.off=off LizzieFrame.display.loading=Leela Zero is loading... -LizzieFrame.display.download-latest-network-prompt=Download the latest network file? This may take some time. \ No newline at end of file +LizzieFrame.display.download-latest-network-prompt=Download the latest network file? This may take some time. +LizzieFrame.display.leelaz-missing=Did not find Leela Zero, update config.txt or download from Leela Zero homepage +LizzieFrame.display.network-missing=Did not find network weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage +LizzieFrame.display.dynamic-komi=dyn. komi: diff --git a/src/main/resources/l10n/DisplayStrings_RO.properties b/src/main/resources/l10n/DisplayStrings_RO.properties new file mode 100644 index 000000000..14c54dce5 --- /dev/null +++ b/src/main/resources/l10n/DisplayStrings_RO.properties @@ -0,0 +1,53 @@ +# Note for localization +# +# The localization file set(resource bundle) consist of one base file(this file) and several translated property files. +# Each localized string is assigned a unique key. All keys referenced in codes should be present in the base file. +# If a key is present in the translated file matching the current system locale, the value in the translated +# file will be used. Otherwise the value in the base file will be used instead. +# Usually strings in the base file are written in English. So that if a string has no translations, it will be +# displayed in English by default. +# +# In Java 8 or before, the encoding of the resource bundle files has no standards. +# To avoid encoding inconsistencies, it is recommended to use "native2ascii" tool in JDK +# to encode native translations in raw unicode codes, such as \u0001 \u0002 if the native strings +# cannot be encoded in ascii. Typically east Asian translations, such as Chinese(both simplified and traditional), +# Korean or Japanese, need the encoding process. +# +# You can directly use the original display texts as the key, but it is recommended to name the key properly. + +LizzieFrame.commands.keyAltC=control-c|copiere SGF în memoria temporară +LizzieFrame.commands.keyAltV=control-v|lipire SGF din memoria temporară +LizzieFrame.commands.keyC=c|afișează/ascunde coordonate +LizzieFrame.commands.keyControl=control|înainte/înapoi 10 mutări +LizzieFrame.commands.keyDownArrow=săgeată jos|înainte +LizzieFrame.commands.keyEnd=end|mergi la sfârșit +LizzieFrame.commands.keyEnter=enter|forțează mutarea lui Leela Zero +LizzieFrame.commands.keyF=f|afișează/ascunde următoarea mutare +LizzieFrame.commands.keyG=g|afișează/ascunde graficul cu variante +LizzieFrame.commands.keyT=t|afișează/ascunde comentariul +LizzieFrame.commands.keyHome=home|mergi la început +LizzieFrame.commands.keyI=i|editează informațiile jocului +LizzieFrame.commands.keyA=a|rulează analiza automată a jocului +LizzieFrame.commands.keyM=m|afișează/ascunde numărul mutărilor +LizzieFrame.commands.keyN=n|începe un joc împotriva lui Leela Zero +LizzieFrame.commands.keyO=o|deschide SGF +LizzieFrame.commands.keyP=p|pas +LizzieFrame.commands.keyS=s|salvează SGF +LizzieFrame.commands.keySpace=spațiu|pornește/oprește analiza +LizzieFrame.commands.keyUpArrow=săgeată sus|înapoi +LizzieFrame.commands.keyV=v|afișează/ascunde variante +LizzieFrame.commands.keyW=w|afișează/ascunde probabilitate victorie +LizzieFrame.commands.keyPeriod=.|scor +LizzieFrame.commands.mouseWheelScroll=rotiță mouse|înainte/înapoi +LizzieFrame.commands.rightClick=clic dreapta|înapoi +LizzieFrame.prompt.failedToOpenSgf=Nu s-a reușit deschiderea fișierului SGF +LizzieFrame.prompt.failedToSaveSgf=Nu s-a reușit salvarea fișierului SGF +LizzieFrame.prompt.sgfExists=Fișierul SGF există deja, doriți să-l înlocuiți? +LizzieFrame.prompt.showControlsHint=x apăsat = afișează comenzi +LizzieFrame.prompt.switching=comutare... +LizzieFrame.display.lastMove=Ulima mutare +LizzieFrame.display.pondering=Analizeză +LizzieFrame.display.on=pornit +LizzieFrame.display.off=oprit +LizzieFrame.display.loading=Leela Zero se încarcă... +LizzieFrame.display.download-latest-network-prompt=Descărcați cel mai nou fișier rețea? Poate să dureze. diff --git a/src/main/resources/l10n/DisplayStrings_zh_CN.properties b/src/main/resources/l10n/DisplayStrings_zh_CN.properties index 3c26237c0..c7effab3e 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_CN.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_CN.properties @@ -6,12 +6,14 @@ LizzieFrame.commands.keyAltC=ctrl-c|\u62F7\u8D1DSGF\u5230\u526A\u8D34\u677F LizzieFrame.commands.keyAltV=ctrl-v|\u4ECE\u526A\u8D34\u677F\u7C98\u8D34SGF\u5C40\u9762 LizzieFrame.commands.keyC=c|\u663E\u793A/\u9690\u85CF\u5750\u6807 +LizzieFrame.commands.keyD=d|show/hide dynamic komi LizzieFrame.commands.keyControl=ctrl|\u5411\u524D/\u5411\u540E10\u6B65 LizzieFrame.commands.keyDownArrow=\u4E0B\u65B9\u5411\u952E|\u5411\u540E\u4E00\u6B65 LizzieFrame.commands.keyEnd=end|\u8DF3\u5230\u68CB\u8C31\u672B\u5C3E LizzieFrame.commands.keyEnter=enter|\u8BA9Leela Zero\u843D\u5B50 LizzieFrame.commands.keyF=f|\u663E\u793A/\u9690\u85CF\u4E0B\u4E00\u624B LizzieFrame.commands.keyG=g|\u663E\u793A/\u9690\u85CF\u5206\u652F\u56FE +LizzieFrame.commands.keyT=t|\u663E\u793A/\u9690\u85CF\u8BC4\u8BBA LizzieFrame.commands.keyHome=home|\u8DF3\u8F6C\u5230\u68CB\u8C31\u5F00\u5934 LizzieFrame.commands.keyI=i|\u7F16\u8F91\u68CB\u5C40\u4FE1\u606F LizzieFrame.commands.keyA=a|\u8FD0\u884C\u7B80\u5355\u7684\u81EA\u52A8\u5168\u76D8\u5206\u6790 @@ -27,13 +29,17 @@ LizzieFrame.commands.keyW=w|\u663E\u793A/\u9690\u85CF\u80DC\u7387\u56FE LizzieFrame.commands.keyPeriod=.(\u5C0F\u6570\u70B9)|\u70B9\u76EE LizzieFrame.commands.mouseWheelScroll=\u9F20\u6807\u6EDA\u8F6E|\u5728\u68CB\u8C31\u4E2D\u5411\u524D/\u5411\u540E\u79FB\u52A8 LizzieFrame.commands.rightClick=\u9F20\u6807\u53F3\u952E|\u56DE\u4E0A\u4E00\u624B -LizzieFrame.prompt.failedToOpenSgf=\u4E0D\u80FD\u6253\u5F00SGF\u6587\u4EF6. -LizzieFrame.prompt.failedToSaveSgf=\u4E0D\u80FD\u4FDD\u5B58SGF\u6587\u4EF6. +LizzieFrame.prompt.failedToOpenFile=\u4E0D\u80FD\u6253\u5F00SGF\u6587\u4EF6. +LizzieFrame.prompt.failedToSaveFile=\u4E0D\u80FD\u4FDD\u5B58SGF\u6587\u4EF6. LizzieFrame.prompt.sgfExists=SGF\u6587\u4EF6\u5DF2\u7ECF\u5B58\u5728, \u9700\u8981\u66FF\u6362\u5417? LizzieFrame.prompt.showControlsHint=\u6309\u4F4FX\u4E0D\u653E\u67E5\u770B\u5FEB\u6377\u952E\u63D0\u793A +LizzieFrame.prompt.switching=\u5207\u6362\u4E2D... LizzieFrame.display.lastMove=\u6700\u540E\u4E00\u624B LizzieFrame.display.pondering=\u5206\u6790 LizzieFrame.display.on=\u5F00\u542F LizzieFrame.display.off=\u6682\u505C LizzieFrame.display.loading=Leela Zero\u6B63\u5728\u8F7D\u5165\u4E2D... LizzieFrame.display.download-latest-network-prompt=Download the latest network file? This may take some time. +LizzieFrame.display.leelaz-missing=Did not find Leela Zero, update config.txt or download from Leela Zero homepage +LizzieFrame.display.network-missing=Did not find network weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage +LizzieFrame.display.dynamic-komi=dyn. komi: diff --git a/src/test/java/common/Util.java b/src/test/java/common/Util.java new file mode 100644 index 000000000..7fa429488 --- /dev/null +++ b/src/test/java/common/Util.java @@ -0,0 +1,136 @@ +package common; + +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.rules.Board; +import featurecat.lizzie.rules.BoardData; +import featurecat.lizzie.rules.BoardHistoryList; +import featurecat.lizzie.rules.BoardHistoryNode; +import featurecat.lizzie.rules.SGFParser; +import featurecat.lizzie.rules.Stone; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Util { + + private static ArrayList laneUsageList = new ArrayList(); + + /** + * Get Variation Tree as String List The logic is same as the function VariationTree.drawTree + * + * @param startLane + * @param startNode + * @param variationNumber + * @param isMain + */ + public static void getVariationTree( + List moveList, + int startLane, + BoardHistoryNode startNode, + int variationNumber, + boolean isMain) { + // Finds depth on leftmost variation of this tree + int depth = BoardHistoryList.getDepth(startNode) + 1; + int lane = startLane; + // Figures out how far out too the right (which lane) we have to go not to collide with other + // variations + while (lane < laneUsageList.size() + && laneUsageList.get(lane) <= startNode.getData().moveNumber + depth) { + // laneUsageList keeps a list of how far down it is to a variation in the different "lanes" + laneUsageList.set(lane, startNode.getData().moveNumber - 1); + lane++; + } + if (lane >= laneUsageList.size()) { + laneUsageList.add(0); + } + if (variationNumber > 1) laneUsageList.set(lane - 1, startNode.getData().moveNumber - 1); + laneUsageList.set(lane, startNode.getData().moveNumber); + + // At this point, lane contains the lane we should use (the main branch is in lane 0) + BoardHistoryNode cur = startNode; + + // Draw main line + StringBuilder sb = new StringBuilder(); + sb.append(formatMove(cur.getData())); + while (cur.next() != null) { + cur = cur.next(); + sb.append(formatMove(cur.getData())); + } + moveList.add(sb.toString()); + // Now we have drawn all the nodes in this variation, and has reached the bottom of this + // variation + // Move back up, and for each, draw any variations we find + while (cur.previous() != null && cur != startNode) { + cur = cur.previous(); + int curwidth = lane; + // Draw each variation, uses recursion + for (int i = 1; i < cur.numberOfChildren(); i++) { + curwidth++; + // Recursion, depth of recursion will normally not be very deep (one recursion level for + // every variation that has a variation (sort of)) + getVariationTree(moveList, curwidth, cur.getVariation(i), i, false); + } + } + } + + private static String formatMove(BoardData data) { + String stone = ""; + if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; + else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; + else return stone; + + char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); + char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + + String comment = ""; + if (data.comment != null && data.comment.trim().length() > 0) { + comment = String.format("C[%s]", data.comment); + } + return String.format(";%s[%c%c]%s", stone, x, y, comment); + } + + public static String trimGameInfo(String sgf) { + String gameInfo = String.format("(?s).*AP\\[Lizzie: %s\\]", Lizzie.lizzieVersion); + return sgf.replaceFirst(gameInfo, "("); + } + + public static String[] splitAwAbSgf(String sgf) { + String[] ret = new String[2]; + String regex = "(A[BW]{1}(\\[[a-z]{2}\\])+)"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(sgf); + StringBuilder sb = new StringBuilder(); + while (matcher.find()) { + sb.append(matcher.group(0)); + } + ret[0] = sb.toString(); + ret[1] = sgf.replaceAll(regex, ""); + return ret; + } + + public static Stone[] convertStones(String awAb) { + Stone[] stones = new Stone[Board.BOARD_SIZE * Board.BOARD_SIZE]; + for (int i = 0; i < stones.length; i++) { + stones[i] = Stone.EMPTY; + } + String regex = "(A[BW]{1})|(?<=\\[)([a-z]{2})(?=\\])"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(awAb); + StringBuilder sb = new StringBuilder(); + Stone stone = Stone.EMPTY; + while (matcher.find()) { + String str = matcher.group(0); + if ("AB".equals(str)) { + stone = Stone.BLACK; + } else if ("AW".equals(str)) { + stone = Stone.WHITE; + } else { + int[] move = SGFParser.convertSgfPosToCoord(str); + int index = Board.getIndex(move[0], move[1]); + stones[index] = stone; + } + } + return stones; + } +} diff --git a/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java b/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java new file mode 100644 index 000000000..dbb5c5103 --- /dev/null +++ b/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java @@ -0,0 +1,64 @@ +package featurecat.lizzie.analysis; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.List; +import org.junit.Test; + +public class MoveDataTest { + @Test + public void testFromInfoLine() { + String info = "move R5 visits 38 winrate 5404 order 0 pv R5 Q5 R6 S4 Q10 C3 D3 C4 C6 C5 D5"; + MoveData moveData = MoveData.fromInfo(info); + + assertEquals(moveData.coordinate, "R5"); + assertEquals(moveData.playouts, 38); + assertEquals(moveData.winrate, 54.04, 0.01); + assertEquals( + moveData.variation, + Arrays.asList("R5", "Q5", "R6", "S4", "Q10", "C3", "D3", "C4", "C6", "C5", "D5")); + } + + private void testSummary( + String summary, String coordinate, int playouts, double winrate, List variation) { + MoveData moveData = MoveData.fromSummary(summary); + assertEquals(moveData.coordinate, coordinate); + assertEquals(moveData.playouts, playouts); + assertEquals(moveData.winrate, winrate, 0.01); + assertEquals(moveData.variation, variation); + } + + @Test + public void summaryLine1() { + testSummary( + " P16 -> 4 (V: 50.94%) (N: 5.79%) PV: P16 N18 R5 Q5", + "P16", 4, 50.94, Arrays.asList("P16", "N18", "R5", "Q5")); + } + + @Test + public void summaryLine2() { + testSummary( + " D9 -> 59 (V: 60.61%) (N: 52.59%) PV: D9 D12 E9 C13 C15 F17", + "D9", 59, 60.61, Arrays.asList("D9", "D12", "E9", "C13", "C15", "F17")); + } + + @Test + public void summaryLine3() { + testSummary( + " B2 -> 1 (V: 46.52%) (N: 86.74%) PV: B2", "B2", 1, 46.52, Arrays.asList("B2")); + } + + @Test + public void summaryLine4() { + testSummary( + " D16 -> 33 (V: 53.63%) (N: 27.64%) PV: D16 D4 Q16 O4 C3 C4", + "D16", 33, 53.63, Arrays.asList("D16", "D4", "Q16", "O4", "C3", "C4")); + } + + @Test + public void summaryLine5() { + testSummary( + " Q16 -> 0 (V: 0.00%) (N: 0.52%) PV: Q16\n", "Q16", 0, 0.0, Arrays.asList("Q16")); + } +} diff --git a/src/test/java/featurecat/lizzie/rules/SGFParserTest.java b/src/test/java/featurecat/lizzie/rules/SGFParserTest.java new file mode 100644 index 000000000..632e70c92 --- /dev/null +++ b/src/test/java/featurecat/lizzie/rules/SGFParserTest.java @@ -0,0 +1,125 @@ +package featurecat.lizzie.rules; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import common.Util; +import featurecat.lizzie.Config; +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.analysis.Leelaz; +import featurecat.lizzie.gui.LizzieFrame; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +public class SGFParserTest { + + private Lizzie lizzie = null; + + @Test + public void run() throws IOException { + lizzie = new Lizzie(); + lizzie.config = new Config(); + lizzie.board = new Board(); + lizzie.frame = new LizzieFrame(); + // new Thread( () -> { + lizzie.leelaz = new Leelaz(); + // }).start(); + + testVariaionOnly1(); + testFull1(); + } + + public void testVariaionOnly1() throws IOException { + + String sgfString = + "(;B[pd];W[dp];B[pp];W[dd];B[fq]" + + "(;W[cn];B[cc];W[cd];B[dc];W[ed];B[fc];W[fd]" + + "(;B[gb]" + + "(;W[hc];B[nq])" + + "(;W[gc];B[ec];W[hc];B[hb];W[ic]))" + + "(;B[gc];W[ec];B[eb];W[fb];B[db];W[hc];B[gb];W[gd];B[hb]))" + + "(;W[nq];B[cn];W[fp];B[gp];W[fo];B[dq];W[cq];B[eq];W[cp];B[dm];W[fm]))"; + + int variationNum = 4; + String mainBranch = + ";B[pd];W[dp];B[pp];W[dd];B[fq];W[cn];B[cc];W[cd];B[dc];W[ed];B[fc];W[fd];B[gb];W[hc];B[nq]"; + String variation1 = ";W[gc];B[ec];W[hc];B[hb];W[ic]"; + String variation2 = ";B[gc];W[ec];B[eb];W[fb];B[db];W[hc];B[gb];W[gd];B[hb]"; + String variation3 = ";W[nq];B[cn];W[fp];B[gp];W[fo];B[dq];W[cq];B[eq];W[cp];B[dm];W[fm]"; + + // Load correctly + boolean loaded = SGFParser.loadFromString(sgfString); + assertTrue(loaded); + + // Variations + List moveList = new ArrayList(); + Util.getVariationTree(moveList, 0, lizzie.board.getHistory().getCurrentHistoryNode(), 0, true); + + assertTrue(moveList != null); + assertEquals(moveList.size(), variationNum); + + assertEquals(moveList.get(0), mainBranch); + assertEquals(moveList.get(1), variation1); + assertEquals(moveList.get(2), variation2); + assertEquals(moveList.get(3), variation3); + + // Save correctly + String saveSgf = SGFParser.saveToString(); + assertTrue(saveSgf != null && saveSgf.trim().length() > 0); + + assertEquals(sgfString, Util.trimGameInfo(saveSgf)); + } + + public void testFull1() throws IOException { + + String sgfInfo = "(;CA[utf8]AP[MultiGo:4.4.4]SZ[19]"; + String sgfAwAb = + "AB[pe][pq][oq][nq][mq][cp][dq][eq][fp]AB[qd]AW[dc][cf][oc][qo][op][np][mp][ep][fq]"; + String sgfContent = + ";W[lp]C[25th question Overall view Black first Superior    White 1 has a long hand. The first requirement in the layout phase is to have a big picture.    What is the next black point in this situation?]" + + "(;B[qi]C[Correct Answer Limiting the thickness    Black 1 is broken. The reason why Black is under the command of four hands is to win the first hand and occupy the black one.    That is to say, on the lower side, the bigger one is the right side. Black 1 is both good and bad, and it limits the development of white and thick. It is good chess. Black 1 is appropriate, and it will not work if you go all the way or take a break.];W[lq];B[rp]C[1 Figure (turning head value?)    After black 1 , white is like 2 songs, then it is not too late to fly black again. There is a saying that \"the head is worth a thousand dollars\" in the chessboard, but in the situation of this picture, the white song has no such value.    Because after the next white A, black B, white must be on the lower side to be complete. It can be seen that for Black, the meaning of playing chess below is also not significant.    The following is a gesture that has come to an end. Both sides have no need to rush to settle down here.])" + + "(;B[kq];W[pi]C[2 diagram (failure)    Black 1 jump failed. The reason is not difficult to understand from the above analysis. If Black wants to jump out, he shouldn’t have four hands in the first place. By the white 2 on the right side of the hand, it immediately constitutes a strong appearance, black is not good. Although the black got some fixed ground below, but the position was too low, and it became a condensate, black is not worth the candle. ]))"; + String sgfString = sgfInfo + sgfAwAb + sgfContent; + + int variationNum = 2; + String mainBranch = + ";W[lp]C[25th question Overall view Black first Superior    White 1 has a long hand. The first requirement in the layout phase is to have a big picture.    What is the next black point in this situation?];B[qi]C[Correct Answer Limiting the thickness    Black 1 is broken. The reason why Black is under the command of four hands is to win the first hand and occupy the black one.    That is to say, on the lower side, the bigger one is the right side. Black 1 is both good and bad, and it limits the development of white and thick. It is good chess. Black 1 is appropriate, and it will not work if you go all the way or take a break.];W[lq];B[rp]C[1 Figure (turning head value?)    After black 1 , white is like 2 songs, then it is not too late to fly black again. There is a saying that \"the head is worth a thousand dollars\" in the chessboard, but in the situation of this picture, the white song has no such value.    Because after the next white A, black B, white must be on the lower side to be complete. It can be seen that for Black, the meaning of playing chess below is also not significant.    The following is a gesture that has come to an end. Both sides have no need to rush to settle down here.]"; + String variation1 = + ";B[kq];W[pi]C[2 diagram (failure)    Black 1 jump failed. The reason is not difficult to understand from the above analysis. If Black wants to jump out, he shouldn’t have four hands in the first place. By the white 2 on the right side of the hand, it immediately constitutes a strong appearance, black is not good. Although the black got some fixed ground below, but the position was too low, and it became a condensate, black is not worth the candle. ]"; + + Stone[] expectStones = Util.convertStones(sgfAwAb); + + // Load correctly + boolean loaded = SGFParser.loadFromString(sgfString); + assertTrue(loaded); + + // Variations + List moveList = new ArrayList(); + Util.getVariationTree(moveList, 0, lizzie.board.getHistory().getCurrentHistoryNode(), 0, true); + + assertTrue(moveList != null); + assertEquals(moveList.size(), variationNum); + assertEquals(moveList.get(0), mainBranch); + assertEquals(moveList.get(1), variation1); + + // AW/AB + assertArrayEquals(expectStones, Lizzie.board.getHistory().getStones()); + + // Save correctly + String saveSgf = SGFParser.saveToString(); + assertTrue(saveSgf != null && saveSgf.trim().length() > 0); + + String sgf = Util.trimGameInfo(saveSgf); + String[] ret = Util.splitAwAbSgf(sgf); + Stone[] actualStones = Util.convertStones(ret[0]); + + // AW/AB + assertArrayEquals(expectStones, actualStones); + + // Content + assertEquals("(" + sgfContent, ret[1]); + } +}