4096

4096 is a Java-based clone of the well-known web and mobile game 2048, which itself clones 1024 and is similiar to THREES. The naming trend is quite obvious, though note that 2^{12} is a power of two where the exponent is divisible by three, futher connecting to the aforementioned game.

In the game, you are faced with a 4×4 matrix, containing powers of two. By swiping in the four cardinal directions (e.g. pressing the arrow keys), you shove all the non-empty cells to that side. When two equal powers of two collide, they fuse together, adding. Once you shoved, an empty tile pseudo-randomly transforms to either a two-tile (90%) or a four-tile (10%).
Your objective at first is to reach the tile 4096, though the real goal is to achieve the highest score. Your score is the sum of all the collisions you managed to cause.

To play 4096, you can either download the .jar file or review and compile the game for yourself, using the source code listed below.

Controls

  • Up, down, left or right arrow key shoves the tiles
  • Escape restarts the game upon a loss
  • F11 toggles fullscreen

A game after a few moves A finished game with a score of 1700


// Java 1.8 Code
// Jonathan Frech,  5th of December 2016
//          edited  6th of December 2016
//          edited  7th of December 2016
//          edited  8th of December 2016
//          edited  9th of December 2016
//          edited 19th of February 2017
//          edited 24th of February 2017
//          edited 28th of February 2017
//          * gave the 4096 tile a color
//          edited 22nd of April    2017
//          * fixed window positioning by changing
//            frame.setLocationRelativeTo(null); to
//            frame.setLocationByPlatform(true);

// import
import javax.swing.*;
import javax.swing.Timer;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;

// J-Utilities
class jutil {
    // digits (3x5 pixel matrix stretched out to 15 bits, then converted to decimal)
    private final static Map<Character, Integer> characters = new HashMap<Character, Integer>(){{
        put('0', 31599);
        put('1',  4681);
        put('2', 29671);
        put('3', 29647);
        put('4', 23497);
        put('5', 31183);
        put('6', 31215);
        put('7', 29257);
        put('8', 31727);
        put('9', 31695);

        put('<', 1296);
        put('>', 1104);
    }};

    // blit one image onto another one
    static void blit(BufferedImage img, BufferedImage IMG, int x, int y) {
        Graphics g = IMG.createGraphics();
        g.drawImage(img, x, y, null);
        g.dispose();
    }
    // center one image onto another one
    static void center(BufferedImage img, BufferedImage IMG) {
        Graphics g = IMG.createGraphics();
        g.drawImage(img, (IMG.getWidth()-img.getWidth())/2, (IMG.getHeight()-img.getHeight())/2, null);
        g.dispose();
    }

    // scale an image (may create a new ratio)
    private static BufferedImage scale(BufferedImage img, int W, int H) {
        BufferedImage IMG = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB);

        Graphics g = IMG.createGraphics();
        g.drawImage(img, 0, 0, W, H, null);
        g.dispose();

        return IMG;
    }

    // scale an image (keeping its ratio)
    static BufferedImage scaleratio(BufferedImage img, int W, int H) {
        double k = (double) img.getWidth()/img.getHeight();
        int w, h;
        w = W;
        h = (int) (w/k);
        if (h > H) {
            h = H;
            w = (int) (h*k);
        }

        return scale(img, w, h);
    }

    // center an image onto graphics object with same ratio and as big as possible
    static void maxfit(BufferedImage img, BufferedImage IMG) {
        center(scaleratio(img, IMG.getWidth(), IMG.getHeight()), IMG);
    }

    // fill an image
    static void fill(BufferedImage img, Color color) {
        Graphics g = img.createGraphics();
        g.setColor(color);
        g.fillRect(0, 0, img.getWidth(), img.getHeight());
        g.dispose();
    }

    // get a character image
    private static BufferedImage character(char ch, Color color) {
        BufferedImage img = new BufferedImage(3, 5, BufferedImage.TYPE_INT_RGB);
        fill(img, color);
        if (characters.containsKey(ch)) {
            int c = characters.get(ch);
            for (int i = 0; i < 15; i++) {
                if ((c >> (14-i)&1) == 1) {
                    img.setRGB(i%3, i/3, (new Color(255, 255, 255)).getRGB());
                }
            }
        }
        return img;
    }

    // get a string image
    static BufferedImage string(String str, Color color) {
        BufferedImage img = new BufferedImage(4*str.length()+1, 7, BufferedImage.TYPE_INT_RGB);
        fill(img, color);
        for (int i = 0; i < str.length(); i++) {
            blit(character(str.charAt(i), color), img, i * 4 + 1, 1);
        }
        return img;
    }

    // random number between 0 and a (inclusive!)
    static int rand(int a) {
        return (int) (Math.random()*(a+1));
    }

    // random number between a and b (inclusive!)
    static int rand(int a, int b) {
        return a + rand(b-a);
    }

    // logger
    static void log(Object o) {
        System.out.println(o);
    }

    // integer power function
    static int pow(int b, int p) {
        return (int) Math.pow(b, p);
    }

    // copy a two-dimensional array
    static int[][] copy(int[][] array) {
        int[][] copy = new int[array.length][];

        for (int x = 0; x < array.length; x++) {
            copy[x] = new int[array[x].length];
            System.arraycopy(array[x], 0, copy[x], 0, array[x].length);
        }

        return copy;
    }
}

