8
\$\begingroup\$

This game is made for JFrame. You can control using the arrows. The numbers in the cells are the degrees of the number 2. It is possible to change the initial position of the window, its size, and the number of rows-columns(they are equal) in the field. You can also change the width of the frames of the cells, indent between them. You can adjust what degree of the number 2 you need to get in order to win. I hope that you point out to me weaknesses in my code, if any.

normal mod

main method:

    final int windowXPos = 100;
    final int windowYPos = 100;
    final int windowWidthAndHeight = 800;
    final int spacesBetweenCells = 4;
    final int widthOfFrame = 4;
    final int inARow = 4;
    final int valueOfCellToWin = 11;

    GameWindow2048 window = new GameWindow2048(windowXPos,windowYPos, windowWidthAndHeight, spacesBetweenCells, widthOfFrame, inARow, valueOfCellToWin);

class Area skeleton:

public class Area {
    Cell[][] area;
    final int valueOfCellToWin;

    public Area(int inARow, int valueOfCellToWin) {
        area = new Cell[inARow][inARow];
        fillAreaWithEmpty();
        addNewCell();
        addNewCell();
        this.valueOfCellToWin = valueOfCellToWin;
    }

    private void fillAreaWithEmpty() {
        for(int y = 0; y < area.length; y++) {
            for(int x = 0; x < area[0].length; x++) {
                area[y][x] = new Cell();
                area[y][x].setEmpty();
            }
        }
    }

    void playAgain() {
        fillAreaWithEmpty();
        addNewCell();
        addNewCell();
    }

    boolean isWin() {}

    boolean addNewCell(){}

    void makeAMove(Direction direction) {}

    private int getAmountOfEmptyCells() {}
}

class Area full code:

public class Area {
    Cell[][] area;
    final int valueOfCellToWin;

    public Area(int inARow, int valueOfCellToWin) {
        area = new Cell[inARow][inARow];
        fillAreaWithEmpty();
        addNewCell();
        addNewCell();
        this.valueOfCellToWin = valueOfCellToWin;
    }

    private void fillAreaWithEmpty() {
        for(int y = 0; y < area.length; y++) {
            for(int x = 0; x < area[0].length; x++) {
                area[y][x] = new Cell();
                area[y][x].setEmpty();
            }
        }
    }

    void playAgain() {
        fillAreaWithEmpty();
        addNewCell();
        addNewCell();
    }

    boolean isWin() {
        for(int y = 0; y < area.length; y++) {
            for(int x = 0; x < area[0].length; x++) {
                if(area[y][x].getPowerOf2() == valueOfCellToWin) {
                    return true;
                }
            }
        }
        return false;
    }

    boolean addNewCell(){
        int amountOfEmptyCells = getAmountOfEmptyCells();
        int numOfCellToFill;
        int counterOfEmptyCells = 0;

        if(amountOfEmptyCells == 0) {
            return false;
        }
        numOfCellToFill = Math.abs(new Random().nextInt()%amountOfEmptyCells)+1;
        out:
        for(int y = 0; y < area.length; y++) {
            for(int x = 0; x < area[0].length; x++) {
                if(area[y][x].isEmpty()) {
                    counterOfEmptyCells++;
                    if(counterOfEmptyCells == numOfCellToFill) {
                        area[y][x].setBeginNumber();
                        break out;
                    }
                }
            }
        }
        return true;
    }

