view game/Engine.java @ 173:8995a0363e0a

Remove secret.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 06 Mar 2017 11:02:46 +0200
parents 5070e57ebbfc
children 55ea5821c802
line wrap: on
line source

/*
 * 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;

    public AboutBox()
    {
        setPos(150f, 150f);
        setSize(720f, 420f);
    }

    public void paint(Graphics2D g)
    {
        g.setPaint(new Color(0.0f, 0.0f, 0.0f, 0.7f));
        g.fill(new RoundRectangle2D.Float(getScaledX(), getScaledY(), getScaledWidth(), getScaledHeight(), 10, 10));

        setTextFont(G.fonts[3], G.metrics[3]);
        setTextPaint(Color.white);
        setCurrPosScaledRel(20, 35);
        drawString(g, "RistiPolku (CrossPaths) v"+ G.version +"\n");

        setTextFont(G.fonts[1], G.metrics[1]);
        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 = textCurrOffs.copy();

        setTextPaint(Color.white);
        drawString(g,
        "Arrow keys / mouse wheel\n"+
        "Enter / mouse click\n"+
        "Space bar\n");

        textCurrPos.x += getScaledX(330);
        textCurrOffs.y = old.y;
        drawString(g,
        "- Rotate piece\n" +
        "- Place/lock piece\n" +
        "- Swap piece\n");

        textCurrPos.x -= getScaledX(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)
    {
        clicked();
        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
{
    double 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.baseFont = Font.createFont(Font.TRUETYPE_FONT, res.getStream());
                
                setupDerivedFonts(1.0f);
            }
            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;
        startTime = System.currentTimeMillis();
    }

    public void startNewGame()
    {
        gameFrames = 0;
        lauta.startNewGame();
    }

    public void setupDerivedFonts(float scale)
    {
        G.fonts[0] = G.baseFont.deriveFont(16f * scale);
        G.fonts[1] = G.baseFont.deriveFont(22f * scale);
        G.fonts[2] = G.baseFont.deriveFont(62f * scale);
        G.fonts[3] = G.baseFont.deriveFont(32f * scale);
    }

    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) || G.metrics == null)
        {
            dbg("Scale changed.\n");
            scaleChanged = true;
            updateBoard = true;

            float dw = dim.width / 1024.0f,
                  dh = dim.height / 768.0f;

            G.screenDim = dim;

            // Rescale fonts
            setupDerivedFonts(dw);

            // Get font metrics against current Graphics2D context
            G.metrics = new FontMetrics[G.numFonts];
            for (int i = 0; i < G.numFonts; i++)
                G.metrics[i] = g2.getFontMetrics(G.fonts[i]);

            // Rescale IDM GUI widgets
            widgets.setScale(dw, dh);
        }
        
        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);
        }


        // 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);
*/

        Toolkit.getDefaultToolkit().sync();
        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

        // About box is modal, so pass key events to it when active
        if (widgets.containsObject(aboutBox))
            aboutBox.keyPressed(e);
        else
            widgets.keyPressed(e);
    }

    public void run()
    {
        while (animEnable)
        {
            // Progress game animation clock
            gameUpdates++;

            // Animate components
            int tmp = (int) ((System.currentTimeMillis() - startTime) / 10f);
            lauta.animate(tmp);

            // Repaint with a frame limiter
            if (gameUpdates % 2 == 1)
                repaint();

            // Sleep for a moment
            try {
                Thread.sleep(15);
            }
            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);
        }
    }
}