// game class
class Game extends JPanel implements ActionListener, KeyListener, ComponentListener {
    private final int DEFAULTWIDTH, DEFAULTHEIGHT;
    private int WIDTH, HEIGHT;
    private boolean fullscreen;
    private Main parent;

    // width, height, field
    private int w = 4;
    private int h = 4;
    private int[][] field;

    // game over
    private boolean gameover;
    private int score = 0;

    // constructor
    Game(Main parent) {
        // parent
        this.parent = parent;

        // screen
        DEFAULTWIDTH = 1080;
        DEFAULTHEIGHT = 720;
        WIDTH = DEFAULTWIDTH;
        HEIGHT = DEFAULTHEIGHT;

        // timer every 100 ms
        Timer timer = new Timer(100, this);
        timer.start();

        // listeners
        addKeyListener(this);
        addComponentListener(this);

        // focus
        setFocusable(true);
        setBackground(new Color(0, 0, 0));

        // initialize game
        newgame();

        // repaint
        repaint();
    }

    // new game
    private void newgame() {
        gameover = false;
        field = new int[w][h];

        // debug fills the board with various powers of two
        boolean debug = false;
        if (debug) {
            for (int x = 0; x < w; x++) {
                for (int y = 0; y < h; y++) {
                    field[x][y] = 1+x+h*y;
                }
            }
            field[w-1][h-1] = 0;
        }

        // normal game start, spawn in two random start tiles
        else {
            spawntile();
            spawntile();
        }
    }

    // save score to disk using a time stamp
    private void savescore() {
        try {
            Calendar c = Calendar.getInstance();
            FileWriter f = new FileWriter("score.txt", true);
            f.write("<" + c.get(Calendar.DAY_OF_MONTH) + "." + c.get(Calendar.MONTH) + "." + c.get(Calendar.YEAR) + " " + c.get(Calendar.HOUR_OF_DAY) + ":" + c.get(Calendar.MINUTE) + "> " + score + " point" + (score == 1 ? "":"s") + "\n");
            f.close();
        }
        catch (IOException e) {
            e.printStackTrace();
        }

    }

    // spawn a new tile
    private void spawntile() {
        ArrayList<Integer> P = new ArrayList<>();
        for (int i = 0; i < w*h; i++) {
            if (field[i%w][i/w] == 0) {
                P.add(i);
            }
        }

        if (P.size() > 0) {
            int p = P.get(jutil.rand(0, P.size()-1));
            if (jutil.rand(9) == 0) {
                field[p%w][p/w] = 2;
            }
            else {
                field[p%w][p/w] = 1;
            }
        }
    }

    // determine tile color
    private Color fieldcolor(int n) {
        // gray when game is over
        if (gameover) {
            return new Color(50, 50, 50);
        }

        // different colors for the different values
        switch(n) {
            case  0: return new Color(50, 50, 50);
            case  1: return new Color(255, 228, 196);
            case  2: return new Color(205, 178, 146);
            case  3: return new Color(205, 128, 96);
            case  4: return new Color(205, 78, 46);
            case  5: return new Color(205, 28, 20);
            case  6: return new Color(255, 28, 20);
            case  7: return new Color(180, 150, 60);
            case  8: return new Color(230, 200, 60);
            case  9: return new Color(50, 70, 100);
            case 10: return new Color(40, 40, 100);
            case 11: return new Color(10, 100, 50);
            case 12: return new Color(10, 175, 60);
        }

        // default color
        return new Color(0, 0, 0);
    }