    void makeAMove(Direction direction) {
        if(direction == Direction.UP) {
            for(int x = 0; x < area[0].length; x++) {
                for(int y = 1; y <= area.length - 1; y++) {
                    if(y == 0) {
                        continue;
                    }
                    if(area[y][x].isEmpty()) {
                        continue;
                    }
                    if(area[y-1][x].isEmpty()) { //empty above - move there
                        area[y-1][x].setPowerOf2(area[y][x].getPowerOf2());
                        area[y][x].setEmpty();
                        y-=2; //as we may need to move this cell even higher.
                        continue;
                    }
                    if(area[y][x].getPowerOf2() == area[y-1][x].getPowerOf2()) { // are the same? make a merge
                        area[y][x].setEmpty();
                        area[y-1][x].increasePowerOf2();
                        // merging occurs, above the cell y-1 x there are no empty cells according to the algorithm, therefore we do not return
                    }
                }
            }
        }
        if(direction == Direction.DOWN) {
            for(int x = 0; x < area[0].length; x++) {
                for(int y = area.length - 2; y >= 0; y--) {
                    if(y == area.length - 1) {
                        continue;
                    }
                    if(area[y][x].isEmpty()) {
                        continue;
                    }
                    if(area[y+1][x].isEmpty()) {
                        area[y+1][x].setPowerOf2(area[y][x].getPowerOf2()); //empty below - move there
                        area[y][x].setEmpty();
                        y+=2; //as we may need to move this cell even lower.
                        continue;
                    }
                    if(area[y][x].getPowerOf2() == area[y+1][x].getPowerOf2()) {
                        area[y][x].setEmpty();
                        area[y+1][x].increasePowerOf2();
                        // merging occurs, below the cell y + 1 x there are no empty cells according to the algorithm, so do not return
                    }
                }
            }
        }
        if(direction == Direction.RIGHT) {
            for(int y = 0; y < area.length; y++) {
                for(int x = area[0].length - 2; x >=0; x--) {
                    if(x == area.length - 1) {
                        continue;
                    }
                    if(area[y][x].isEmpty()) {
                        continue;
                    }
                    if(area[y][x+1].isEmpty()) {
                        area[y][x+1].setPowerOf2(area[y][x].getPowerOf2());
                        area[y][x].setEmpty();
                        x+=2; //as we may need to move this cell to the right.
                        continue;
                    }
                    if(area[y][x].getPowerOf2() == area[y][x+1].getPowerOf2()) {
                        area[y][x].setEmpty();
                        area[y][x+1].increasePowerOf2();
                        //merging occurs, to the right of the cell y x+1 there are no empty cells according to the algorithm, so do not return
                    }
                }
            }
        }
        if(direction == Direction.LEFT) {
            for(int y = 0; y < area.length; y++) {
                for(int x = 1; x <= area[0].length-1; x++) {
                    if(x == 0) {
                        continue;
                    }
                    if(area[y][x].isEmpty()) {
                        continue;
                    }
                    if(area[y][x-1].isEmpty()) {
                        area[y][x-1].setPowerOf2(area[y][x].getPowerOf2());
                        area[y][x].setEmpty();
                        x-=2; // as we may need to move this cell to the left.
                        continue;
                    }
                    if(area[y][x].getPowerOf2() == area[y][x-1].getPowerOf2()) {
                        area[y][x].setEmpty();
                        area[y][x-1].increasePowerOf2();
                        //merging occurs, to the left of the cell y x-1 there are no empty cells according to the algorithm, so do not return
                    }
                }
            }
        }
    }

    private int getAmountOfEmptyCells() {
        int amountOfEmptyCells = 0;
        for(int y = 0; y < area.length; y++) {
            for(int x = 0; x < area[0].length; x++) {
                if(area[y][x].isEmpty()) {
                    amountOfEmptyCells++;
                }
            }
        }
        return amountOfEmptyCells;
    }
    Cell[][] getArea() {
        return area;
    }
}

enum GamesState fullcode:

enum GamesState{
    CONTINUETHEGAME, WIN, DEFEAT
}

class Cell fullcode:

class Cell{
    private int powerOf2; // 0 - is emptyCell

    void setBeginNumber() {
        powerOf2 = (Math.abs(new Random().nextInt()%10) != 9) ? 1 : 2;
    }
    void increasePowerOf2() {
        this.powerOf2++;
    }
    int getPowerOf2() {
        return powerOf2;
    }
    boolean isEmpty() {
        return powerOf2 == 0;
    }
    void setPowerOf2(int powerOf2) {
        this.powerOf2 = powerOf2;
    }
    void setEmpty() {
        this.powerOf2 = 0;
    }
}

class Component:

public class Component extends JPanel {
    final Cell[][] area;
    final int windowWidthAndHeight;
    final int widthOfOneCellInsideFrame;
    final int spacesBetweenCells;
    final int widthOfFrame;
    final int widthOfRectOfFrame;
    final int fontSize;
    final Font font;
    GamesState statusOfGame = GamesState.CONTINUETHEGAME;

    public Component(Cell[][] area, int windowWidthAndHeight, int spacesBetweenCells, int widthOfFrame) {
        this.area = area;
        this.windowWidthAndHeight = windowWidthAndHeight;
        this.spacesBetweenCells = spacesBetweenCells;
        this.widthOfFrame = widthOfFrame;
        widthOfOneCellInsideFrame = (windowWidthAndHeight - (2 * area.length * widthOfFrame + (area.length - 1) * spacesBetweenCells)) / area.length;
        widthOfRectOfFrame = widthOfFrame * 2 + widthOfOneCellInsideFrame;
        fontSize = widthOfOneCellInsideFrame / 3;
        font = new Font("Tahoma", Font.BOLD|Font.ITALIC, fontSize);
    }

