From 99c369d61c060e184219d6a2f030c51a7ef26cab Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Sun, 7 Oct 2018 12:09:06 +0200 Subject: [PATCH] formatted --- .../jhlabs/image/AbstractBufferedImageOp.java | 93 +- .../java/com/jhlabs/image/ConvolveFilter.java | 542 ++-- .../java/com/jhlabs/image/GaussianFilter.java | 259 +- src/main/java/com/jhlabs/image/ImageMath.java | 1189 +++++---- .../java/com/jhlabs/image/PixelUtils.java | 388 ++- .../java/featurecat/benchmark/Stopwatch.java | 91 +- src/main/java/featurecat/lizzie/Config.java | 566 ++-- src/main/java/featurecat/lizzie/Lizzie.java | 104 +- src/main/java/featurecat/lizzie/Util.java | 114 +- .../featurecat/lizzie/analysis/Branch.java | 56 +- .../featurecat/lizzie/analysis/GameInfo.java | 99 +- .../featurecat/lizzie/analysis/Leelaz.java | 990 +++---- .../lizzie/analysis/LeelazListener.java | 2 +- .../featurecat/lizzie/analysis/MoveData.java | 118 +- .../featurecat/lizzie/gui/BoardRenderer.java | 2217 ++++++++-------- .../featurecat/lizzie/gui/GameInfoDialog.java | 242 +- .../java/featurecat/lizzie/gui/Input.java | 709 +++-- .../featurecat/lizzie/gui/LizzieFrame.java | 1852 ++++++------- .../featurecat/lizzie/gui/NewGameDialog.java | 355 +-- .../featurecat/lizzie/gui/VariationTree.java | 281 +- .../featurecat/lizzie/gui/WinrateGraph.java | 404 +-- .../featurecat/lizzie/plugin/IPlugin.java | 56 +- .../lizzie/plugin/PluginManager.java | 140 +- .../java/featurecat/lizzie/rules/Board.java | 2340 +++++++++-------- .../featurecat/lizzie/rules/BoardData.java | 224 +- .../lizzie/rules/BoardHistoryList.java | 685 +++-- .../lizzie/rules/BoardHistoryNode.java | 314 ++- .../featurecat/lizzie/rules/GIBParser.java | 171 +- .../featurecat/lizzie/rules/SGFParser.java | 1030 ++++---- .../java/featurecat/lizzie/rules/Stone.java | 139 +- .../java/featurecat/lizzie/rules/Zobrist.java | 108 +- .../featurecat/lizzie/theme/DefaultTheme.java | 105 +- .../java/featurecat/lizzie/theme/ITheme.java | 65 +- src/test/java/common/Util.java | 238 +- .../lizzie/analysis/MoveDataTest.java | 66 +- .../lizzie/rules/SGFParserTest.java | 221 +- 36 files changed, 8419 insertions(+), 8154 deletions(-) diff --git a/src/main/java/com/jhlabs/image/AbstractBufferedImageOp.java b/src/main/java/com/jhlabs/image/AbstractBufferedImageOp.java index dc1257fb3..3172085cd 100644 --- a/src/main/java/com/jhlabs/image/AbstractBufferedImageOp.java +++ b/src/main/java/com/jhlabs/image/AbstractBufferedImageOp.java @@ -1,6 +1,6 @@ /* -** Copyright 2005 Huxtable.com. All rights reserved. -*/ + ** Copyright 2005 Huxtable.com. All rights reserved. + */ package com.jhlabs.image; @@ -13,47 +13,48 @@ */ 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 + 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); + } +} diff --git a/src/main/java/com/jhlabs/image/ConvolveFilter.java b/src/main/java/com/jhlabs/image/ConvolveFilter.java index 46a947dad..83efc8b56 100644 --- a/src/main/java/com/jhlabs/image/ConvolveFilter.java +++ b/src/main/java/com/jhlabs/image/ConvolveFilter.java @@ -1,285 +1,295 @@ /* -** Copyright 2005 Huxtable.com. All rights reserved. -*/ + ** Copyright 2005 Huxtable.com. All rights reserved. + */ package com.jhlabs.image; import java.awt.*; -import java.awt.image.*; import java.awt.geom.*; +import java.awt.image.*; /** * 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; - } + static final long serialVersionUID = 2239251672685254626L; - 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 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; + } } - - public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) { - if ( dstPt == null ) - dstPt = new Point2D.Double(); - dstPt.setLocation( srcPt.getX(), srcPt.getY() ); - return dstPt; + } + + /** 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]; - public RenderingHints getRenderingHints() { - return null; + 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 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 + public String toString() { + return "Blur/Convolve..."; + } +} diff --git a/src/main/java/com/jhlabs/image/GaussianFilter.java b/src/main/java/com/jhlabs/image/GaussianFilter.java index fb04e6f99..9aa4717e0 100644 --- a/src/main/java/com/jhlabs/image/GaussianFilter.java +++ b/src/main/java/com/jhlabs/image/GaussianFilter.java @@ -1,142 +1,143 @@ 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. + * 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; + 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); + } + + /** 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; - public String toString() { - return "Blur/Gaussian Blur..."; - } -} \ No newline at end of file + return new Kernel(rows, 1, matrix); + } + + public String toString() { + return "Blur/Gaussian Blur..."; + } +} diff --git a/src/main/java/com/jhlabs/image/ImageMath.java b/src/main/java/com/jhlabs/image/ImageMath.java index 1e95374d0..bfd069028 100644 --- a/src/main/java/com/jhlabs/image/ImageMath.java +++ b/src/main/java/com/jhlabs/image/ImageMath.java @@ -1,602 +1,599 @@ /* -** Copyright 2005 Huxtable.com. All rights reserved. -*/ + ** Copyright 2005 Huxtable.com. All rights reserved. + */ package com.jhlabs.image; -/** - * A class containing static math methods useful for image processing. - */ +/** 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 + public static final float PI = (float) Math.PI; + public static final float HALF_PI = (float) Math.PI / 2.0f; + public static final float QUARTER_PI = (float) Math.PI / 4.0f; + public static final 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 static final float m00 = -0.5f; + private static final float m01 = 1.5f; + private static final float m02 = -1.5f; + private static final float m03 = 0.5f; + private static final float m10 = 1.0f; + private static final float m11 = -2.5f; + private static final float m12 = 2.0f; + private static final float m13 = -0.5f; + private static final float m20 = -0.5f; + private static final float m21 = 0.0f; + private static final float m22 = 0.5f; + private static final float m23 = 0.0f; + private static final float m30 = 0.0f; + private static final float m31 = 1.0f; + private static final float m32 = 0.0f; + private static final 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++; + } + } + } +} diff --git a/src/main/java/com/jhlabs/image/PixelUtils.java b/src/main/java/com/jhlabs/image/PixelUtils.java index daf6f9d98..b80fb22e0 100644 --- a/src/main/java/com/jhlabs/image/PixelUtils.java +++ b/src/main/java/com/jhlabs/image/PixelUtils.java @@ -1,211 +1,207 @@ /* -** Copyright 2005 Huxtable.com. All rights reserved. -*/ + ** Copyright 2005 Huxtable.com. All rights reserved. + */ package com.jhlabs.image; -import java.util.*; import java.awt.Color; +import java.util.*; /** - * Some more useful math functions for image processing. - * These are becoming obsolete as we move to Java2D. Use MiscComposite instead. + * 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; + public static final int REPLACE = 0; + public static final int NORMAL = 1; + public static final int MIN = 2; + public static final int MAX = 3; + public static final int ADD = 4; + public static final int SUBTRACT = 5; + public static final int DIFFERENCE = 6; + public static final int MULTIPLY = 7; + public static final int HUE = 8; + public static final int SATURATION = 9; + public static final int VALUE = 10; + public static final int COLOR = 11; + public static final int SCREEN = 12; + public static final int AVERAGE = 13; + public static final int OVERLAY = 14; + public static final int CLEAR = 15; + public static final int EXCHANGE = 16; + public static final int DISSOLVE = 17; + public static final int DST_IN = 18; + public static final int ALPHA = 19; + public static final 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 static Random randomGenerator = new Random(); + private static final float hsb1[] = new float[3]; // FIXME-not thread safe + private static final float hsb2[] = new float[3]; // FIXME-not thread safe - /** - * 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; - } + // Return rgb1 painted onto rgb2 + public static int combinePixels(int rgb1, int rgb2, int op) { + return combinePixels(rgb1, rgb2, op, 0xff); + } - 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; + public static int combinePixels(int rgb1, int rgb2, int op, int extraAlpha, int channelMask) { + return (rgb2 & ~channelMask) | combinePixels(rgb1 & channelMask, rgb2, op, extraAlpha); + } - 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; - } + 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; -} \ No newline at end of file + 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; + } +} 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 8f6d7a3f1..ee5c70b50 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -1,7 +1,5 @@ package featurecat.lizzie; -import org.json.*; - import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; @@ -9,304 +7,314 @@ 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 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); - } - } - - 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 (needValidation && 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 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) { - if (config == null) { - return false; - } - - boolean madeCorrections = false; - - // Check ui configs - JSONObject ui = config.getJSONObject("ui"); + FileInputStream fp = new FileInputStream(file); - // 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"); - // 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; - } - - 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, 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"); - 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"); - } + 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; + if (modified) { + writeConfig(mergedcfg, file); } - 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; - } - 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; } - /** - * 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(); - } - } - } + boolean madeCorrections = false; - return "./leelaz"; - } + // 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", 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("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-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; + // 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); - //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; + // 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"); + 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 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("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-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 f88df1e3a..08db4991f 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -1,78 +1,74 @@ package featurecat.lizzie; -import org.json.JSONException; import featurecat.lizzie.analysis.Leelaz; +import featurecat.lizzie.gui.LizzieFrame; import featurecat.lizzie.plugin.PluginManager; import featurecat.lizzie.rules.Board; -import featurecat.lizzie.rules.SGFParser; -import featurecat.lizzie.gui.LizzieFrame; -import org.json.JSONObject; - -import javax.swing.*; import java.io.File; import java.io.IOException; -import java.util.ResourceBundle; +import javax.swing.*; +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"; + 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(); - PluginManager.loadPlugins(); - board = new Board(); - frame = new LizzieFrame(); + /** 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(); + PluginManager.loadPlugins(); + board = new Board(); + frame = new LizzieFrame(); - new Thread( () -> { - try { + new Thread( + () -> { + try { leelaz = new Leelaz(); - if(config.handicapInsteadOfWinrate) { - leelaz.estimatePassWinrate(); + if (config.handicapInsteadOfWinrate) { + leelaz.estimatePassWinrate(); } if (args.length == 1) { - frame.loadFile(new File(args[0])); + frame.loadFile(new File(args[0])); } else if (config.config.getJSONObject("ui").getBoolean("resume-previous-game")) { - board.resumePreviousGame(); + board.resumePreviousGame(); } leelaz.togglePonder(); - } catch (IOException e) { + } catch (IOException e) { e.printStackTrace(); System.exit(-1); - } - }).start(); - } - - 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.saveFile(); - } - } - if (board != null) { - board.autosaveToMemory(); - } + } + }) + .start(); + } - try { - config.persist(); - } catch (IOException err) { - // 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.saveFile(); + } + } + if (board != null) { + board.autosaveToMemory(); + } - if (leelaz != null) - leelaz.shutdown(); - System.exit(0); + try { + config.persist(); + } catch (IOException err) { + // Failed to save config } + if (leelaz != null) leelaz.shutdown(); + System.exit(0); + } } diff --git a/src/main/java/featurecat/lizzie/Util.java b/src/main/java/featurecat/lizzie/Util.java index 395dfd73a..432ebb1f5 100644 --- a/src/main/java/featurecat/lizzie/Util.java +++ b/src/main/java/featurecat/lizzie/Util.java @@ -11,73 +11,67 @@ 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 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(); } - /** - * 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(); - } + 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(); } + } } diff --git a/src/main/java/featurecat/lizzie/analysis/Branch.java b/src/main/java/featurecat/lizzie/analysis/Branch.java index ac2439c3f..a0f61d1d5 100644 --- a/src/main/java/featurecat/lizzie/analysis/Branch.java +++ b/src/main/java/featurecat/lizzie/analysis/Branch.java @@ -4,35 +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)); - 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; - } + 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 2c8c79a93..5c2774437 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -1,569 +1,583 @@ package featurecat.lizzie.analysis; -import featurecat.lizzie.Config; 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.regex.Matcher; -import java.util.regex.Pattern; +import javax.swing.*; +import org.json.JSONException; +import org.json.JSONObject; /** - * 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 + * 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 ArrayDeque cmdQueue; + 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; - // 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<>(); + // 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<>(); - isPondering = false; - startPonderTime = System.currentTimeMillis(); - cmdNumber = 1; - currentCmdNum = 0; - cmdQueue = new ArrayDeque<>(); + isPondering = false; + startPonderTime = System.currentTimeMillis(); + cmdNumber = 1; + currentCmdNum = 0; + cmdQueue = new ArrayDeque<>(); - JSONObject config = Lizzie.config.config.getJSONObject("leelaz"); + JSONObject config = Lizzie.config.config.getJSONObject("leelaz"); - printCommunication = config.getBoolean("print-comms"); - maxAnalyzeTimeMillis = MINUTE * config.getInt("max-analyze-time-minutes"); - - if (config.getBoolean("automatically-download-latest-network")) { - updateToLatestNetwork(); - } + printCommunication = config.getBoolean("print-comms"); + maxAnalyzeTimeMillis = MINUTE * config.getInt("max-analyze-time-minutes"); - File startfolder = new File(config.optString("engine-start-location", ".")); - String engineCommand = config.getString("engine-command"); - 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 - List commands = Arrays.asList(engineCommand.split(" ")); - - // 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"); - } - - // 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"); - } - - // 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(); - 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. - } + File startfolder = new File(config.optString("engine-start-location", ".")); + String engineCommand = config.getString("engine-command"); + 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 + List commands = Arrays.asList(engineCommand.split(" ")); + + // 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"); } - private String getBestNetworkHash() throws IOException { - return Util.downloadAsString(new URL(baseURL + "/best-network-hash")).split("\n")[0]; + // 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"); } - 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; - - String bestNetworkHash = getBestNetworkHash(); - - 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(); + Lizzie.frame.refreshBackground(); + } + + 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) { - bestMoves = new ArrayList<>(); - String[] variations = line.split(" info "); - for (String var : variations) { - bestMoves.add(MoveData.fromInfo(var)); - } - } + 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; - /** - * Parse a line of Leelaz output - * - * @param line output line - */ - private void parseLine(String line) { - synchronized (this) { - if (line.startsWith("komi=")) { - try { - 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; - 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; - - } else if (Lizzie.frame != null && (line.startsWith("=") || line.startsWith("?"))) { - if (printCommunication) { - System.out.print(line); - } - 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; - } - } - } - } + String bestNetworkHash = getBestNetworkHash(); - /** - * 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 - } - } - } + return !currentNetworkHash.equals(bestNetworkHash); } - - /** - * Continually reads and processes output from leelaz - */ - private void read() { + } + + /** 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)); + } + } + + /** + * 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; } - } - - /** - * 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(); + } else if (line.startsWith("opp_komi=")) { + try { + dynamicOppKomi = Float.parseFloat(line.substring("opp_komi=".length()).trim()); + } catch (NumberFormatException nfe) { + dynamicOppKomi = Float.NaN; } - } - - /** - * 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); + } else if (line.equals("\n")) { + // End of response + } else if (line.startsWith("info")) { + isLoaded = true; + 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 - */ - private void sendCommandToLeelaz(String command) { - if (command.startsWith("fixed_handicap")) - isSettingHandicap = true; - 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); } - 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; } + } } - - /** - * 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(); + } + + /** + * 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(); + 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 String getDynamicKomi() { - if (Float.isNaN(dynamicKomi) || Float.isNaN(dynamicOppKomi)) { - return null; - } - return String.format("%.1f / %.1f", dynamicKomi, dynamicOppKomi); + } + + /** 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 boolean isPondering() { - return isPondering; + 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(); } - - public class WinrateStats { - public double maxWinrate; - public int totalPlayouts; - - public WinrateStats(double maxWinrate, int totalPlayouts) { - this.maxWinrate = maxWinrate; - this.totalPlayouts = totalPlayouts; - } + } + + /** 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. - - // 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 boolean isPondering() { + return isPondering; + } - // 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); + public class WinrateStats { + public double maxWinrate; + public int totalPlayouts; - // 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 + 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); } - // 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; + } } 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 f66486dbb..988182925 100644 --- a/src/main/java/featurecat/lizzie/analysis/MoveData.java +++ b/src/main/java/featurecat/lizzie/analysis/MoveData.java @@ -6,73 +6,71 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * Holds the data from Leelaz's pondering mode - */ +/** Holds the data from Leelaz's pondering mode */ public class MoveData { - public String coordinate; - public int playouts; - public double winrate; - public List variation; + public String coordinate; + public int playouts; + public double winrate; + public List variation; - private MoveData() {} + private MoveData() {} - /** - * 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(" "); + /** + * 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(" "); - // Todo: Proper tag parsing in case gtp protocol is extended(?)/changed - for (int i=0; i(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; - } - } + // 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; } - return result; + if (key.equals("visits")) { + result.playouts = Integer.parseInt(value); + } + if (key.equals("winrate")) { + result.winrate = Integer.parseInt(value) / 100.0; + } + } } + return result; + } - /** - * 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; - } + /** + * 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: (.+).*$"); + 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 12c048374..e3e7e18d4 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -1,8 +1,5 @@ package featurecat.lizzie.gui; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.Branch; import featurecat.lizzie.analysis.MoveData; @@ -13,1135 +10,1285 @@ import featurecat.lizzie.rules.SGFParser; import featurecat.lizzie.rules.Stone; import featurecat.lizzie.rules.Zobrist; - +import featurecat.lizzie.theme.DefaultTheme; +import featurecat.lizzie.theme.ITheme; import java.awt.*; import java.awt.font.TextAttribute; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; -import java.awt.image.ImageObserver; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; - -import featurecat.lizzie.theme.DefaultTheme; -import featurecat.lizzie.theme.ITheme; +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; - - private int x, y; - private int boardLength; + 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; - private JSONObject uiConfig, uiPersist; + private int x, y; + private int boardLength; - private int scaledMargin, availableLength, squareLength, stoneRadius; - private Branch branch; - private List bestMoves; + private JSONObject uiConfig, uiPersist; + 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 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 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 ITheme theme; + 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; - private int maxAlpha = 240; + private int maxAlpha = 240; - public BoardRenderer(boolean isMainBoard) { - uiConfig = Lizzie.config.config.getJSONObject("ui"); - theme = ITheme.loadTheme(uiConfig.getString("theme")); - if (theme == null) { - theme = new DefaultTheme(); - } - uiPersist = Lizzie.config.persisted.getJSONObject("ui-persist"); - try { - maxAlpha = uiPersist.getInt("max-alpha"); - } catch (JSONException e) {} - this.isMainBoard = isMainBoard; + public BoardRenderer(boolean isMainBoard) { + uiConfig = Lizzie.config.config.getJSONObject("ui"); + theme = ITheme.loadTheme(uiConfig.getString("theme")); + if (theme == null) { + theme = new DefaultTheme(); } - - /** - * 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); - } - - drawStoneMarkup(g); - } - - PluginManager.onDraw(g); -// timer.lap("leelaz"); - -// timer.print(); + 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(); + drawBackground(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; - - int[] calculatedPixelMargins = calculatePixelMargins(); - boardLength = calculatedPixelMargins[0]; - scaledMargin = calculatedPixelMargins[1]; - availableLength = calculatedPixelMargins[2]; + renderImages(g); + // timer.lap("rendering images"); - 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() || - Lizzie.board.isForceRefresh()) { - - Lizzie.board.setForceRefresh(false); + if (!isShowingRawBoard()) { + drawMoveNumbers(g); + // timer.lap("movenumbers"); + if (!Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.config.showBestMoves) + drawLeelazSuggestions(g); - cachedBackgroundImage = new BufferedImage(Lizzie.frame.getWidth(), Lizzie.frame.getHeight(), - BufferedImage.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); - } - 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(); - } + if (Lizzie.config.showNextMoves) { + drawNextMoves(g); + } - g0.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); - g0.drawImage(cachedBackgroundImage, 0, 0, null); - cachedX = x; - cachedY = y; + drawStoneMarkup(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); - } + PluginManager.onDraw(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 originalBoardLength = 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 + (originalBoardLength - boardLength) / 2, y + (originalBoardLength - boardLength) / 2); + } + + /** + * 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() + || Lizzie.board.isForceRefresh()) { + + Lizzie.board.setForceRefresh(false); + + cachedBackgroundImage = + new BufferedImage( + Lizzie.frame.getWidth(), Lizzie.frame.getHeight(), BufferedImage.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(RenderingHints.KEY_ANTIALIASING, RenderingHints.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) { + drawStarPoints9x9(g); + } else if (Board.BOARD_SIZE == 13) { + drawStarPoints13x13(g); + } else { + drawStarPoints19x19(g); + } + } + + 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); + } } - - /** - * 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 (Lizzie.board.inScoreMode()) lastInScoreMode = true; - + } + + 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); + } } - /* - * 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(); + // Draw center + int centerX = x + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; + int centerY = y + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; + fillCircle(g, centerX, centerY, starPointRadius); + } + + 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 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; - } - - 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); - - if (branch == null) - return; - showingBranch = true; - - g.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++) { - 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; - - drawStone(g, gShadow, stoneX, stoneY, branch.data.stones[Board.getIndex(i, j)].unGhosted(), i, j); - - } + // Draw center + int centerX = x + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; + int centerY = y + scaledMargin + squareLength * STARPOINT_GRID_DISTANCE; + 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); } + } - g.dispose(); - gShadow.dispose(); + cachedZhash = Lizzie.board.getData().zobrist.clone(); + cachedDisplayedBranchLength = displayedBranchLength; + g.dispose(); + gShadow.dispose(); + lastInScoreMode = false; } - - 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.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; } - return null; + } } - - private MoveData getBestMove() { - return bestMoves.isEmpty() ? null : bestMoves.get(0); + g.dispose(); + } + + /** 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; - /** - * 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); - } + if (isMainBoard && (isShowingRawBoard() || !Lizzie.config.showBranch)) { + 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); - } - - return; - } + Graphics2D g = (Graphics2D) branchStonesImage.getGraphics(); + Graphics2D gShadow = (Graphics2D) branchStonesShadowImage.getGraphics(); - int[] moveNumberList = branch == null ? Lizzie.board.getMoveNumberList() : branch.data.moveNumberList; + MoveData suggestedMove = (isMainBoard ? mouseHoveredMove() : getBestMove()); + if (suggestedMove == null) return; + variation = suggestedMove.variation; + branch = new Branch(Lizzie.board, variation); - // Allow to display only last move number - int lastMoveNumber = branch == null ? Lizzie.board.getData().moveNumber : branch.data.moveNumber; - int onlyLastMoveNumber = Lizzie.config.uiConfig.optInt("only-last-move-number", 9999); + if (branch == null) return; + showingBranch = true; - 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; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - // Allow to display only last move number - if (lastMoveNumber - moveNumberList[Board.getIndex(i, j)] >= onlyLastMoveNumber) { - 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; - 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); - } + int stoneX = scaledMargin + squareLength * i; + int stoneY = scaledMargin + squareLength * j; - String moveNumberString = moveNumberList[Board.getIndex(i, j)] + ""; - drawString(g, stoneX, stoneY, LizzieFrame.OpenSansRegularBase, moveNumberString, (float) (stoneRadius * 1.4), (int) (stoneRadius * 1.4)); - } - } - } + drawStone( + g, gShadow, stoneX, stoneY, branch.data.stones[Board.getIndex(i, j)].unGhosted(), i, j); + } } - /** - * 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 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; - boolean hasMaxWinrate = move.winrate == maxWinrate; - - 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 + (maxAlpha - 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 != hasMaxWinrate) { - 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 && (hasMaxWinrate || percentPlayouts >= uiConfig.getDouble("min-playout-ratio-for-stats"))) || - (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); - } - } - } - - + 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; } - } - 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)); - } + if (coord[0] == Lizzie.frame.mouseHoverCoordinate[0] + && coord[1] == Lizzie.frame.mouseHoverCoordinate[1]) { + return move; } + } } - - private void drawWoodenBoard(Graphics2D g) { - if (uiConfig.getBoolean("fancy-board")) { - // fancy version - int shadowRadius = (int) (boardLength * MARGIN / 6); - BufferedImage boardImage = theme.getBoard(); - // Support seamless texture - drawTextureImage(g, boardImage == null ? theme.getBoard() : boardImage, 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 { - // 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); - } + 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(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + g.drawImage(cachedStonesShadowImage, x, y, null); + if (Lizzie.config.showBranch) { + g.drawImage(branchStonesShadowImage, x, y, null); } - - /** - * 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}; + g.drawImage(cachedStonesImage, x, y, null); + if (Lizzie.config.showBranch) { + g.drawImage(branchStonesImage, x, y, null); } - - private void drawShadow(Graphics2D g, int centerX, int centerY, boolean isGhost) { - drawShadow(g, centerX, centerY, isGhost, 1); + } + + /** 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); + } + + return; } - 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); + int[] moveNumberList = + branch == null ? Lizzie.board.getMoveNumberList() : branch.data.moveNumberList; + // Allow to display only last move number + int lastMoveNumber = + branch == null ? Lizzie.board.getData().moveNumber : branch.data.moveNumber; + int onlyLastMoveNumber = Lizzie.config.uiConfig.optInt("only-last-move-number", 9999); - final Paint TOP_GRADIENT_PAINT; - final Paint LOWER_RIGHT_GRADIENT_PAINT; + 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; - 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) - }); + // Allow to display only last move number + if (lastMoveNumber - moveNumberList[Board.getIndex(i, j)] >= onlyLastMoveNumber) { + continue; } - 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); + 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)); } - 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(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; - - 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); - Image stone = isBlack ? theme.getBlackStone(new int[]{x, y}) : theme.getWhiteStone(new int[]{x, y}); - int size = stoneRadius * 2 + 1; - g.drawImage(getScaleStone(stone, isBlack, size, size), centerX - stoneRadius, centerY - stoneRadius, size, size, null); - } else { - drawShadow(gShadow, centerX, centerY, true); - g.setColor(isBlack ? (isGhost ? new Color(0, 0, 0) : Color.BLACK) : (isGhost ? new Color(255, 255, 255) : Color.WHITE)); - fillCircle(g, centerX, centerY, stoneRadius); - if (!isBlack) { - g.setColor(isGhost ? new Color(0, 0, 0) : Color.BLACK); - drawCircle(g, centerX, centerY, stoneRadius); - } + } + + /** + * 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 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; } - } - } - - /** - * Get scaled stone, if cached then return cached - */ - public BufferedImage getScaleStone(Image img, boolean isBlack, int width, int height) { - BufferedImage stone = isBlack ? cachedBlackStoneImage : cachedWhiteStoneImage; - if (stone == null || stone.getWidth() != width || stone.getHeight() != height) { - stone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = stone.createGraphics(); - g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, null); - g2.dispose(); - if (isBlack) { - cachedBlackStoneImage = stone; + if (coord[0] == i && coord[1] == j) { + move = m; + break; + } + } + + if (move == null) continue; + + boolean isBestMove = bestMoves.get(0) == move; + boolean hasMaxWinrate = move.winrate == maxWinrate; + + 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 + + (maxAlpha - 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 != hasMaxWinrate) { + strokeWidth = 2; + g.setColor(isBestMove ? Color.RED : Color.BLUE); + g.setStroke(new BasicStroke(strokeWidth)); } else { - cachedWhiteStoneImage = stone; + g.setColor(color.darker()); } - } - return stone; - } - - /** - * 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, BufferedImage.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, BufferedImage.TYPE_INT_ARGB); -// Graphics2D g2 = tmp.createGraphics(); -// g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.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)); - } - - /** - * Draw stone Markups - * - * @param g - */ - private void drawStoneMarkup(Graphics2D g) { - - BoardData data = Lizzie.board.getHistory().getData(); - - data.getProperties().forEach((key, value) -> { - if (SGFParser.isListProperty(key)) { - String[] labels = value.split(","); - for (String label : labels) { - String[] moves = label.split(":"); - int[] move = SGFParser.convertSgfPosToCoord(moves[0]); - if (move != null) { - int[] lastMove = branch == null ? Lizzie.board.getLastMove() : branch.data.lastMove; - if (!Arrays.equals(move, lastMove)) { - int moveX = x + scaledMargin + squareLength * move[0]; - int moveY = y + scaledMargin + squareLength * move[1]; - g.setColor(Lizzie.board.getStones()[Board.getIndex(move[0], move[1])].isBlack() ? Color.WHITE : Color.BLACK); - g.setStroke(new BasicStroke(2)); - if ("LB".equals(key) && moves.length > 1) { - // Label - drawString(g, moveX, moveY, LizzieFrame.OpenSansRegularBase, moves[1], (float) (stoneRadius * 1.4), (int) (stoneRadius * 1.4)); - } else if ("TR".equals(key)) { - // Triangle - drawTriangle(g, moveX, moveY, (stoneRadius + 1)*2/3); - } else if ("SQ".equals(key)) { - // Square - drawSquare(g, moveX, moveY, (stoneRadius + 1)/2); - } else if ("CR".equals(key)) { - // Circle - drawCircle(g, moveX, moveY, stoneRadius*2/3); - } else if ("MA".equals(key)) { - // Mark (X) - drawMarkX(g, moveX, moveY, (stoneRadius + 1)/2); - } - } - } - } + drawCircle(g, suggestionX, suggestionY, stoneRadius - strokeWidth / 2); + g.setStroke(new BasicStroke(1)); + } + + if ((branch == null + && (hasMaxWinrate + || percentPlayouts >= uiConfig.getDouble("min-playout-ratio-for-stats"))) + || (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; } - }); - } - - /** - * Draws the triangle of a circle centered at (centerX, centerY) with radius $radius$ - */ - private void drawTriangle(Graphics2D g, int centerX, int centerY, int radius) { - int offset = (int)(3.0/2.0*radius/Math.sqrt(3.0)); - int x[] = {centerX, centerX - offset, centerX + offset}; - int y[] = {centerY - radius, centerY + radius/2, centerY + radius/2}; - g.drawPolygon(x, y, 3); - } - - /** - * Draws the square of a circle centered at (centerX, centerY) with radius $radius$ - */ - private void drawSquare(Graphics2D g, int centerX, int centerY, int radius) { - g.drawRect(centerX - radius, centerY - radius, radius * 2, radius * 2); - } - - /** - * Draws the mark(X) of a circle centered at (centerX, centerY) with radius $radius$ - */ - private void drawMarkX(Graphics2D g, int centerX, int centerY, int radius) { - g.drawLine(centerX - radius, centerY - radius, centerX + radius, centerY + radius); - g.drawLine(centerX - radius, centerY + radius, centerX + radius, centerY - radius); - } - - /** - * 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); - } - - 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); - } + 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); + } - /** - * @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); + 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); + } } + } } - - - private int[] calculatePixelMargins() { - return calculatePixelMargins(boardLength); + } + + 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)); + } } - - /** - * 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; + } + + private void drawWoodenBoard(Graphics2D g) { + if (uiConfig.getBoolean("fancy-board")) { + // fancy version + int shadowRadius = (int) (boardLength * MARGIN / 6); + BufferedImage boardImage = theme.getBoard(); + // Support seamless texture + drawTextureImage( + g, + boardImage == null ? theme.getBoard() : boardImage, + 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 { + // 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); } - - public Point getLocation() { - return new Point(x, y); + } + + /** + * 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)}); } - /** - * Set the maximum boardLength to render the board - * - * @param boardLength the boardLength of the board - */ - public void setBoardLength(int boardLength) { - this.boardLength = boardLength; - } + final Paint originalPaint = g.getPaint(); - /** - * @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)); + 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); } - - /** - * 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); + 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(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; + + 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); + Image stone = + isBlack ? theme.getBlackStone(new int[] {x, y}) : theme.getWhiteStone(new int[] {x, y}); + int size = stoneRadius * 2 + 1; + g.drawImage( + getScaleStone(stone, isBlack, size, size), + centerX - stoneRadius, + centerY - stoneRadius, + size, + size, + null); + } else { + drawShadow(gShadow, centerX, centerY, true); + g.setColor( + isBlack + ? (isGhost ? new Color(0, 0, 0) : Color.BLACK) + : (isGhost ? new Color(255, 255, 255) : Color.WHITE)); + fillCircle(g, centerX, centerY, stoneRadius); + if (!isBlack) { + g.setColor(isGhost ? new Color(0, 0, 0) : Color.BLACK); + drawCircle(g, centerX, centerY, stoneRadius); + } + } } - - private boolean isShowingRawBoard() { - return (displayedBranchLength == SHOW_RAW_BOARD || displayedBranchLength == 0); + } + + /** Get scaled stone, if cached then return cached */ + public BufferedImage getScaleStone(Image img, boolean isBlack, int width, int height) { + BufferedImage stone = isBlack ? cachedBlackStoneImage : cachedWhiteStoneImage; + if (stone == null || stone.getWidth() != width || stone.getHeight() != height) { + stone = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = stone.createGraphics(); + g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0, null); + g2.dispose(); + if (isBlack) { + cachedBlackStoneImage = stone; + } else { + cachedWhiteStoneImage = stone; + } } - - private int maxBranchMoves() { - switch (displayedBranchLength) { - case SHOW_NORMAL_BOARD: - return Integer.MAX_VALUE; - case SHOW_RAW_BOARD: - return -1; - default: - return displayedBranchLength; - } + return stone; + } + + /** + * 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, BufferedImage.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, BufferedImage.TYPE_INT_ARGB); + // Graphics2D g2 = tmp.createGraphics(); + // g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + // RenderingHints.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)); + } + + /** + * Draw stone Markups + * + * @param g + */ + private void drawStoneMarkup(Graphics2D g) { + + BoardData data = Lizzie.board.getHistory().getData(); + + data.getProperties() + .forEach( + (key, value) -> { + if (SGFParser.isListProperty(key)) { + String[] labels = value.split(","); + for (String label : labels) { + String[] moves = label.split(":"); + int[] move = SGFParser.convertSgfPosToCoord(moves[0]); + if (move != null) { + int[] lastMove = + branch == null ? Lizzie.board.getLastMove() : branch.data.lastMove; + if (!Arrays.equals(move, lastMove)) { + int moveX = x + scaledMargin + squareLength * move[0]; + int moveY = y + scaledMargin + squareLength * move[1]; + g.setColor( + Lizzie.board.getStones()[Board.getIndex(move[0], move[1])].isBlack() + ? Color.WHITE + : Color.BLACK); + g.setStroke(new BasicStroke(2)); + if ("LB".equals(key) && moves.length > 1) { + // Label + drawString( + g, + moveX, + moveY, + LizzieFrame.OpenSansRegularBase, + moves[1], + (float) (stoneRadius * 1.4), + (int) (stoneRadius * 1.4)); + } else if ("TR".equals(key)) { + // Triangle + drawTriangle(g, moveX, moveY, (stoneRadius + 1) * 2 / 3); + } else if ("SQ".equals(key)) { + // Square + drawSquare(g, moveX, moveY, (stoneRadius + 1) / 2); + } else if ("CR".equals(key)) { + // Circle + drawCircle(g, moveX, moveY, stoneRadius * 2 / 3); + } else if ("MA".equals(key)) { + // Mark (X) + drawMarkX(g, moveX, moveY, (stoneRadius + 1) / 2); + } + } + } + } + } + }); + } + + /** Draws the triangle of a circle centered at (centerX, centerY) with radius $radius$ */ + private void drawTriangle(Graphics2D g, int centerX, int centerY, int radius) { + int offset = (int) (3.0 / 2.0 * radius / Math.sqrt(3.0)); + int x[] = {centerX, centerX - offset, centerX + offset}; + int y[] = {centerY - radius, centerY + radius / 2, centerY + radius / 2}; + g.drawPolygon(x, y, 3); + } + + /** Draws the square of a circle centered at (centerX, centerY) with radius $radius$ */ + private void drawSquare(Graphics2D g, int centerX, int centerY, int radius) { + g.drawRect(centerX - radius, centerY - radius, radius * 2, radius * 2); + } + + /** Draws the mark(X) of a circle centered at (centerX, centerY) with radius $radius$ */ + private void drawMarkX(Graphics2D g, int centerX, int centerY, int radius) { + g.drawLine(centerX - radius, centerY - radius, centerX + radius, centerY + radius); + g.drawLine(centerX - radius, centerY + radius, centerX + radius, centerY - radius); + } + + /** 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; } - - public boolean isShowingBranch() { - return showingBranch; + // 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); + } + + 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 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); } - - public void setDisplayedBranchLength(int n) { - displayedBranchLength = n; + } + + 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 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 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 = 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 = Math.min(maxAlpha + k, 255); - uiPersist.put("max-alpha", maxAlpha); - } + public void increaseMaxAlpha(int k) { + maxAlpha = Math.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 ee32f376a..e576bb8bb 100644 --- a/src/main/java/featurecat/lizzie/gui/Input.java +++ b/src/main/java/featurecat/lizzie/gui/Input.java @@ -1,411 +1,398 @@ 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 featurecat.lizzie.plugin.PluginManager; +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) { + 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) {} + + @Override + public void mouseExited(MouseEvent e) {} + + @Override + public void mouseDragged(MouseEvent e) { + int x = e.getX(); + int y = e.getY(); + + Lizzie.frame.onMouseDragged(x, y); + } + + @Override + public void mouseMoved(MouseEvent e) { + PluginManager.onMouseMoved(e); + int x = e.getX(); + int y = e.getY(); + + Lizzie.frame.onMouseMoved(x, y); + } + + @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(); + if (Lizzie.frame.incrementDisplayedBranchLength(-movesToAdvance)) { + return; } - @Override - public void mouseReleased(MouseEvent e) { - PluginManager.onMouseReleased(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 mouseEntered(MouseEvent e) { - + if (Lizzie.frame.incrementDisplayedBranchLength(movesToAdvance)) { + return; } - @Override - public void mouseExited(MouseEvent e) { + for (int i = 0; i < movesToAdvance; i++) Lizzie.board.nextMove(); + } + private void startRawBoard() { + if (!Lizzie.config.showRawBoard) { + Lizzie.frame.startRawBoard(); } - - @Override - public void mouseDragged(MouseEvent e) { - int x = e.getX(); - int y = e.getY(); - - Lizzie.frame.onMouseDragged(x, y); + 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(); + } - @Override - public void mouseMoved(MouseEvent e) { - PluginManager.onMouseMoved(e); - int x = e.getX(); - int y = e.getY(); - - Lizzie.frame.onMouseMoved(x, y); + private void previousBranch() { + if (Lizzie.frame.isPlayingAgainstLeelaz) { + Lizzie.frame.isPlayingAgainstLeelaz = false; } + Lizzie.board.previousBranch(); + } - @Override - public void keyTyped(KeyEvent e) { + private void moveBranchUp() { + Lizzie.board.moveBranchUp(); + } - } + private void moveBranchDown() { + Lizzie.board.moveBranchDown(); + } - private void undo() { - undo(1); - } + private void deleteMove() { + Lizzie.board.deleteMove(); + } - private void undo(int movesToAdvance) { - if (Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.frame.isPlayingAgainstLeelaz = false; - } - if (Lizzie.frame.incrementDisplayedBranchLength(- movesToAdvance)) { - return; - } + private void deleteBranch() { + Lizzie.board.deleteBranch(); + } - for (int i = 0; i < movesToAdvance; i++) - Lizzie.board.previousMove(); - } + private boolean controlIsPressed(KeyEvent e) { + boolean mac = System.getProperty("os.name", "").toUpperCase().startsWith("MAC"); + return e.isControlDown() || (mac && e.isMetaDown()); + } - 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 toggleShowDynamicKomi() { + Lizzie.config.showDynamicKomi = !Lizzie.config.showDynamicKomi; + } - private void redo() { - redo(1); - } + @Override + public void keyPressed(KeyEvent e) { - private void redo(int movesToAdvance) { - if (Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.frame.isPlayingAgainstLeelaz = false; - } - if (Lizzie.frame.incrementDisplayedBranchLength(movesToAdvance)) { - return; - } + PluginManager.onKeyPressed(e); - for (int i = 0; i < movesToAdvance; i++) - Lizzie.board.nextMove(); - } + // If any controls key is pressed, let's disable analysis mode. + // This is probably the user attempting to exit analysis mode. + boolean shouldDisableAnalysis = true; - private void startRawBoard() { - if (!Lizzie.config.showRawBoard) { - Lizzie.frame.startRawBoard(); + switch (e.getKeyCode()) { + case VK_RIGHT: + if (e.isShiftDown()) { + moveBranchDown(); + } else { + nextBranch(); } - Lizzie.config.showRawBoard = true; - } + break; - 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; - } + 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; - private void nextBranch() { - if (Lizzie.frame.isPlayingAgainstLeelaz) { - Lizzie.frame.isPlayingAgainstLeelaz = false; + case VK_PAGE_DOWN: + if (controlIsPressed(e) && e.isShiftDown()) { + Lizzie.frame.increaseMaxAlpha(-5); + } else { + redo(10); } - Lizzie.board.nextBranch(); - } + break; - private void previousBranch() { + 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; } - Lizzie.board.previousBranch(); - } + 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); + } + 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(); + } + 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; - private void moveBranchUp() { - Lizzie.board.moveBranchUp(); - } + case VK_Z: + if (e.isShiftDown()) { + toggleHints(); + } else { + startRawBoard(); + } + break; - private void moveBranchDown() { - Lizzie.board.moveBranchDown(); - } + case VK_A: + shouldDisableAnalysis = false; + Lizzie.board.toggleAnalysis(); + break; - private void deleteMove() { Lizzie.board.deleteMove(); } + case VK_PERIOD: + if (Lizzie.board.getHistory().getNext() == null) { + Lizzie.board.setScoreMode(!Lizzie.board.inScoreMode()); + } + break; - private void deleteBranch() { Lizzie.board.deleteBranch(); } + case VK_D: + toggleShowDynamicKomi(); + break; - private boolean controlIsPressed(KeyEvent e) { - boolean mac = System.getProperty("os.name", "").toUpperCase().startsWith("MAC"); - return e.isControlDown() || (mac && e.isMetaDown()); + default: + shouldDisableAnalysis = false; } - private void toggleShowDynamicKomi() { - Lizzie.config.showDynamicKomi = !Lizzie.config.showDynamicKomi; - } + if (shouldDisableAnalysis && Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); - @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: - if (controlIsPressed(e) && e.isShiftDown()) { - Lizzie.frame.increaseMaxAlpha(-5); - } else { - 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: - if (controlIsPressed(e) && e.isShiftDown()) { - Lizzie.frame.increaseMaxAlpha(5); - } else { - 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.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(); - } - 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; - - case VK_D: - toggleShowDynamicKomi(); - break; - - default: - shouldDisableAnalysis = false; - } + Lizzie.frame.repaint(); + } - if (shouldDisableAnalysis && Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); + 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(); - } - - 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: - } - } + break; - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - if (Lizzie.board.inAnalysisMode()) - Lizzie.board.toggleAnalysis(); - if (e.getWheelRotation() > 0) { - redo(); - } else if (e.getWheelRotation() < 0) { - undo(); - } + case VK_Z: + stopRawBoard(); Lizzie.frame.repaint(); + break; + + 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(); + } } diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 68f879196..e2f44b8fe 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,17 +9,20 @@ import featurecat.lizzie.rules.BoardData; import featurecat.lizzie.rules.GIBParser; import featurecat.lizzie.rules.SGFParser; -import org.json.JSONObject; -import org.json.JSONArray; - -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.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferStrategy; @@ -39,910 +32,989 @@ 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(); - - private long lastAutosaveTime = System.currentTimeMillis(); - - 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); - - boardRenderer = new BoardRenderer(true); - subBoardRenderer = new BoardRenderer(false); - variationTree = new VariationTree(); - winrateGraph = new WinrateGraph(); - - 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 - - if (Lizzie.config.startMaximized) { - setExtendedState(Frame.MAXIMIZED_BOTH); // start maximized - } - - 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(); - } + 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(); + + private long lastAutosaveTime = System.currentTimeMillis(); + + 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); + + boardRenderer = new BoardRenderer(true); + subBoardRenderer = new BoardRenderer(false); + variationTree = new VariationTree(); + winrateGraph = new WinrateGraph(); + + 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 + + if (Lizzie.config.startMaximized) { + setExtendedState(Frame.MAXIMIZED_BOTH); // start maximized + } + + 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; } - } - - 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 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"); + } + if (!file.getPath().endsWith(".sgf")) { + 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()); + SGFParser.save(Lizzie.board, file.getPath()); + filesystem.put("last-folder", file.getParent()); } catch (IOException err) { - JOptionPane.showConfirmDialog(null, resourceBundle.getString("LizzieFrame.prompt.failedToOpenFile"), "Error", JOptionPane.ERROR); + JOptionPane.showConfirmDialog( + null, + resourceBundle.getString("LizzieFrame.prompt.failedTosaveFile"), + "Error", + JOptionPane.ERROR); } } + } + + 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); + } - 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) { - drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.pondering") + - (Lizzie.leelaz.isPondering()?resourceBundle.getString("LizzieFrame.display.on"):resourceBundle.getString("LizzieFrame.display.off")), - ponderingX, ponderingY, ponderingSize); - } - - if (Lizzie.config.showDynamicKomi && Lizzie.leelaz.getDynamicKomi() != null) { - drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.dynamic-komi"), dynamicKomiLabelX, dynamicKomiLabelY, dynamicKomiSize); - drawPonderingState(g, Lizzie.leelaz.getDynamicKomi(), dynamicKomiX, dynamicKomiY, dynamicKomiSize); - } - - // 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 if (Lizzie.config.showStatus) { - drawPonderingState(g, resourceBundle.getString("LizzieFrame.display.loading"), loadingX, loadingY, loadingSize); - } - - if (Lizzie.config.showCaptured) - drawCaptured(g, capx, capy, capw, caph); - - // cleanup - g.dispose(); - } - - // 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 background = boardRenderer.theme.getBackground(); - int drawWidth = Math.max(background.getWidth(), getWidth()); - int drawHeight = Math.max(background.getHeight(), getHeight()); - // Support seamless texture - boardRenderer.drawTextureImage(g, background, 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) { - 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); - } - - 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); + // initialize - /** - * Display the controls - */ - void drawControls() { - userAlreadyKnowsAboutCommandString = true; + cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D g = (Graphics2D) cachedImage.getGraphics(); - cachedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); + if (Lizzie.config.showStatus) drawCommandString(g); - // redraw background - createBackground(); + boardRenderer.setLocation(boardX, boardY); + boardRenderer.setBoardLength(maxSize); + boardRenderer.draw(g); - List commandsToShow = new ArrayList<>(Arrays.asList(commands)); - if (Lizzie.leelaz.getDynamicKomi() != null) { - commandsToShow.add(resourceBundle.getString("LizzieFrame.commands.keyD")); + if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { + if (Lizzie.config.showStatus) { + drawPonderingState( + g, + resourceBundle.getString("LizzieFrame.display.pondering") + + (Lizzie.leelaz.isPondering() + ? resourceBundle.getString("LizzieFrame.display.on") + : resourceBundle.getString("LizzieFrame.display.off")), + ponderingX, + ponderingY, + ponderingSize); } - 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 = commandsToShow.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(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; + if (Lizzie.config.showDynamicKomi && Lizzie.leelaz.getDynamicKomi() != null) { + drawPonderingState( + g, + resourceBundle.getString("LizzieFrame.display.dynamic-komi"), + dynamicKomiLabelX, + dynamicKomiLabelY, + dynamicKomiSize); + drawPonderingState( + g, Lizzie.leelaz.getDynamicKomi(), dynamicKomiX, dynamicKomiY, dynamicKomiSize); } - 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; - } - - 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; + // 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); } - 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 (Lizzie.config.showVariationGraph) { + drawVariationTreeContainer(backgroundG, vx, vy, vw, vh); + variationTree.draw(g, treex, treey, treew, treeh); } - - 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); + 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) { + drawPonderingState( + g, + resourceBundle.getString("LizzieFrame.display.loading"), + loadingX, + loadingY, + loadingSize); + } - 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); - } + if (Lizzie.config.showCaptured) drawCaptured(g, capx, capy, capw, caph); + + // cleanup + g.dispose(); + } + + // draw the image + Graphics2D bsGraphics = (Graphics2D) bs.getDrawGraphics(); + bsGraphics.drawImage(cachedBackground, 0, 0, null); + bsGraphics.drawImage(cachedImage, 0, 0, null); - 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); - } + // 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 background = boardRenderer.theme.getBackground(); + int drawWidth = Math.max(background.getWidth(), getWidth()); + int drawHeight = Math.max(background.getHeight(), getHeight()); + // Support seamless texture + boardRenderer.drawTextureImage(g, background, 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) { + 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); + } + + 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")); + } + + 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 = + commandsToShow + .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(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; + } + + 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; + } + + 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); + } - private void setPanelFont(Graphics2D g, float size) { - Font font = OpenSansRegularBase.deriveFont(Font.PLAIN, size); - g.setFont(font); + 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); + } + } + + 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); + } + + 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]); + } + if (Lizzie.config.showWinrate && moveNumber >= 0) { + isPlayingAgainstLeelaz = false; + Lizzie.board.goToMoveNumberBeyondBranch(moveNumber); + } + if (Lizzie.config.showSubBoard && subBoardRenderer.isInside(x, y)) { + Lizzie.config.toggleLargeSubBoard(); + } + 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]); + } } - - /** - * 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(); - } + 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 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; + } + } + + 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 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; - } - - 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 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; - } - } - - 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(); - } - } - } - - 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 toggleCoordinates() { - showCoordinates = !showCoordinates; - } - - public void setPlayers(String whitePlayer, String blackPlayer) { - setTitle(String.format("%s (%s [W] vs %s [B])", DEFAULT_TITLE, - whitePlayer, blackPlayer)); - } - - 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() { - setTitle(DEFAULT_TITLE); - } - - 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(); + } + + 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 toggleCoordinates() { + showCoordinates = !showCoordinates; + } + + public void setPlayers(String whitePlayer, String blackPlayer) { + setTitle(String.format("%s (%s [W] vs %s [B])", DEFAULT_TITLE, whitePlayer, blackPlayer)); + } + + 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() { + setTitle(DEFAULT_TITLE); + } + + 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); } - } + } - 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(); - } + // 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); - } + public void increaseMaxAlpha(int k) { + boardRenderer.increaseMaxAlpha(k); + } } diff --git a/src/main/java/featurecat/lizzie/gui/NewGameDialog.java b/src/main/java/featurecat/lizzie/gui/NewGameDialog.java index 1d3a8389a..585676d10 100644 --- a/src/main/java/featurecat/lizzie/gui/NewGameDialog.java +++ b/src/main/java/featurecat/lizzie/gui/NewGameDialog.java @@ -5,185 +5,194 @@ 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()); + + 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. } - - 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..72f072bf0 100644 --- a/src/main/java/featurecat/lizzie/gui/VariationTree.java +++ b/src/main/java/featurecat/lizzie/gui/VariationTree.java @@ -3,143 +3,164 @@ 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; - - public VariationTree() - { - laneUsageList = new ArrayList(); + 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++; } - - 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; - } + 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 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); + // 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 + + // 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..1b0de91c6 100644 --- a/src/main/java/featurecat/lizzie/gui/WinrateGraph.java +++ b/src/main/java/featurecat/lizzie/gui/WinrateGraph.java @@ -4,223 +4,227 @@ 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); - - // 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); - } + 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); + } + + 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); + } - g.setColor(Color.green); - g.setStroke(new BasicStroke(3)); + // 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; + } - BoardHistoryNode topOfVariation = null; - int numMoves = 0; - if (!BoardHistoryList.isMainTrunk(curMove)) + 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; + } { - // 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); + // 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); } - - // 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 (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 (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; + if (lastNodeOk) g.setColor(Color.green); + else g.setColor(Color.blue.darker()); - 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 index 190d4b54d..7b909dcd5 100644 --- a/src/main/java/featurecat/lizzie/plugin/IPlugin.java +++ b/src/main/java/featurecat/lizzie/plugin/IPlugin.java @@ -1,6 +1,5 @@ package featurecat.lizzie.plugin; - import java.awt.Graphics2D; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; @@ -11,52 +10,37 @@ 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 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(); - public default void onMouseMoved(MouseEvent e) { + return plugin; + } - } + public default void onInit() throws IOException {} - public default void onKeyPressed(KeyEvent e) { + public default void onMousePressed(MouseEvent e) {} - } + public default void onMouseReleased(MouseEvent e) {} - public default void onKeyReleased(KeyEvent e) { + public default void onMouseMoved(MouseEvent e) {} - } + public default void onKeyPressed(KeyEvent e) {} - public default boolean onDraw(Graphics2D g) { - return false; - } + public default void onKeyReleased(KeyEvent e) {} - public default void onShutdown() throws IOException { + public default boolean onDraw(Graphics2D g) { + return false; + } - } + public default void onShutdown() throws IOException {} - public default void onSgfLoaded() { + public default void onSgfLoaded() {} - } + public String getName(); - public String getName(); - public String getVersion(); + public String getVersion(); } diff --git a/src/main/java/featurecat/lizzie/plugin/PluginManager.java b/src/main/java/featurecat/lizzie/plugin/PluginManager.java index 426a36a77..c9116d989 100644 --- a/src/main/java/featurecat/lizzie/plugin/PluginManager.java +++ b/src/main/java/featurecat/lizzie/plugin/PluginManager.java @@ -1,100 +1,100 @@ package featurecat.lizzie.plugin; +import featurecat.lizzie.Lizzie; 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; +import java.util.HashSet; public class PluginManager { - public static HashSet plugins; + 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(); + 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(); - } - } + 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 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 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 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 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 onKeyReleased(KeyEvent e) { + for (IPlugin plugin : plugins) { + plugin.onKeyReleased(e); } + } + + public static void onShutdown() { - public static void onShutdown(){ - - for (IPlugin plugin : plugins) { - try {plugin.onShutdown(); - } catch(IOException e) { - e.printStackTrace(); - } - } + 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 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(); + 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 999e21e13..2e4783a70 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -4,1241 +4,1279 @@ import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.analysis.LeelazListener; import featurecat.lizzie.analysis.MoveData; -import featurecat.lizzie.rules.SGFParser; - import java.io.IOException; -import javax.swing.*; import java.util.ArrayDeque; import java.util.Deque; -import java.util.Queue; import java.util.List; import java.util.Map; +import java.util.Queue; +import javax.swing.*; import org.json.JSONException; public class Board implements LeelazListener { - public static 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; - - // Force refresh board - private boolean forceRefresh = false; - - public Board() { - initialize(); + public static int BOARD_SIZE = Lizzie.config.config.getJSONObject("ui").optInt("board-size", 19); + private static final String alphabet = "ABCDEFGHJKLMNOPQRST"; + + private BoardHistoryList history; + private Stone[] capturedStones; + + private boolean scoreMode; + + private boolean analysisMode = false; + private int playoutsAnalysis = 100; + + // Force refresh board + private boolean forceRefresh = false; + + public Board() { + initialize(); + } + + /** 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; + + boolean blackToPlay = true; + int[] lastMove = null; + + capturedStones = null; + scoreMode = false; + + history = + new BoardHistoryList( + new BoardData( + stones, + lastMove, + Stone.EMPTY, + blackToPlay, + new Zobrist(), + 0, + new int[BOARD_SIZE * BOARD_SIZE], + 0, + 0, + 50, + 0)); + } + + /** + * 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; } - - /** - * 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; - - boolean blackToPlay = true; - int[] lastMove = null; - - capturedStones = null; - scoreMode = false; - - history = new BoardHistoryList(new BoardData(stones, lastMove, Stone.EMPTY, blackToPlay, - new Zobrist(), 0, new int[BOARD_SIZE * BOARD_SIZE], 0, 0, 50, 0)); + // 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; + } + + /** + * Open board again when the SZ property is setup by sgf + * + * @param size + */ + public void reopen(int size) { + size = (size == 13 || size == 9) ? size : 19; + if (size != BOARD_SIZE) { + BOARD_SIZE = size; + initialize(); + forceRefresh = true; } - - /** - * 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; + } + + public boolean isForceRefresh() { + return forceRefresh; + } + + public void setForceRefresh(boolean forceRefresh) { + this.forceRefresh = forceRefresh; + } + + /** + * 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; + } } - - /** - * 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; + } + + /** + * Update the move number. Thread safe + * + * @param moveNumber the move number of stone + */ + public void moveNumber(int moveNumber) { + synchronized (this) { + BoardData data = history.getData(); + if (data != null) { + data.moveNumber = moveNumber; + if (data.lastMove != null) { + int[] moveNumberList = history.getMoveNumberList(); + moveNumberList[Board.getIndex(data.lastMove[0], data.lastMove[1])] = moveNumber; + BoardHistoryNode node = history.getCurrentHistoryNode().previous(); + while (node != null && node.numberOfChildren() <= 1) { + BoardData nodeData = node.getData(); + if (nodeData != null + && nodeData.lastMove != null + && nodeData.moveNumber >= moveNumber) { + moveNumber = (moveNumber > 1) ? moveNumber - 1 : 0; + moveNumberList[Board.getIndex(nodeData.lastMove[0], nodeData.lastMove[1])] = + moveNumber; + } + node = node.previous(); + } } - // 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; + } + + /** + * Add 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 addStone(int x, int y, Stone color) { + synchronized (this) { + if (!isValid(x, y) || history.getStones()[getIndex(x, y)] != Stone.EMPTY) return; + + Stone[] stones = history.getData().stones; + Zobrist zobrist = history.getData().zobrist; + + // set the stone at (x, y) to color + stones[getIndex(x, y)] = color; + zobrist.toggleStone(x, y, color); + + Lizzie.frame.repaint(); } - - /** - * Open board again when the SZ property is setup by sgf - * - * @param size - */ - public void reopen(int size) { - size = (size == 13 || size == 9) ? size : 19; - if (size != BOARD_SIZE) { - BOARD_SIZE = size; - initialize(); - forceRefresh = true; - } + } + + /** + * Remove 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 removeStone(int x, int y, Stone color) { + synchronized (this) { + if (!isValid(x, y) || history.getStones()[getIndex(x, y)] == Stone.EMPTY) return; + + BoardData data = history.getData(); + Stone[] stones = data.stones; + Zobrist zobrist = data.zobrist; + + // set the stone at (x, y) to empty + Stone oriColor = stones[getIndex(x, y)]; + stones[getIndex(x, y)] = Stone.EMPTY; + zobrist.toggleStone(x, y, oriColor); + data.moveNumber = 0; + data.moveNumberList[Board.getIndex(x, y)] = 0; + + Lizzie.frame.repaint(); } - - public boolean isForceRefresh() { - return forceRefresh; + } + + /** + * Add a key and value to node + * + * @param key + * @param value + */ + public void addNodeProperty(String key, String value) { + synchronized (this) { + if (history.getData() != null) { + history.getData().addProperty(key, value); + } } - - public void setForceRefresh(boolean forceRefresh) { - this.forceRefresh = forceRefresh; + } + + /** + * Add a keys and values to node + * + * @param map + */ + public void addNodeProperties(Map properties) { + synchronized (this) { + if (history.getData() != null) { + history.getData().addProperties(properties); + } } - - /** - * 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; - } - } + } + + /** + * 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(); } - - /** - * Update the move number. Thread safe - * @param moveNumber the move number of stone - */ - public void moveNumber(int moveNumber) { - synchronized (this) { - BoardData data = history.getData(); - if (data != null) { - data.moveNumber = moveNumber; - if (data.lastMove != null) { - int[] moveNumberList = history.getMoveNumberList(); - moveNumberList[Board.getIndex(data.lastMove[0], data.lastMove[1])] = moveNumber; - BoardHistoryNode node = history.getCurrentHistoryNode().previous(); - while (node != null && node.numberOfChildren() <= 1) { - BoardData nodeData = node.getData(); - if (nodeData != null && nodeData.lastMove != null && nodeData.moveNumber >= moveNumber) { - moveNumber = (moveNumber > 1) ? moveNumber - 1 : 0; - moveNumberList[Board.getIndex(nodeData.lastMove[0], nodeData.lastMove[1])] = moveNumber; - } - node = node.previous(); - } - } - } + } + + /** 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) { + place(x, y, color, false); + } + + /** + * 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 + * @param newBranch add a new branch + */ + public void place(int x, int y, Stone color, boolean newBranch) { + 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 && !newBranch)) + 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)); } - } - - /** - * Add 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 addStone(int x, int y, Stone color) { - synchronized (this) { - - if (!isValid(x, y) || history.getStones()[getIndex(x, y)] != Stone.EMPTY) - return; - - Stone[] stones = history.getData().stones; - Zobrist zobrist = history.getData().zobrist; - - // set the stone at (x, y) to color - stones[getIndex(x, y)] = color; - zobrist.toggleStone(x, y, color); - - 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, newBranch); + + Lizzie.frame.repaint(); } - - - /** - * Remove 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 removeStone(int x, int y, Stone color) { - synchronized (this) { - - if (!isValid(x, y) || history.getStones()[getIndex(x, y)] == Stone.EMPTY) - return; - - BoardData data = history.getData(); - Stone[] stones = data.stones; - Zobrist zobrist = data.zobrist; - - // set the stone at (x, y) to empty - Stone oriColor = stones[getIndex(x, y)]; - stones[getIndex(x, y)] = Stone.EMPTY; - zobrist.toggleStone(x, y, oriColor); - data.moveNumber = 0; - data.moveNumberList[Board.getIndex(x, y)] = 0; - - 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. 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; } - /** - * Add a key and value to node - * - * @param key - * @param value - */ - public void addNodeProperty(String key, String value) { - synchronized (this) { - - if (history.getData() != null) { - history.getData().addProperty(key, value); - } - } + 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; } - /** - * Add a keys and values to node - * - * @param map - */ - public void addNodeProperties(Map properties) { - synchronized (this) { - - if (history.getData() != null) { - history.getData().addProperties(properties); - } + // 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; } + } - /** - * 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")); + public boolean goToMoveNumber(int moveNumber) { + return goToMoveNumberHelper(moveNumber, false); + } - // update history with pass - history.addOrGoto(newState); + public boolean goToMoveNumberWithinBranch(int moveNumber) { + return goToMoveNumberHelper(moveNumber, true); + } - Lizzie.frame.repaint(); - } - } - - /** - * overloaded method for pass(), chooses color in an alternating pattern - */ - public void pass() { - pass(history.isBlacksTurn() ? Stone.BLACK : Stone.WHITE); + 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); } - - /** - * 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) { - place(x, y, color, false); - } - - /** - * 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 - * @param newBranch add a new branch - */ - public void place(int x, int y, Stone color, boolean newBranch) { - 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 && !newBranch)) - 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, newBranch); - - Lizzie.frame.repaint(); + 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; } + } + if (!(delta > 0 ? nextMove() : previousMove())) { + break; + } + moved = true; } - - /** - * 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; + 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])); } - - 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; + Lizzie.frame.repaint(); + return true; + } + return false; } - - /** - * 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; + } + + /* + * 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; + } } - - // 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; + 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(); } - } - - public boolean goToMoveNumber(int moveNumber) { - return goToMoveNumberHelper(moveNumber, false); - } - - 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); - } - 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; - } + } + + // 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 (!(delta > 0 ? nextMove() : previousMove())) { - break; - } - moved = true; + } + if (isDeepEnough) break; } - 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])); - } - Lizzie.frame.repaint(); - return true; + } + // 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 false; + } } + startIdx = 0; + curNode = history.getCurrentHistoryNode(); + } + 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(); - - } - } - - // 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(); + } + + /* + * 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; } - return true; + } + 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 moveBranchUp() { + synchronized (this) { + history.getCurrentHistoryNode().topOfBranch().moveUp(); } + } - public void moveBranchDown() { - synchronized (this) { - history.getCurrentHistoryNode().topOfBranch().moveDown(); - } + public void moveBranchDown() { + synchronized (this) { + history.getCurrentHistoryNode().topOfBranch().moveDown(); } - - 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 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 deleteBranch() { - int originalMoveNumber = history.getMoveNumber(); - undoToChildOfPreviousWithVariation(); - int moveNumberBeforeOperation = history.getMoveNumber(); - deleteMove(); - boolean canceled = (history.getMoveNumber() == moveNumberBeforeOperation); - if (canceled) { - goToMoveNumber(originalMoveNumber); - } + } + + public void deleteBranch() { + int originalMoveNumber = history.getMoveNumber(); + undoToChildOfPreviousWithVariation(); + int moveNumberBeforeOperation = history.getMoveNumber(); + deleteMove(); + boolean canceled = (history.getMoveNumber() == moveNumberBeforeOperation); + if (canceled) { + goToMoveNumber(originalMoveNumber); } - - public BoardData getData() { - return history.getData(); + } + + 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; } - - public BoardHistoryList getHistory() { - return history; + } + + 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; } - - /** - * Clears all history and starts over from empty board. - */ - public void clear() { - Lizzie.leelaz.sendCommand("clear_board"); - Lizzie.frame.resetTitle(); - initialize(); + 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; } - - /** - * 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; - } - } - - 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(); - } - - /* - * 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; + x++; + } } - - /* - * 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; } - } - - 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(); - } - } + if (sum >= playoutsAnalysis) { + nextMove(); } + } } - - public void autosave() { - if (autosaveToMemory()) { - try { - Lizzie.config.persist(); - } catch (IOException err) {} - } + } + + public void autosave() { + if (autosaveToMemory()) { + try { + Lizzie.config.persist(); + } catch (IOException err) { + } } + } - 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 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; } - - public void resumePreviousGame() { - try { - SGFParser.loadFromString(Lizzie.config.persisted.getString("autosave")); - while (nextMove()) ; - } catch (JSONException err) {} + 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 af5421387..6bc42c018 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardData.java +++ b/src/main/java/featurecat/lizzie/rules/BoardData.java @@ -4,111 +4,121 @@ import java.util.Map; 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; - - // Comment in the Sgf move - public String comment; - - // Node properties - private final Map properties = new HashMap(); - - 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; - } - - /** - * Add a key and value - * - * @param key - * @param value - */ - public void addProperty(String key, String value) { - SGFParser.addProperty(properties, key, value); - } - - /** - * Get a value with key - * - * @param key - * @return - */ - public String getProperty(String key) { - return properties.get(key); - } - - /** - * Get a value with key, or the default if there is no such key - * - * @param key - * @param defaultValue - * @return - */ - public String optProperty(String key, String defaultValue) { - return SGFParser.optProperty(properties, key, defaultValue); - } - - /** - * Get the properties - * - * @return - */ - public Map getProperties() { - return properties; - } - - - /** - * Add the properties - * - * @return - */ - public void addProperties(Map addProps) { - SGFParser.addProperties(this.properties, addProps); - } - - /** - * Add the properties from string - * - * @return - */ - public void addProperties(String propsStr) { - SGFParser.addProperties(properties, propsStr); - } - - /** - * Get properties string - * - * @return - */ - public String propertiesString() { - return SGFParser.propertiesString(properties); - } + 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; + + // Node properties + private final Map properties = new HashMap(); + + 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; + } + + /** + * Add a key and value + * + * @param key + * @param value + */ + public void addProperty(String key, String value) { + SGFParser.addProperty(properties, key, value); + } + + /** + * Get a value with key + * + * @param key + * @return + */ + public String getProperty(String key) { + return properties.get(key); + } + + /** + * Get a value with key, or the default if there is no such key + * + * @param key + * @param defaultValue + * @return + */ + public String optProperty(String key, String defaultValue) { + return SGFParser.optProperty(properties, key, defaultValue); + } + + /** + * Get the properties + * + * @return + */ + public Map getProperties() { + return properties; + } + + /** + * Add the properties + * + * @return + */ + public void addProperties(Map addProps) { + SGFParser.addProperties(this.properties, addProps); + } + + /** + * Add the properties from string + * + * @return + */ + public void addProperties(String propsStr) { + SGFParser.addProperties(properties, propsStr); + } + + /** + * Get properties string + * + * @return + */ + public String propertiesString() { + return SGFParser.propertiesString(properties); + } } diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index 7fbb2c31b..fb2347f2e 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -1,370 +1,335 @@ 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; - } - - 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) { - addOrGoto(data, false); - } - - public void addOrGoto(BoardData data, boolean newBranch) { - head = head.addOrGoto(data, newBranch); - } - - /** - * 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 right, returns the node stored there - * - * @return the next node, null if there is no next node - */ - public BoardHistoryNode nextNode() { - if (head.next() == null) - return null; - else - head = head.next(); - - return head; - } - - /** - * 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) { + addOrGoto(data, false); + } + + public void addOrGoto(BoardData data, boolean newBranch) { + head = head.addOrGoto(data, newBranch); + } + + /** + * 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 right, returns the node stored there + * + * @return the next node, null if there is no next node + */ + public BoardHistoryNode nextNode() { + if (head.next() == null) return null; + else head = head.next(); + + return head; + } + + /** + * 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 8c346b807..45aa25ff8 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java @@ -1,190 +1,184 @@ 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) { - return addOrGoto(data, false); - } - - /** - * 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 - * @param newBranch add a new branch - * @return the node that was just set - */ - public BoardHistoryNode addOrGoto(BoardData data, boolean newBranch) { - // 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; -// } -// } -// } - if (!newBranch) { - 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); - } - } + 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) { + return addOrGoto(data, false); + } + + /** + * 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 + * @param newBranch add a new branch + * @return the node that was just set + */ + public BoardHistoryNode addOrGoto(BoardData data, boolean newBranch) { + // 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; + // } + // } + // } + if (!newBranch) { + 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; + } } + BoardHistoryNode node = new BoardHistoryNode(data); + // Add node + nexts.add(node); + node.previous = this; - /** - * @return data stored on this node - */ - public BoardData getData() { - return data; - } + return node; + } - /** - * @return nexts for display - */ - public List getNexts() { - return nexts; - } + /** @return data stored on this node */ + public BoardData getData() { + return data; + } - public BoardHistoryNode previous() { - return previous; - } + /** @return nexts for display */ + public List getNexts() { + return nexts; + } - public BoardHistoryNode next() { - if (nexts.size() == 0) { - return null; - } else { - return nexts.get(0); - } - } + public BoardHistoryNode previous() { + return previous; + } - public BoardHistoryNode topOfBranch() { - BoardHistoryNode top = this; - while (top.previous != null && top.previous.nexts.size() == 1) { - top = top.previous; - } - return top; + public BoardHistoryNode next() { + if (nexts.size() == 0) { + return null; + } else { + return nexts.get(0); } + } - public int numberOfChildren() { - if (nexts == null) { - return 0; - } else { - return nexts.size(); - } + public BoardHistoryNode topOfBranch() { + BoardHistoryNode top = this; + while (top.previous != null && top.previous.nexts.size() == 1) { + top = top.previous; } + return top; + } - public boolean isFirstChild() { - return (previous != null) && previous.next() == this; + public int numberOfChildren() { + if (nexts == null) { + return 0; + } else { + return nexts.size(); } + } - public BoardHistoryNode getVariation(int idx) { - if (nexts.size() <= idx) { - return null; - } else { - return nexts.get(idx); - } + 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 moveUp() { + if (previous != null) { + previous.moveChildUp(this); } + } - public void moveDown() { - if (previous != null) { - previous.moveChildDown(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 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 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); - } + public void deleteChild(int idx) { + if (idx < numberOfChildren()) { + nexts.remove(idx); } + } } diff --git a/src/main/java/featurecat/lizzie/rules/GIBParser.java b/src/main/java/featurecat/lizzie/rules/GIBParser.java index 7961a5230..cead794c7 100644 --- a/src/main/java/featurecat/lizzie/rules/GIBParser.java +++ b/src/main/java/featurecat/lizzie/rules/GIBParser.java @@ -2,108 +2,107 @@ 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); + PluginManager.onSgfLoaded(); + 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 f8390dc81..2b3a19004 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -1,571 +1,579 @@ package featurecat.lizzie.rules; -import java.util.HashMap; -import java.util.Map; -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"); - - private static final String[] listProps = new String[] { "LB", "CR", "SQ", "MA", "TR", "AB", "AW", "AE"}; - private static final String[] markupProps = new String[] { "LB", "CR", "SQ", "MA", "TR"}; - - 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; - } + private static final String[] listProps = + new String[] {"LB", "CR", "SQ", "MA", "TR", "AB", "AW", "AE"}; + private static final String[] markupProps = new String[] {"LB", "CR", "SQ", "MA", "TR"}; - 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 load(String filename) throws IOException { + // Clear the board + Lizzie.board.clear(); - boolean returnValue = parse(value); - PluginManager.onSgfLoaded(); - return returnValue; + File file = new File(filename); + if (!file.exists() || !file.canRead()) { + return false; } - public static boolean loadFromString(String sgfString) { - // Clear the board - Lizzie.board.clear(); - - return parse(sgfString); + FileInputStream fp = new FileInputStream(file); + InputStreamReader reader = new InputStreamReader(fp); + StringBuilder builder = new StringBuilder(); + while (reader.ready()) { + builder.append((char) reader.read()); } - - 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; + reader.close(); + fp.close(); + String value = builder.toString(); + if (value.isEmpty()) { + return false; } - 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; - } - - // Determine the SZ property - Pattern szPattern = Pattern.compile("(?s).*?SZ\\[(\\d+)\\](?s).*"); - Matcher szMatcher = szPattern.matcher(value); - if (szMatcher.matches()) { - Lizzie.board.reopen(Integer.parseInt(szMatcher.group(1))); - } else { - Lizzie.board.reopen(19); - } + boolean returnValue = parse(value); + PluginManager.onSgfLoaded(); + return returnValue; + } + + public static boolean loadFromString(String sgfString) { + // Clear the board + Lizzie.board.clear(); + + return parse(sgfString); + } + + 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; + } + + 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; - // Game properties - Map gameProperties = new HashMap(); - boolean inTag = false, isMultiGo = false, escaping = false, moveStart = false, addPassForAwAb = true; - 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; - } + // Determine the SZ property + Pattern szPattern = Pattern.compile("(?s).*?SZ\\[(\\d+)\\](?s).*"); + Matcher szMatcher = szPattern.matcher(value); + if (szMatcher.matches()) { + Lizzie.board.reopen(Integer.parseInt(szMatcher.group(1))); + } else { + Lizzie.board.reopen(19); + } - String blackPlayer = "", whitePlayer = ""; + 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; + // Game properties + Map gameProperties = new HashMap(); + boolean inTag = false, + isMultiGo = false, + escaping = false, + moveStart = false, + addPassForAwAb = true; + 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; + } - // 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; + 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); } - 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); - } - } - 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(); - } - } - 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") || tag.equals("W")) { - moveStart = true; - addPassForAwAb = true; - int[] move = convertSgfPosToCoord(tagContent); - // Save the step count - subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1); - if (move == null) { - Lizzie.board.pass(tag.equals("B") ? Stone.BLACK : Stone.WHITE); - } else { - Lizzie.board.place(move[0], move[1], tag.equals("B") ? Stone.BLACK : Stone.WHITE, subTreeStepMap.get(subTreeDepth) == 1); - } - } else if (tag.equals("C")) { - // Support comment - if (!moveStart) { - awabComment = tagContent; - } else { - Lizzie.board.comment(tagContent); - } - } else if (tag.equals("AB") || tag.equals("AW")) { - int[] move = convertSgfPosToCoord(tagContent); - if (moveStart) { - // add to node properties - Lizzie.board.addNodeProperty(tag, tagContent); - if (addPassForAwAb) { - Lizzie.board.pass(tag.equals("AB") ? Stone.BLACK : Stone.WHITE); - addPassForAwAb = false; - } - if (move != null) { - Lizzie.board.addStone(move[0], move[1], tag.equals("AB") ? Stone.BLACK : Stone.WHITE); - } - } else { - if (move == null) { - Lizzie.board.pass(tag.equals("AB") ? Stone.BLACK : Stone.WHITE); - } else { - Lizzie.board.place(move[0], move[1], tag.equals("AB") ? Stone.BLACK : 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(); - } - } else { - if (moveStart) { - // Other SGF node properties - Lizzie.board.addNodeProperty(tag, tagContent); - if ("MN".equals(tag)) { - Lizzie.board.moveNumber(Integer.parseInt(tagContent)); - } else if ("AE".equals(tag)) { - // remove a stone - if (addPassForAwAb) { - Lizzie.board.pass(tag.equals("AB") ? Stone.BLACK : Stone.WHITE); - addPassForAwAb = false; - } - int[] move = convertSgfPosToCoord(tagContent); - if (move != null) { - Lizzie.board.removeStone(move[0], move[1], tag.equals("AB") ? Stone.BLACK : Stone.WHITE); - } - } - } else { - addProperty(gameProperties, tag, 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); - } - } + } + 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(); + } } - } - - Lizzie.frame.setPlayers(whitePlayer, blackPlayer); + 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") || tag.equals("W")) { + moveStart = true; + addPassForAwAb = true; + int[] move = convertSgfPosToCoord(tagContent); + // Save the step count + subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1); + if (move == null) { + Lizzie.board.pass(tag.equals("B") ? Stone.BLACK : Stone.WHITE); + } else { + Lizzie.board.place( + move[0], + move[1], + tag.equals("B") ? Stone.BLACK : Stone.WHITE, + subTreeStepMap.get(subTreeDepth) == 1); + } + } else if (tag.equals("C")) { + // Support comment + if (!moveStart) { + awabComment = tagContent; + } else { + Lizzie.board.comment(tagContent); + } + } else if (tag.equals("AB") || tag.equals("AW")) { + int[] move = convertSgfPosToCoord(tagContent); + if (moveStart) { + // add to node properties + Lizzie.board.addNodeProperty(tag, tagContent); + if (addPassForAwAb) { + Lizzie.board.pass(tag.equals("AB") ? Stone.BLACK : Stone.WHITE); + addPassForAwAb = false; + } + if (move != null) { + Lizzie.board.addStone( + move[0], move[1], tag.equals("AB") ? Stone.BLACK : Stone.WHITE); + } + } else { + if (move == null) { + Lizzie.board.pass(tag.equals("AB") ? Stone.BLACK : Stone.WHITE); + } else { + Lizzie.board.place(move[0], move[1], tag.equals("AB") ? Stone.BLACK : 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(); + } + } else { + if (moveStart) { + // Other SGF node properties + Lizzie.board.addNodeProperty(tag, tagContent); + if ("MN".equals(tag)) { + Lizzie.board.moveNumber(Integer.parseInt(tagContent)); + } else if ("AE".equals(tag)) { + // remove a stone + if (addPassForAwAb) { + Lizzie.board.pass(tag.equals("AB") ? Stone.BLACK : Stone.WHITE); + addPassForAwAb = false; + } + int[] move = convertSgfPosToCoord(tagContent); + if (move != null) { + Lizzie.board.removeStone( + move[0], move[1], tag.equals("AB") ? Stone.BLACK : Stone.WHITE); + } + } + } else { + addProperty(gameProperties, tag, 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); + } + } + } + } - // Rewind to game start - while (Lizzie.board.previousMove()) ; + Lizzie.frame.setPlayers(whitePlayer, blackPlayer); - // Set AW/AB Comment - if (awabComment != null) { - Lizzie.board.comment(awabComment); - } - if (gameProperties.size() > 0) { - Lizzie.board.addNodeProperties(gameProperties); - } + // 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(); - } + if (gameProperties.size() > 0) { + Lizzie.board.addNodeProperties(gameProperties); } - public static void save(Board board, String filename) throws IOException { - try (Writer writer = new OutputStreamWriter(new FileOutputStream(filename))) { - saveToStream(board, writer); - } + return true; + } + + public static String saveToString() throws IOException { + try (StringWriter writer = new StringWriter()) { + saveToStream(Lizzie.board, writer); + return writer.toString(); } + } - private static void saveToStream(Board board, Writer writer) throws IOException { - // collect game info - BoardHistoryList history = board.getHistory().shallowCopy(); - 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("(;"); - StringBuilder generalProps = new StringBuilder(""); - if (handicap != 0) generalProps.append(String.format("HA[%s]", handicap)); - generalProps.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(); - - // Game properties - history.getData().addProperties(generalProps.toString()); - builder.append(history.getData().propertiesString()); - - // 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); - } + 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().shallowCopy(); + 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("(;"); + StringBuilder generalProps = new StringBuilder(""); + if (handicap != 0) generalProps.append(String.format("HA[%s]", handicap)); + generalProps.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(); + + // Game properties + history.getData().addProperties(generalProps.toString()); + builder.append(history.getData().propertiesString()); + + // 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)); } - - // The AW/AB Comment - if (history.getData().comment != null) { - builder.append(String.format("C[%s]", history.getData().comment)); + } + } 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. + // The AW/AB Comment + if (history.getData().comment != null) { + builder.append(String.format("C[%s]", history.getData().comment)); + } - // Write variation tree - builder.append(generateNode(board, history.getCurrentHistoryNode())); + // replay moves, and convert them to tags. + // * format: ";B[xy]" or ";W[xy]" + // * with 'xy' = coordinates ; or 'tt' for pass. - // close file - builder.append(')'); - writer.append(builder.toString()); - } + // Write variation tree + builder.append(generateNode(board, history.getCurrentHistoryNode())); - /** - * Generate node with variations - */ - private static String generateNode(Board board, BoardHistoryNode node) throws IOException { - StringBuilder builder = new StringBuilder(""); + // close file + builder.append(')'); + writer.append(builder.toString()); + } - if (node != null) { + /** Generate node with variations */ + private static String generateNode(Board board, BoardHistoryNode node) throws IOException { + StringBuilder builder = new StringBuilder(""); - BoardData data = node.getData(); - String stone = ""; - if (Stone.BLACK.equals(data.lastMoveColor) || Stone.WHITE.equals(data.lastMoveColor)) { + if (node != null) { - if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; - else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; + BoardData data = node.getData(); + String stone = ""; + if (Stone.BLACK.equals(data.lastMoveColor) || Stone.WHITE.equals(data.lastMoveColor)) { - char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); - char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B"; + else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W"; - builder.append(String.format(";%s[%c%c]", stone, x, y)); + char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); + char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); - // Node properties - builder.append(data.propertiesString()); + 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)); - } - } + // Node properties + builder.append(data.propertiesString()); - 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(); - } + // Write the comment + if (data.comment != null) { + builder.append(String.format("C[%s]", data.comment)); } - + } + + 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(); + } } - public static boolean isListProperty(String key) { - for(String k : listProps) { - if(k.equals(key)) { - return true; - } - } - return false; - } + return builder.toString(); + } - public static boolean isMarkupProperty(String key) { - for(String k : markupProps) { - if(k.equals(key)) { - return true; - } - } - return false; + public static boolean isListProperty(String key) { + for (String k : listProps) { + if (k.equals(key)) { + return true; + } } + return false; + } - /** - * Get a value with key, or the default if there is no such key - * - * @param key - * @param defaultValue - * @return - */ - public static String optProperty(Map props, String key, String defaultValue) { - return props.getOrDefault(key, defaultValue); + public static boolean isMarkupProperty(String key) { + for (String k : markupProps) { + if (k.equals(key)) { + return true; + } } - - /** - * Add a key and value - * - * @param key - * @param value - */ - public static void addProperty(Map props, String key, String value) { - if (SGFParser.isListProperty(key)) { - // Label and add/remove stones - props.merge(key, value, (old, val) -> old + "," + val); - } else { - props.put(key, value); - } + return false; + } + + /** + * Get a value with key, or the default if there is no such key + * + * @param key + * @param defaultValue + * @return + */ + public static String optProperty(Map props, String key, String defaultValue) { + return props.getOrDefault(key, defaultValue); + } + + /** + * Add a key and value + * + * @param key + * @param value + */ + public static void addProperty(Map props, String key, String value) { + if (SGFParser.isListProperty(key)) { + // Label and add/remove stones + props.merge(key, value, (old, val) -> old + "," + val); + } else { + props.put(key, value); } - - /** - * Add the properties - * - * @return - */ - public static void addProperties(Map props, Map addProps) { - if (addProps != null && addProps.size() > 0) { - addProps.forEach((key, value) -> addProperty(props, key, value)); - } + } + + /** + * Add the properties + * + * @return + */ + public static void addProperties(Map props, Map addProps) { + if (addProps != null && addProps.size() > 0) { + addProps.forEach((key, value) -> addProperty(props, key, value)); } - - /** - * Add the properties from string - * - * @return - */ - public static void addProperties(Map props, String propsStr) { - if (propsStr != null) { - boolean inTag = false, escaping = false; - String tag = null; - StringBuilder tagBuilder = new StringBuilder(); - StringBuilder tagContentBuilder = new StringBuilder(); - - for (int i = 0; i < propsStr.length(); i++) { - char c = propsStr.charAt(i); - if (escaping) { - tagContentBuilder.append(c); - escaping = false; - continue; - } - switch (c) { - case '(': - if (inTag) { - if (i > 0) { - tagContentBuilder.append(c); - } - } - break; - case ')': - if (inTag) { - tagContentBuilder.append(c); - } - break; - case '[': - inTag = true; - String tagTemp = tagBuilder.toString(); - if (!tagTemp.isEmpty()) { - tag = tagTemp.replaceAll("[a-z]", ""); - } - tagContentBuilder = new StringBuilder(); - break; - case ']': - inTag = false; - tagBuilder = new StringBuilder(); - addProperty(props, tag, tagContentBuilder.toString()); - break; - case ';': - break; - default: - if (inTag) { - if (c == '\\') { - escaping = true; - continue; - } - tagContentBuilder.append(c); - } else { - if (c != '\n' && c != '\r' && c != '\t' && c != ' ') { - tagBuilder.append(c); - } - } - } + } + + /** + * Add the properties from string + * + * @return + */ + public static void addProperties(Map props, String propsStr) { + if (propsStr != null) { + boolean inTag = false, escaping = false; + String tag = null; + StringBuilder tagBuilder = new StringBuilder(); + StringBuilder tagContentBuilder = new StringBuilder(); + + for (int i = 0; i < propsStr.length(); i++) { + char c = propsStr.charAt(i); + if (escaping) { + tagContentBuilder.append(c); + escaping = false; + continue; + } + switch (c) { + case '(': + if (inTag) { + if (i > 0) { + tagContentBuilder.append(c); + } + } + break; + case ')': + if (inTag) { + tagContentBuilder.append(c); + } + break; + case '[': + inTag = true; + String tagTemp = tagBuilder.toString(); + if (!tagTemp.isEmpty()) { + tag = tagTemp.replaceAll("[a-z]", ""); + } + tagContentBuilder = new StringBuilder(); + break; + case ']': + inTag = false; + tagBuilder = new StringBuilder(); + addProperty(props, tag, tagContentBuilder.toString()); + break; + case ';': + break; + default: + if (inTag) { + if (c == '\\') { + escaping = true; + continue; + } + tagContentBuilder.append(c); + } else { + if (c != '\n' && c != '\r' && c != '\t' && c != ' ') { + tagBuilder.append(c); + } } } + } } - - /** - * Get properties string - * - * @return - */ - public static String propertiesString(Map props) { - StringBuilder sb = new StringBuilder(); - if (props != null) { - props.forEach((key, value) -> sb.append(nodeString(key, value))); - } - return sb.toString(); + } + + /** + * Get properties string + * + * @return + */ + public static String propertiesString(Map props) { + StringBuilder sb = new StringBuilder(); + if (props != null) { + props.forEach((key, value) -> sb.append(nodeString(key, value))); } - - /** - * Get node string - * - * @param key - * @param value - * @return - */ - public static String nodeString(String key, String value) { - StringBuilder sb = new StringBuilder(); - if (SGFParser.isListProperty(key)) { - // Label and add/remove stones - sb.append(key); - String[] vals = value.split(","); - for (String val : vals) { - sb.append("[").append(val).append("]"); - } - } else { - sb.append(key).append("[").append(value).append("]"); - } - return sb.toString(); + return sb.toString(); + } + + /** + * Get node string + * + * @param key + * @param value + * @return + */ + public static String nodeString(String key, String value) { + StringBuilder sb = new StringBuilder(); + if (SGFParser.isListProperty(key)) { + // Label and add/remove stones + sb.append(key); + String[] vals = value.split(","); + for (String val : vals) { + sb.append("[").append(val).append("]"); + } + } else { + sb.append(key).append("[").append(value).append("]"); } + return sb.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 index e6ea85ccd..4ee055537 100644 --- a/src/main/java/featurecat/lizzie/theme/DefaultTheme.java +++ b/src/main/java/featurecat/lizzie/theme/DefaultTheme.java @@ -1,66 +1,61 @@ package featurecat.lizzie.theme; import java.awt.image.BufferedImage; -import java.io.File; import java.io.IOException; - import javax.imageio.ImageIO; - -/** - * DefaultTheme - */ +/** 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(getClass().getResourceAsStream("/assets/black0.png")); - } catch (IOException e) { - e.printStackTrace(); - } - } - return blackStoneCached; + 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(getClass().getResourceAsStream("/assets/black0.png")); + } catch (IOException e) { + e.printStackTrace(); + } } - - @Override - public BufferedImage getWhiteStone(int[] position) { - if (whiteStoneCached == null) { - try { - whiteStoneCached = ImageIO.read(getClass().getResourceAsStream("/assets/white0.png")); - } catch (IOException e) { - e.printStackTrace(); - } - } - return whiteStoneCached; + return blackStoneCached; + } + + @Override + public BufferedImage getWhiteStone(int[] position) { + if (whiteStoneCached == null) { + try { + whiteStoneCached = ImageIO.read(getClass().getResourceAsStream("/assets/white0.png")); + } catch (IOException e) { + e.printStackTrace(); + } } - - @Override - public BufferedImage getBoard() { - if (boardCached == null) { - try { - boardCached = ImageIO.read(getClass().getResourceAsStream("/assets/board.png")); - } catch (IOException e) { - e.printStackTrace(); - } - } - return boardCached; + return whiteStoneCached; + } + + @Override + public BufferedImage getBoard() { + if (boardCached == null) { + try { + boardCached = ImageIO.read(getClass().getResourceAsStream("/assets/board.png")); + } catch (IOException e) { + e.printStackTrace(); + } } - - @Override - public BufferedImage getBackground() { - if (backgroundCached == null) { - try { - backgroundCached = ImageIO.read(getClass().getResourceAsStream("/assets/background.jpg")); - } catch (IOException e) { - e.printStackTrace(); - } - } - return backgroundCached; + return boardCached; + } + + @Override + public BufferedImage getBackground() { + if (backgroundCached == null) { + try { + backgroundCached = ImageIO.read(getClass().getResourceAsStream("/assets/background.jpg")); + } catch (IOException e) { + e.printStackTrace(); + } } -} \ No newline at end of file + return backgroundCached; + } +} diff --git a/src/main/java/featurecat/lizzie/theme/ITheme.java b/src/main/java/featurecat/lizzie/theme/ITheme.java index e820a4e44..c98c57963 100644 --- a/src/main/java/featurecat/lizzie/theme/ITheme.java +++ b/src/main/java/featurecat/lizzie/theme/ITheme.java @@ -6,46 +6,45 @@ import java.net.URLClassLoader; import java.util.ArrayList; -/** - * ITheme - */ +/** 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) { + 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(); + 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); + // 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 getWhiteStone(int[] position); - BufferedImage getBoard(); + BufferedImage getBoard(); - BufferedImage getBackground(); + BufferedImage getBackground(); } diff --git a/src/test/java/common/Util.java b/src/test/java/common/Util.java index e201d57f3..2811152d5 100644 --- a/src/test/java/common/Util.java +++ b/src/test/java/common/Util.java @@ -1,149 +1,137 @@ package common; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import java.awt.Color; -import java.awt.Graphics2D; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.swing.UnsupportedLookAndFeelException; - -import org.json.JSONException; -import org.junit.Test; - -import featurecat.lizzie.Config; import featurecat.lizzie.Lizzie; -import featurecat.lizzie.analysis.Leelaz; -import featurecat.lizzie.analysis.MoveData; -import featurecat.lizzie.gui.LizzieFrame; 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; + private static ArrayList laneUsageList = new ArrayList(); - // 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); - } - } + /** + * 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); } - - 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; + if (variationNumber > 1) laneUsageList.set(lane - 1, startNode.getData().moveNumber - 1); + laneUsageList.set(lane, startNode.getData().moveNumber); - char x = data.lastMove == null ? 't' : (char) (data.lastMove[0] + 'a'); - char y = data.lastMove == null ? 't' : (char) (data.lastMove[1] + 'a'); + // At this point, lane contains the lane we should use (the main branch is in lane 0) + BoardHistoryNode cur = startNode; - 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); + // 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 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 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; } - - 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; + 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 index 1c71ec958..dbb5c5103 100644 --- a/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java +++ b/src/test/java/featurecat/lizzie/analysis/MoveDataTest.java @@ -1,52 +1,64 @@ package featurecat.lizzie.analysis; -import org.junit.Test; -import java.util.List; -import java.util.Arrays; 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() { + @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")); + 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); + 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 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 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 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 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")); + @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 index 893cb64e2..632e70c92 100644 --- a/src/test/java/featurecat/lizzie/rules/SGFParserTest.java +++ b/src/test/java/featurecat/lizzie/rules/SGFParserTest.java @@ -1,124 +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 static org.junit.Assert.assertArrayEquals; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.junit.Test; 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]); - } - + 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]); + } }