    // move field tiles
    private void fieldshift(char dir, int[][] field) {
        // used for moving a tile across the whole screen
        int i;

        // left
        if (dir == '<') {
            for (int y = 0; y < h; y++) {
                for (int x = 1; x < w; x++) {
                    i = x;
                    while (i > 0 && field[i-1][y] == 0) {
                        field[i-1][y] = field[i][y];
                        field[i][y] = 0;
                        i--;
                    }
                }
            }
        }

        // right
        else if (dir == '>') {
            for (int y = 0; y < h; y++) {
                for (int x = w-2; x > -1; x--) {
                    i = x;
                    while (i < w-1 && field[i+1][y] == 0) {
                        field[i+1][y] = field[i][y];
                        field[i][y] = 0;
                        i++;
                    }
                }
            }
        }

        // up
        else if (dir == '^') {
            for (int x = 0; x < w; x++) {
                for (int y = 1; y < h; y++) {
                    i = y;
                    while (i > 0 && field[x][i-1] == 0) {
                        field[x][i-1] = field[x][i];
                        field[x][i] = 0;
                        i--;
                    }
                }
            }
        }

        // down
        else if (dir == 'v') {
            for (int x = 0; x < w; x++) {
                for (int y = h-2; y > -1; y--) {
                    i = y;
                    while (i < h-1 && field[x][i+1] == 0) {
                        field[x][i+1] = field[x][i];
                        field[x][i] = 0;
                        i++;
                    }
                }
            }
        }
    }

    // add field tiles
    private int fieldadd(char dir, int[][] field) {
        // track score added
        int score = 0;

        // left
        if (dir == '<') {
            for (int y = 0; y < h; y++) {
                for (int x = 1; x < w; x++) {
                    if (field[x][y] != 0 && field[x][y] == field[x-1][y]) {
                        field[x-1][y]++;
                        score += jutil.pow(2, field[x-1][y]);
                        field[x][y] = 0;
                    }
                }
            }
        }

        // right
        else if (dir == '>') {
            for (int y = 0; y < h; y++) {
                for (int x = w-2; x > -1; x--) {
                    if (field[x][y] != 0 && field[x][y] == field[x+1][y]) {
                        field[x+1][y]++;
                        score += jutil.pow(2, field[x+1][y]);
                        field[x][y] = 0;
                    }
                }
            }
        }

        // up
        else if (dir == '^') {
            for (int x = 0; x < w; x++) {
                for (int y = 1; y < h; y++) {
                    if (field[x][y] != 0 && field[x][y] == field[x][y-1]) {
                        field[x][y-1]++;
                        score += jutil.pow(2, field[x][y-1]);
                        field[x][y] = 0;
                    }
                }
            }
        }

        // down
        else if (dir == 'v') {
            for (int x = 0; x < w; x++) {
                for (int y = h-2; y > -1; y--) {
                    if (field[x][y] != 0 && field[x][y] == field[x][y+1]) {
                        field[x][y+1]++;
                        score += jutil.pow(2, field[x][y+1]);
                        field[x][y] = 0;
                    }
                }
            }
        }

        // return additional score
        return score;
    }

    // perform a field action
    private int fieldaction(char dir, int[][] field) {
        // track score added
        int score = 0;

        // move all, add, move all
        fieldshift(dir, field);
        score += fieldadd(dir, field);
        fieldshift(dir, field);

        // return additional score
        return score;
    }

    // perform a field input (fieldaction with tile spawn and game over check)
    private void fieldinput(char dir) {
        // copy
        int[][] f = jutil.copy(field);

        // perform action, increase score
        score += fieldaction(dir, field);

        // add new tile
        if (!Arrays.deepEquals(f, field)) {
            spawntile();
        }

        // check gameover
        checkgameover();
    }