    @Override
    public void paintComponent(Graphics gr){
        gr.clearRect(0, 0, windowWidthAndHeight, windowWidthAndHeight);
        gr.setColor(Color.lightGray);
        gr.fillRect(0,0, windowWidthAndHeight, windowWidthAndHeight);
        System.out.println(statusOfGame);
        if(statusOfGame == GamesState.CONTINUETHEGAME) {
            printArea(gr);
        } else if (statusOfGame == GamesState.WIN){
            gr.setColor(Color.BLUE);
            gr.setFont(new Font("Tahoma", Font.BOLD|Font.ITALIC, windowWidthAndHeight / 25));
            gr.drawString("U WON! If u wanna play again, press 'r'", windowWidthAndHeight/6,windowWidthAndHeight/3);
        } else {
            gr.setColor(Color.RED);
            gr.setFont(new Font("Tahoma", Font.BOLD|Font.ITALIC, windowWidthAndHeight / 25));
            gr.drawString("DEFEAT! If u wanna try again, press 'r'", windowWidthAndHeight/6,windowWidthAndHeight/3);
        }


    }
    void printArea(Graphics gr) {
        for(int y = 0, YPos; y < area.length; y++) {
            for(int x = 0, XPos; x < area[0].length; x++) {
                YPos = 2 * y * widthOfFrame;
                XPos = 2 * x * widthOfFrame;
                if(y != 0) {
                    YPos += y * (spacesBetweenCells + widthOfOneCellInsideFrame);
                }
                if(x != 0) {
                    XPos += x * (spacesBetweenCells + widthOfOneCellInsideFrame);
                }
                gr.setColor(Color.BLACK);
                gr.fillRect(XPos,YPos, widthOfRectOfFrame, widthOfRectOfFrame);
                gr.setColor(Color.BLUE);
                gr.fillRect(XPos + widthOfFrame,YPos + widthOfFrame, widthOfOneCellInsideFrame, widthOfOneCellInsideFrame);
                gr.setColor(Color.MAGENTA);
                if(!area[y][x].isEmpty()) {
                    gr.setFont(font);
                    gr.drawString(Integer.toString(area[y][x].getPowerOf2()), XPos + widthOfFrame + (widthOfOneCellInsideFrame / 2) - fontSize / 4, YPos + widthOfFrame + (widthOfOneCellInsideFrame / 2) - fontSize / 4);

                }
            }
        }
    }

    public void setGameState(GamesState statusOfGame) {
        this.statusOfGame = statusOfGame;
    }
}

class GameWindow2048 with enum Direction:

public class GameWindow2048 extends JFrame{
    final int windowWidthAndHeight;
    final int spacesBetweenCells;
    final int widthOfFrame;
    final Area area;
    final Component component;

    Direction directionTemp;
    GamesState statusOfGame = GamesState.CONTINUETHEGAME;

