diff src/Engine.java @ 161:fb33d3796942

Rename source directory.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 21 Jun 2016 12:53:53 +0300
parents game/Engine.java@1ba6f56203f2
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Engine.java	Tue Jun 21 12:53:53 2016 +0300
@@ -0,0 +1,909 @@
+/*
+ * Ristipolku Game Engine
+ * (C) Copyright 2011 Matti 'ccr' Hämäläinen <ccr@tnsp.org>
+ */
+package game;
+
+import java.awt.*;
+import java.awt.geom.*;
+import java.awt.event.*;
+import java.awt.image.*;
+import java.awt.event.*;
+import java.awt.font.*;
+import javax.imageio.*;
+import javax.swing.*;
+import java.util.*;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.io.*;
+import game.*;
+import javax.sound.sampled.*;
+
+
+class AboutBox extends IDMWidget
+{
+    BufferedImage aboutImg;
+    boolean aboutSecret;
+
+    IDMPoint currPos, currOffs;
+    Paint textPaint;
+    Font textFont;
+    FontMetrics textMetrics;
+
+
+    public AboutBox()
+    {
+        try {
+            ResourceLoader res = new ResourceLoader("graphics/girl.jpg");
+            aboutImg = ImageIO.read(res.getStream());
+        }
+        catch (IOException e)
+        {
+        }
+
+        aboutSecret = false;
+
+        setPos(150f, 150f);
+        setSize(675f, 420f);
+    }
+
+    public void setTextFont(Font font, FontMetrics metrics)
+    {
+        textFont = font;
+        textMetrics = metrics;
+    }
+
+    public void setTextPaint(Paint paint)
+    {
+        textPaint = paint;
+    }
+
+    public void setCurrPos(IDMPoint npos)
+    {
+        currPos = npos;
+        currOffs = new IDMPoint(0, 0);
+    }
+
+    public void setCurrPos(float x, float y)
+    {
+        setCurrPos(new IDMPoint(x, y));
+    }
+
+    public void drawString(Graphics2D g, String text)
+    {
+        Paint savePaint = g.getPaint();
+        g.setPaint(textPaint);
+        g.setFont(textFont);
+
+        int i = 0;
+        while (i < text.length())
+        {
+            int p = text.indexOf("\n", i);
+            boolean linefeed;
+            String str;
+            if (p >= i)
+            {
+                str = text.substring(i, p);
+                i = p + 1;
+                linefeed = true;
+            }
+            else
+            {
+                str = text.substring(i);
+                i += str.length();
+                linefeed = false;
+            }
+
+            g.drawString(str, currPos.x + currOffs.x, currPos.y + currOffs.y);
+
+            if (linefeed)
+            {
+                currOffs.x = 0;
+                currOffs.y += textMetrics.getHeight();
+            }
+            else
+            {
+                currOffs.x += textMetrics.stringWidth(str);
+            }
+        }
+
+        g.setPaint(savePaint);
+    }
+
+    public void paint(Graphics2D g)
+    {
+        int x = getScaledX(), y = getScaledY(),
+            w = getScaledWidth(), h = getScaledHeight();
+
+        g.setPaint(new Color(0.0f, 0.0f, 0.0f, 0.7f));
+        g.fill(new RoundRectangle2D.Float(x, y, w, h, 10, 10));
+
+        setTextFont(G.fonts[3], G.metrics[3]);
+        setTextPaint(Color.white);
+
+        setCurrPos(x + 20, y + 30);
+        drawString(g, "RistiPolku (CrossPaths) v"+ G.version +"\n");
+
+        setTextFont(G.fonts[1], G.metrics[1]);
+        if (aboutSecret)
+        {
+            g.drawImage(aboutImg, x + 20, y + 55,
+                aboutImg.getWidth(), aboutImg.getHeight(), null);
+
+            setCurrPos(x + 225, y + 75);
+            drawString(g,
+                "Dedicated to my\n" +
+                "favorite woman\n" +
+                "in the world.");
+
+            setCurrPos(x + 370, y + 175);
+            drawString(g, "- Matti");
+        }
+        else
+        {
+            setTextPaint(Color.yellow);
+            drawString(g, "(c) Copyright 2011 Tecnic Software productions (TNSP)\n");
+
+            setTextPaint(Color.white);
+            drawString(g, "Programming, graphics and design by " +
+                          "Matti 'ccr' Hämäläinen.\n" +
+                          "Audio from archive.org, used non-commercially.\n \n");
+
+            setTextPaint(Color.red);
+            drawString(g, "Controls:\n");
+
+            IDMPoint old = currOffs.copy();
+
+            setTextPaint(Color.white);
+            drawString(g,
+            "Arrow keys / mouse wheel\n"+
+            "Enter / mouse click\n"+
+            "Space bar\n");
+
+            currPos.x += 330;
+            currOffs.y = old.y;
+            drawString(g,
+            "- Rotate piece\n" +
+            "- Place/lock piece\n" +
+            "- Swap piece\n");
+
+            currPos.x -= 330;
+            setTextPaint(Color.green);
+            drawString(g,
+            "\nObjective: Create a path long as possible by rotating\n"+
+            "and placing pieces. More points will be awarded for\n"+
+            "advancing the path by several segments per turn."
+            );
+        }
+    }
+
+    public boolean keyPressed(KeyEvent e)
+    {
+        if (e.getKeyCode() == KeyEvent.VK_L)
+        {
+            aboutSecret = true;
+        }
+        else
+        {
+            clicked();
+            aboutSecret = false;
+        }
+        return true;
+    }
+
+    public void clicked()
+    {
+        parent.remove(this);
+    }
+}
+
+
+class GameBoard extends IDMWidget
+{
+    static final int boardSize = 9;
+    static final int boardMiddle = 4;
+    Piece[][] board;
+    float pscale, ptime;
+
+    public boolean flagGameOver, flagBoardIsDirty;
+    int gameScore;
+
+    Piece currPiece, nextPiece;
+    int currX, currY, currPoint;
+
+    Sound sndPlaced;
+
+    private final ReentrantReadWriteLock pointLock = new ReentrantReadWriteLock();
+    private ArrayList<AnimatedPointElement> pointElems;
+
+    public GameBoard(IDMPoint pos, float ps)
+    {
+        super(pos);
+        pscale = ps;
+
+//        sndPlaced = G.smgr.getSound("sounds/placed.wav");
+
+        pointElems = new ArrayList<AnimatedPointElement>();
+
+        startNewGame();
+    }
+
+    public void startNewGame()
+    {
+        board = new Piece[boardSize][boardSize];
+        board[boardMiddle][boardMiddle] = new Piece(PieceType.START);
+
+        currX = boardMiddle;
+        currY = boardMiddle;
+        currPoint = 0;
+
+        currPiece = null;
+        nextPiece = new Piece(PieceType.ACTIVE);
+
+        flagGameOver = false;
+        flagBoardIsDirty = true;
+        pieceFinishTurn();
+        gameScore = 0;
+    }
+
+    public void paintBackPlate(Graphics2D g)
+    {
+        g.setPaint(new Color(0.0f, 0.0f, 0.0f, 0.2f));
+        g.setStroke(new BasicStroke(5.0f));
+        g.draw(new RoundRectangle2D.Float(getScaledX(), getScaledY(),
+            boardSize * pscale, boardSize * pscale,
+            pscale / 5, pscale / 5));
+    }
+
+    public void paintBoard(Graphics2D g, boolean drawCurrent)
+    {
+        for (int y = 0; y < boardSize; y++)
+        for (int x = 0; x < boardSize; x++)
+        if (board[x][y] != null)
+        {
+            if ((drawCurrent && board[x][y] == currPiece) ||
+                (!drawCurrent && board[x][y] != currPiece))
+            {
+                board[x][y].paint(g,
+                    getScaledX() + (x * pscale),
+                    getScaledY() + (y * pscale),
+                    pscale - pscale / 10);
+            }
+        }
+    }
+
+    public void paint(Graphics2D g)
+    {
+        paintBoard(g, true);
+
+        Lock read = pointLock.readLock();
+        read.lock();
+        try
+        {
+            for (AnimatedPointElement elem : pointElems)
+            {
+                elem.paint(g);
+            }
+        }
+        finally
+        {
+            read.unlock();
+        }
+
+
+        if (!flagGameOver)
+        {
+            if (nextPiece != null)
+            {
+                // Draw next piece
+                AffineTransform save = g.getTransform();
+                nextPiece.paint(g, G.screenDim.width * 0.85f - 90.0f/2.0f, G.screenDim.height * 0.43f, 90.0f);
+                g.setTransform(save);
+            }
+        }
+        else
+        {
+            // Game over text
+            String text = "Game Over!";
+            int textWidth = G.metrics[2].stringWidth(text);
+            g.setFont(G.fonts[2]);
+
+            g.setPaint(new Color(0.0f, 0.0f, 0.0f, 0.5f));
+            g.drawString(text, (G.screenDim.width - textWidth) / 2 + 5, G.screenDim.height / 2 + 5);
+
+            double f = Math.sin(ptime * 0.1) * 4.0;
+            g.setPaint(Color.white);
+            g.drawString(text, (G.screenDim.width - textWidth) / 2 + (float) f, G.screenDim.height / 2 + (float) f);
+        }
+
+        // Score
+        String text = ""+ String.format("%05d", gameScore);
+        int textWidth = G.metrics[2].stringWidth(text);
+        g.setFont(G.fonts[2]);
+        g.setPaint(Color.white);
+        g.drawString(text, (G.screenDim.width * 0.85f) - textWidth / 2, G.screenDim.height * 0.3f);
+    }
+
+    public boolean contains(float x, float y)
+    {
+        return (x >= getScaledX() &&
+                y >= getScaledY() &&
+                x < getScaledX() + boardSize * pscale &&
+                y < getScaledY() + boardSize * pscale);
+    }
+
+    public boolean isBoardDirty()
+    {
+        if (flagBoardIsDirty)
+        {
+            flagBoardIsDirty = false;
+            return true;
+        }
+        else
+            return false;
+    }
+
+    public void animate(float time)
+    {
+        if (nextPiece != null)
+        {
+            nextPiece.animate(time);
+        }
+            
+        ptime = time;
+        for (int y = 0; y < boardSize; y++)
+        for (int x = 0; x < boardSize; x++)
+        if (board[x][y] != null)
+        {
+            board[x][y].animate(time);
+            if (board[x][y] != currPiece && board[x][y].active)
+                flagBoardIsDirty = true;
+        }
+
+        Lock write = pointLock.writeLock();
+        write.lock();
+        try
+        {
+            ArrayList<AnimatedPointElement> tmp = new ArrayList<AnimatedPointElement>();
+
+            for (AnimatedPointElement elem : pointElems)
+            {
+                elem.animate(time);
+                if (elem.active)
+                    tmp.add(elem);
+            }
+
+            pointElems = tmp;
+
+            if (time % 32 == 1)
+            {
+                Random rnd = new Random();
+                pointElems.add(new AnimatedPointElement(
+                    new IDMPoint(10 + rnd.nextInt(400), 10 + rnd.nextInt(100)), "."));
+            }
+        }
+        finally
+        {
+            write.unlock();
+        }
+    }
+
+    public void pieceRotate(Piece.RotateDir dir)
+    {
+        if (currPiece != null && !flagGameOver)
+        {
+            currPiece.rotate(dir);
+        }
+    }
+
+    // Change coordinates based on the "outgoing"
+    // piece connection point.
+    private void pieceMoveTo(int point)
+    {
+        switch (point)
+        {
+            case 0: currY--; break;
+            case 1: currY--; break;
+
+            case 2: currX++; break;
+            case 3: currX++; break;
+
+            case 4: currY++; break;
+            case 5: currY++; break;
+
+            case 6: currX--; break;
+            case 7: currX--; break;
+        }
+    }
+
+    public void pieceCreateNew()
+    {
+        currPiece = nextPiece;
+        currPiece.changed();
+        nextPiece = new Piece(PieceType.ACTIVE);
+        flagBoardIsDirty = true;
+    }
+
+    public void pieceSwapCurrent()
+    {
+        if (!flagGameOver)
+        {
+            Piece tmp = currPiece;
+            currPiece = nextPiece;
+            nextPiece = tmp;
+            board[currX][currY] = currPiece;
+            currPiece.changed();
+            nextPiece.changed();
+            flagBoardIsDirty = true;
+        }
+    }
+
+    // Check one piece, set connections, find the new placement
+    // based on piece rotations etc.
+    private boolean pieceCheck(Piece piece)
+    {
+        if (piece == null)
+        {
+            // Create new piece
+            pieceCreateNew();
+            board[currX][currY] = currPiece;
+            return true;
+        }
+        else
+        if (piece.getType() == PieceType.START)
+        {
+            if (currPiece != null)
+            {
+                // Hit center starting piece, game over
+                flagGameOver = true;
+                return true;
+            }
+            else
+            {
+                // Start piece as first piece means game is starting
+                pieceMoveTo(currPoint);
+                pieceCreateNew();
+                board[currX][currY] = currPiece;
+                return true;
+            }
+        }
+
+        // Mark the current piece as locked
+        piece.setType(PieceType.LOCKED);
+
+        // Solve connection (with rotations) through the piece
+        currPoint = piece.getRotatedPoint(piece.getMatchingPoint(currPoint));
+
+        // Mark connection as active
+        piece.setConnectionState(currPoint, true);
+
+        // Solve exit point (with rotations)
+        currPoint = piece.getAntiRotatedPoint(piece.getConnection(currPoint));
+
+        // Move to next position accordingly
+        pieceMoveTo(currPoint);
+        return false;
+    }
+
+    // Finish one move/turn of the game, resolve path and find placement
+    // of the next piece, or set "game over" state if required.
+    public void pieceFinishTurn()
+    {
+        boolean finished = false;
+        int connections = 0;
+
+        if (currPiece != null)
+        {
+            G.smgr.play(sndPlaced);
+        }
+
+        while (!finished)
+        {
+            if (currX >= 0 && currX < boardSize && currY >= 0 && currY < boardSize)
+            {
+                int oldX = currX, oldY = currY;
+                connections++;
+                finished = pieceCheck(board[currX][currY]);
+
+                if (!finished)
+                {
+                    Lock write = pointLock.writeLock();
+                    write.lock();
+                    try
+                    {
+                        pointElems.add(new AnimatedPointElement(
+                            new IDMPoint(
+                            getScaledX() + ((oldX + 0.5f) * pscale),
+                            getScaledY() + ((oldY + 0.5f) * pscale)),
+                            "" + connections));
+                    }
+                    finally
+                    {
+                        write.unlock();
+                    }
+                }
+
+            }
+            else
+            {
+                // Outside of the board, game over
+                finished = true;
+                flagGameOver = true;
+            }
+        }
+
+        // Compute and add score
+        gameScore += connections * connections;
+
+        // If game over, clear the game
+        if (flagGameOver)
+        {
+            currPiece = null;
+        }
+
+        flagBoardIsDirty = true;
+    }
+
+    public boolean mouseWheelMoved(MouseWheelEvent e)
+    {
+        int notches = e.getWheelRotation();
+
+        if (notches < 0)
+            pieceRotate(Piece.RotateDir.LEFT);
+        else
+            pieceRotate(Piece.RotateDir.RIGHT);
+
+        return true;
+    }
+
+    public void clicked()
+    {
+        if (!flagGameOver)
+            pieceFinishTurn();
+    }
+
+    public boolean keyPressed(KeyEvent e)
+    {
+        if (flagGameOver)
+            return false;
+
+        switch (e.getKeyCode())
+        {
+            case KeyEvent.VK_LEFT:
+            case KeyEvent.VK_UP:
+                pieceRotate(Piece.RotateDir.LEFT);
+                return true;
+
+            case KeyEvent.VK_RIGHT:
+            case KeyEvent.VK_DOWN:
+                pieceRotate(Piece.RotateDir.RIGHT);
+                return true;
+
+            case KeyEvent.VK_ENTER:
+                pieceFinishTurn();
+                return true;
+        }
+        return false;
+    }
+}
+
+
+public class Engine extends JPanel
+                    implements Runnable, KeyListener,
+                    MouseListener, MouseWheelListener
+{
+    long startTime;
+    float gameUpdates, gameFrames;
+
+    Thread animThread;
+    boolean animEnable = false;
+
+    GameBoard lauta = null;
+    InputStream musa;
+    IDMContainer widgets;
+    AboutBox aboutBox;
+
+    public void dbg(String msg)
+    {
+        System.out.print("Engine: " + msg);
+    }
+
+    public Engine()
+    {
+        // Initialize globals
+        System.out.print("Engine() constructor\n");
+
+        // Sound system
+        G.smgr = new SoundManager(new AudioFormat(22050, 16, 1, true, false), 1);
+
+        // Load resources
+        try
+        {
+            ResourceLoader res = new ResourceLoader("graphics/board.jpg");
+            G.lautaBG = ImageIO.read(res.getStream());
+
+            try {
+                res = new ResourceLoader("graphics/font.ttf");
+
+                G.fonts = new Font[G.numFonts];
+                G.fonts[0] = Font.createFont(Font.TRUETYPE_FONT, res.getStream());
+                G.fonts[1] = G.fonts[0].deriveFont(24f);
+                G.fonts[2] = G.fonts[0].deriveFont(64f);
+                G.fonts[3] = G.fonts[0].deriveFont(32f);
+            }
+            catch (FontFormatException e)
+            {
+                dbg("Could not initialize fonts.\n");
+            }
+
+            res = new ResourceLoader("sounds/gamemusic.wav");
+            musa = res.getStream();
+        }
+        catch (IOException e)
+        {
+            JOptionPane.showMessageDialog(null,
+                e.getMessage(),
+                "Initialization error",
+                JOptionPane.ERROR_MESSAGE);
+
+            dbg(e.getMessage());
+        }
+
+        // Create IDM GUI widgets
+        widgets = new IDMContainer();
+
+        lauta = new GameBoard(new IDMPoint(95, 130), 63);
+        widgets.add(lauta);
+
+        widgets.add(new BtnSwapPiece(767f, 450f));
+        widgets.add(new BtnAbout    (767f, 550f));
+        widgets.add(new BtnNewGame  (767f, 630f));
+
+        aboutBox = new AboutBox();
+
+        // Game
+        startNewGame();
+
+        // Initialize event listeners
+        addKeyListener(this);
+        addMouseListener(this);
+        addMouseWheelListener(this);
+
+        // Start playing background music
+        G.smgr.play(musa);
+
+        // Get initial focus
+        if (!hasFocus())
+        {
+            dbg("Requesting focus.\n");
+            requestFocus();
+        }
+
+        gameUpdates = 0;
+    }
+
+    public void startNewGame()
+    {
+        gameFrames = 0;
+        startTime = new Date().getTime();
+        lauta.startNewGame();
+    }
+
+    public void paintComponent(Graphics g)
+    {
+        Graphics2D g2 = (Graphics2D) g;
+        boolean scaleChanged = false,
+                updateBoard = lauta.isBoardDirty();
+
+        // Rescale if parent component size has changed
+        Dimension dim = getSize();
+        if (G.screenDim == null || !dim.equals(G.screenDim))
+        {
+            float dw = dim.width / 1024.0f,
+                  dh = dim.height / 768.0f;
+
+            // Rescale IDM GUI widgets
+            widgets.setScale(dw, dh);
+            G.screenDim = dim;
+
+            // Rescale background image
+            // Rescale fonts
+            G.fonts[1] = G.fonts[0].deriveFont(24f * dw);
+            G.fonts[2] = G.fonts[0].deriveFont(64f * dw);
+            G.fonts[3] = G.fonts[0].deriveFont(32f * dw);
+
+            dbg("Scale changed.\n");
+            scaleChanged = true;
+            updateBoard = true;
+        }
+        
+        if (updateBoard)
+        {
+//            dbg("updateBoard()\n");
+            G.lautaBGScaled = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_RGB);
+            Graphics2D gimg = G.lautaBGScaled.createGraphics();
+            gimg.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+                                  RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+
+            gimg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                                RenderingHints.VALUE_ANTIALIAS_ON);
+
+            gimg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
+                                RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+            gimg.drawImage(G.lautaBG, 0, 0, dim.width, dim.height, null);
+            lauta.paintBackPlate(gimg);
+            lauta.paintBoard(gimg, false);
+        }
+
+        // Get font metrics against current Graphics2D context
+        if (G.metrics == null || scaleChanged)
+        {
+            G.metrics = new FontMetrics[G.numFonts];
+            for (int i = 0; i < G.numFonts; i++)
+                G.metrics[i] = g2.getFontMetrics(G.fonts[i]);
+        }
+
+        // Draw background image, pieces, widgets
+        g2.drawImage(G.lautaBGScaled, 0, 0, null);
+
+        // Use antialiasing when rendering the game elements
+        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                            RenderingHints.VALUE_ANTIALIAS_ON);
+
+        g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
+                            RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+        widgets.paint(g2);
+
+
+        // Frames per second counter
+/*
+        g2.setFont(G.fonts[1]);
+        long currTime = new Date().getTime();
+        g2.drawString("fps = "+ ((gameFrames * 1000) / (currTime - startTime)), G.screenDim.width - 120, 20);
+*/
+        gameFrames++;
+    }
+
+    public void startThreads()
+    {
+        dbg("startThreads()\n");
+        if (animThread == null)
+        {
+            animThread = new Thread(this);
+            animEnable = true;
+            animThread.start();
+        }
+    }
+
+    public void stopThreads()
+    {
+        dbg("stopThreads()\n");
+
+        // Stop animations
+        if (animThread != null)
+        {
+            animThread.interrupt();
+            animEnable = false;
+            animThread = null;
+        }
+
+        // Shut down sound manager
+        G.smgr.close();
+    }
+
+    public void mouseEntered(MouseEvent e)
+    {
+        widgets.mouseEntered(e);
+    }
+    public void mouseExited(MouseEvent e)
+    {
+        widgets.mouseExited(e);
+    }
+
+    public void mousePressed(MouseEvent e)
+    {
+        if (widgets.containsObject(aboutBox))
+            aboutBox.mousePressed(e);
+        else
+            widgets.mousePressed(e);
+    }
+
+    public void mouseReleased(MouseEvent e)
+    {
+        if (widgets.containsObject(aboutBox))
+            aboutBox.mouseReleased(e);
+        else
+            widgets.mouseReleased(e);
+    }
+
+    public void mouseClicked(MouseEvent e)
+    {
+        if (!hasFocus())
+        {
+            dbg("Requesting focus.\n");
+            requestFocus();
+        }
+    }
+
+    public void mouseWheelMoved(MouseWheelEvent e)
+    {
+        lauta.mouseWheelMoved(e);
+    }
+
+    public void keyTyped(KeyEvent e) { }
+    public void keyReleased(KeyEvent e) { }
+
+    public void keyPressed(KeyEvent e)
+    {
+        // Handle keyboard input
+        if (widgets.containsObject(aboutBox))
+            aboutBox.keyPressed(e);
+        else
+            widgets.keyPressed(e);
+    }
+
+    public void run()
+    {
+        while (animEnable)
+        {
+            // Progress game animation clock
+            gameUpdates++;
+
+            // Animate components
+            lauta.animate(gameUpdates);
+
+            // Repaint with a frame limiter
+            if (gameUpdates % 4 == 1)
+                repaint();
+
+            // Sleep for a moment
+            try {
+                Thread.sleep(10);
+            }
+            catch (InterruptedException x) {
+            }
+        }
+    }
+
+    class BtnNewGame extends IDMButton
+    {
+        public BtnNewGame(float x, float y)
+        {
+            super(x, y, KeyEvent.VK_ESCAPE, G.fonts[1], "New Game");
+        }
+
+        public void clicked()
+        {
+            startNewGame();
+        }
+    }
+
+    class BtnSwapPiece extends IDMButton
+    {
+        public BtnSwapPiece(float x, float y)
+        {
+            super(x, y, KeyEvent.VK_SPACE, G.fonts[1], "Swap");
+        }
+
+        public void clicked()
+        {
+            lauta.pieceSwapCurrent();
+        }
+    }
+
+    class BtnAbout extends IDMButton
+    {
+        public BtnAbout(float x, float y)
+        {
+            super(x, y, KeyEvent.VK_A, G.fonts[1], "About");
+        }
+
+        public void clicked()
+        {
+            if (!widgets.containsObject(aboutBox))
+                widgets.add(aboutBox);
+        }
+    }
+}