    // check for game over
    private void checkgameover() {
        // only check for gameover if the game is still running
        if (gameover) {
            return;
        }

        // check for any empty tiles
        for (int x = 0; x < w; x++) {
            for (int y = 0; y < h; y++) {
                if (field[x][y] == 0) {
                    return;
                }
            }
        }

        // copy, apply every possible action
        int[][] f = jutil.copy(field);
        fieldaction('<', f);
        fieldaction('>', f);
        fieldaction('^', f);
        fieldaction('v', f);

        // check for any empty tiles
        for (int x = 0; x < w; x++) {
            for (int y = 0; y < h; y++) {
                if (f[x][y] == 0) {
                    return;
                }
            }
        }

        // game over
        gameover = true;

        // save score to disk
        savescore();
    }

    // label painter
    @Override
    public void paintComponent(Graphics g) {
        // super
        super.paintComponent(g);

        // image as big as panel
        BufferedImage IMG = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);

        // render field
        int n = 100;
        BufferedImage img = new BufferedImage(w*n, h*n, BufferedImage.TYPE_INT_RGB);
        BufferedImage i = new BufferedImage(n, n, BufferedImage.TYPE_INT_RGB);
        Color color;
        String str;
        for (int x = 0; x < w; x++) {
            for (int y = 0; y < h; y++) {
                color = fieldcolor(field[x][y]);
                jutil.fill(i, color);
                if (field[x][y] == 0) {
                    str = "";
                }
                else {
                    str = Integer.toString(jutil.pow(2, field[x][y]));
                }

                jutil.center(jutil.scaleratio(jutil.string(str, color), n, n), i);
                jutil.blit(i, img, x*n, y*n);
            }
        }

        if (gameover) {
            jutil.center(jutil.scaleratio(jutil.string("> " + score + " <", new Color(0, 0, 0)), w*n, h*n), img);
        }

        // put image onto image, then image onto graphics object
        jutil.maxfit(img, IMG);
        g.drawImage(IMG, 0, 0, WIDTH, HEIGHT, null);

        // dispose
        g.dispose();
    }

    // set size
    private void setsize(int w, int h) {
        WIDTH = w;
        HEIGHT = h;

        repaint();
    }

    // set size through a dimension object
    private void setsize(Dimension d) {
        setsize((int) d.getWidth(), (int) d.getHeight());
    }

    // set fullscreen
    private void setfullscreen(boolean f) {
        // dispose

        parent.dispose();
        if (f) {
            Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
            parent.setSize(d);
            setsize(d);
            parent.setUndecorated(true);
            parent.setLocation(0, 0);
            fullscreen = true;
        }
        else {
            Dimension d = new Dimension(DEFAULTWIDTH, DEFAULTHEIGHT);
            parent.setSize(d);
            setsize(d);
            parent.setUndecorated(false);
            parent.setLocationByPlatform(true);
            fullscreen = false;
        }

        // make visible
        parent.setVisible(true);
    }

    // toggle fullscreen
    private void togglefullscreen() {
        setfullscreen(!fullscreen);
    }

    // label size
    @Override
    public Dimension getPreferredSize() {
        return new Dimension(WIDTH, HEIGHT);
    }

    // action listener
    @Override
    public void actionPerformed(ActionEvent e) {
        repaint();
    }

    // key listener
    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_F11) {
            togglefullscreen();
        }

        if (!gameover) {
            if (e.getKeyCode() == KeyEvent.VK_LEFT) {
                fieldinput('<');
            }
            else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
                fieldinput('>');
            }
            else if (e.getKeyCode() == KeyEvent.VK_UP) {
                fieldinput('^');
            }
            else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
                fieldinput('v');
            }
        }
        else {
            if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                newgame();
            }
        }

        repaint();
    }
    @Override
    public void keyReleased(KeyEvent e) {}
    @Override
    public void keyTyped(KeyEvent e) {}

    // component listener
    @Override
    public void componentMoved(ComponentEvent e) {}
    @Override
    public void componentHidden(ComponentEvent e) {}
    @Override
    public void componentShown(ComponentEvent e) {}
    @Override
    public void componentResized(ComponentEvent e) {
        setsize(e.getComponent().getWidth(), e.getComponent().getHeight());
    }
}

// Main class
class Main extends JFrame {
    // constructor
    private Main() {
        setTitle("4096");
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        setLocationByPlatform(true);

        add(new Game(this));
        pack();
        setVisible(true);
    }

    // main function
    public static void main(String[] args) {
        new Main();
    }
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s