    public GameWindow2048(int windowXPos, int windowYPos, int windowWidthAndHeight, int spacesBetweenCells, int widthOfFrame, int inARow, int valueOfCellToWin){ // сделать builder
        this.windowWidthAndHeight = windowWidthAndHeight;
        this.spacesBetweenCells = spacesBetweenCells;
        this.widthOfFrame = widthOfFrame;
        addKeyListener(new myKey()); 
        setFocusable(true); 
        setBounds(windowXPos,windowYPos,windowWidthAndHeight,windowWidthAndHeight + 20); //20 - калибровка
        setTitle("2048"); 
        Container container = getContentPane(); 
        area = new Area(inARow, valueOfCellToWin);
        component = new Component(area.getArea(), windowWidthAndHeight - 14, spacesBetweenCells, widthOfFrame); //14 - калибровка
        container.add(component); 
        setVisible(true); 
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public class myKey implements KeyListener{
        public void keyReleased(KeyEvent e) { //game
            if(statusOfGame == GamesState.CONTINUETHEGAME) {
                directionTemp = getDirection(e);
                if(directionTemp != null) {
                    area.makeAMove(directionTemp);
                    if(!area.addNewCell()) {
                        statusOfGame = GamesState.DEFEAT;
                        component.setGameState(statusOfGame);
                    }
                    if(area.isWin()) {
                        System.out.println("Win");
                        statusOfGame = GamesState.WIN;
                        component.setGameState(statusOfGame);
                    }
                }
                component.repaint();
            } else {
                System.out.println(e.getKeyCode());
                if(e.getKeyCode() == 82) {
                    area.playAgain();
                    statusOfGame = GamesState.CONTINUETHEGAME;
                    component.setGameState(statusOfGame);
                    component.repaint();
                }
            }

        } 




        Direction getDirection(KeyEvent e) {
            switch(e.getKeyCode()) {
                case 39:
                    return Direction.RIGHT;
                case 37:
                    return Direction.LEFT;
                case 38:
                    return Direction.UP;
                case 40:
                    return Direction.DOWN;
                default:
                    return null;
            }
        }
        public void keyPressed(KeyEvent e){}
        public void keyTyped(KeyEvent e){} 
    } 

    enum Direction{
        RIGHT, LEFT, UP, DOWN
    }
}
\$\endgroup\$

3 Answers 3

6
\$\begingroup\$

Others have discussed some broad concepts. I'll add some small but concrete items:

1 A new Cell starts with 0 by default. Do one or the other not both:

area[y][x] = new Cell();
area[y][x].setEmpty();

2 There is no need to keep recomputing getAmountOfEmptyCells(), you could keep a counter in Area that is updated with each Cell

3 Please make all fields private unless they are intended to be shared (you should always default to private in an OO language).

4 Don't call your class Component :) It is very well known existing piece of AWT/Swing (your JFrame already extends java.awt.Component!). I think possibly Area is a little general as well. Try something like Board/BoardUI instead of Area/Component.

5 Avoid having two sources of gameState. I actually think this should live inside Area.

statusOfGame = GamesState.DEFEAT;
component.setGameState(statusOfGame);

6 GameWindow2048.myKey is a classname in lower case!

7 The logic in GameWindow2048.myKey which makes actually moves and changes gameState would be better located inside Area. It is part of your "core" layer and a trigger (such as a key press) should only call the logic. Imagine having more triggers for the same action (swipe on a tablet, mouse-drag on desktop, voice control, mind control ...)

8 Area should be constructed externally and passed in to your UI layer. This is the key tenet that lets you keep "ui" and "core" separate.

area = new Area(inARow, valueOfCellToWin);

9 Direction should be moved into Area. Again, the "core" layer should have zero knowledge of UI classes.

10 area[0].length I think it is reasonable for Area to have an int size; field instead of doing this.

11

(Math.abs(new Random().nextInt()%10) != 9) ? 1 : 2;

I think perhaps it would be better like

private static final Random r = new Random();
...
powerOf2 = r.nextInt(10) == 9 ? 2 : 1; // 10% chance of a "2"

12 Seeing this inside a for loop scares me as a reader! Perhaps the inner loop should be going the other direction to keep pushing cells?

y-=2; //as we may need to move this cell even higher
\$\endgroup\$
5
\$\begingroup\$

I have some suggestions for your code, for me it's a good point you have separated graphic part from logic encapsulating game logic in two classes Cell and Area. Your class Cell at the moment just contains an integer value, so instead of declaring a matrix Area of Cell objects you could use a matrix of ints and redefine class Area like the code below:

public class Area {
    private int[][] area;
    private final int valueOfAreaToWin;
    private final int n;

    public Area(int n, int valueOfCellToWin) {
        this.area = new int[n][n];
        this.valueOfAreaToWin = valueOfCellToWin;
        this.n = n;
    }
}

If you can try to do just initialization of fields in your class constructor and nothing else. The use of a matrix of ints simplifies all the methods of your class because you modify a matrix of ints instead of calling methods of Cell class like the code below:

private void fillAreaWithZeros() {
    for (int[] row : area) {
        Arrays.fill(row, 0);
    }
}

public int getAmountOfZeros() {
    int amount = 0;
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            if (area[i][j] == 0) {
                ++amount;
            }
        }
    }
    return amount;
}

public boolean isWin() {
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            if (area[i][j] == 0) { return false; }
        }
    }
    return true;
}

When you iterate over matrix elements, try to use n, m for number of rows and columns, i and j for indexes because they are expected to be usually found when you work with matrices. In your method addNewCell I found the following code:

out:
for(int y = 0; y < area.length; y++) {
    for(int x = 0; x < area[0].length; x++) {
         if(area[y][x].isEmpty()) {
            counterOfEmptyCells++;
            if(counterOfEmptyCells == numOfCellToFill) {
                 area[y][x].setBeginNumber();
                 break out;
             }
          }
     }
}

It is legitime to use a label but this complicates readibility of your code, to obtain the same behaviour you can create a boolean condition and check it inside your outer loop like below:

boolean cond = true;
for(int i = 0; i < n && cond; ++i) {
    for(int j = 0; j < n; ++j) {
        if(area[i][j] == 0) {
            counterOfEmptyCells++;
            if(counterOfEmptyCells == numOfCellToFill) {
                int value = Math.abs(random.nextInt() % 10) != 9 ? 1 : 2;
                area[i][j] = value;
                cond = false;
                break;
            }
        }
    }
}

Inside the inner if cond will be set to false before breaking the inner for, and the next check of the outer for will find cond equal to false and will terminate.

Note: as @drekbour said in his comment below in this case because the composite loop is the last instruction of the method it is better directly return false inside the inner loop.

\$\endgroup\$
7
  • 1
    \$\begingroup\$ On that last point, why not just return true instead of checking cond all the time. This is the last activity of that method so nothing is gained by either variant compared to just returning. \$\endgroup\$
    – drekbour
    Commented Nov 11, 2019 at 21:57
  • \$\begingroup\$ @drekbour Good point, I add it to my answer as a note. \$\endgroup\$ Commented Nov 12, 2019 at 7:33
  • 1
    \$\begingroup\$ i think it is a good idea to have a cell class - since it's not a mere integer but it's really a cell. it's an antipattern: primitive obsession. Ever since the cell class provides very helpful methods... \$\endgroup\$ Commented Nov 12, 2019 at 7:35
  • \$\begingroup\$ the other things pointed out are very helpful +1 \$\endgroup\$ Commented Nov 12, 2019 at 7:55
  • 1
    \$\begingroup\$ I should point out that OO-obsession is also a thing that needs moderating :) If you need to write any kind of computer player, you may end up with many copies of the "board" in some kind of tree/search algorithm. Representing the state as a (single?) primitive array makes notable difference to your allocation rate and CPU cost. \$\endgroup\$
    – drekbour
    Commented Nov 13, 2019 at 10:22
5
\$\begingroup\$

some minor issues

dry - dont repeat yourself

Area.moveADirectionis full of redundant code - try to create a method for all that is in common - may i suggest a method void move (int dx, int dy) that is applyable for any direction?

another repetation is your code to iterate through cells in the Area class

for(int y = 0; y < area.length; y++) {
    for(int x = 0; x < area[0].length; x++) {
                ...
    }
}

you could instead provide a method for that List<Cell> getCells()and use the java8 stream api for example:

private int getAmountOfEmptyCells() {
    //written straight out of my head without any compiler verification
    return getCells().stream().mapToInt(c -> c.isEmpty?0:1).sum();
}

segregation of concerns

your cell should be able to draw itself. if it would do so, you could simplify your draw code in Component:

void printArea(Graphics gr) {
    area.getCells().forEach(c -> c.draw(gr, XPos,YPos);
}

and the cell would know how to draw itself, new code for Cell class:

public void draw(Graphics grm int xpos, int ypos){...};

naming convention

public class myKey implements KeyListener should be uppercase

for(int y = 0, YPos; y < area.length; y++) { YPos should be yPos at least... where do you define YPos??, same for XPos

complexity

create a configuration object for your constructor to hanlde all those arguments - you can then even provide default parameters

class Configuration {
    final int windowXPos = 100;
    final int windowYPos = 100;
    final int windowWidthAndHeight = 800;
    final int spacesBetweenCells = 4;
    final int widthOfFrame = 4;
    final int inARow = 4;
    final int valueOfCellToWin = 11;
}

that results into

Configuration defaultConfig = new Configuration();
defaultConfig.windowXPos = 123; //example to override default values
GameWindow2048 window = new GameWindow2048(defaultConfig);

input handling

instead of directly listening to keyevent you should use keybinding, see the javaDoc tutorial page

\$\endgroup\$
5
  • 1
    \$\begingroup\$ hm, about "Configuration" - its a builder pattern. I thought about it, but didn’t think of setting default values. \$\endgroup\$
    – Miron
    Commented Nov 12, 2019 at 8:55
  • \$\begingroup\$ best you would offer (aside from default constructor) a constructor from file, where you have all those settings already in a properties file. \$\endgroup\$ Commented Nov 12, 2019 at 10:36
  • 1
    \$\begingroup\$ Re. separation of concerns. I agree the drawing of a cell should be separated from drawing of the board. I'm not so keen on putting AWT code directly into the Cell class which is clearly a "Model" layer entity. The UI layer should be totally divorced from model as it's (IMO) the most common "major change" made to any game. \$\endgroup\$
    – drekbour
    Commented Nov 13, 2019 at 10:32
  • \$\begingroup\$ @drekbour i definitely agree with you, it's not a goof idea to drag that AWT dependency into thr model. I was too fast on my anser \$\endgroup\$ Commented Nov 13, 2019 at 10:48
  • 1
    \$\begingroup\$ oops. awt -> swing. There you go, that was a fast change of UI technology! :) \$\endgroup\$
    – drekbour
    Commented Nov 13, 2019 at 10:54

Not the answer you're looking for? Browse other questions tagged or ask your own question.