Java实现2048小游戏,我竟然一盘都没赢,你们说我到底行不行?

引言:
前几天偶尔看到了这个数字游戏,感觉还蛮有意思,就玩了一下,竟然赢不了,怎么玩都是输,真是邪门了,这不作为程序员员,我玩不赢我就自己写一个,行不行?我自己写的,我想赢就赢,条件我自己设定,就是玩!!

效果图


实现思路
绘制窗口。
创建菜单。
创建所有空白卡片。
随机创建一个卡片(2或者4)。
键盘事件监听(上、下、左、右键监听)。
根据键盘的方向,处理数字的移动合并。
加入成功、失败判定。
处理其他收尾工作。
代码实现
创建窗口
首先创建一个游戏窗体类GameFrame,继承至JFrame,用来显示在屏幕上(window的对象),每个游戏都有一个窗口,设置好窗口标题、尺寸、布局等就可以。

package main;

import java.awt.Color;
import javax.swing.JFrame;
/**
 *窗体类
 */
public class GameFrame extends JFrame {
    //构造方法
    public GameFrame(){
        setTitle("2048");//设置标题
        setSize(370, 420);//设置窗体大小
        getContentPane().setBackground(new Color(66,136,83));//加上背景
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//关闭后进程退出
        setLocationRelativeTo(null);//居中
        setResizable(false);//不允许变大
    }
}

创建面板容器GamePanel继承至JPanel

package main;

import javax.swing.JFrame;
import javax.swing.JPanel;

/*
 * 画布类
 */
public class GamePanel extends JPanel{
    private JFrame mainFrame=null;
    private GamePanel panel = null;
    //构造里面初始化相关参数
    public GamePanel(JFrame frame){
        this.setLayout(null);
        this.setOpaque(false);
        this.mainFrame=frame;
        this.panel =this;
    }
}

再创建一个Main类,来启动这个窗口,用来启动。

package main;

//Main类
public class Main {

    public static void main(String[] args) {
        GameFrame frame = new GameFrame();
        GamePanel panel = new GamePanel(frame);
        frame.add(panel);
        frame.setVisible(true);
    }
}

右键执行这个Main类,窗口建出来了


创建菜单
private Font createFont(){
        return new Font("思源宋体",Font.BOLD,18);
    }
    //创建菜单
    private void createMenu() {
        //创建JMenuBar
        JMenuBar jmb = new JMenuBar();
        //取得字体
        Font tFont = createFont(); 
        //创建游戏选项
        JMenu jMenu1 = new JMenu("游戏");
        jMenu1.setFont(tFont);
        //创建帮助选项
        JMenu jMenu2 = new JMenu("帮助");
        jMenu2.setFont(tFont);
        
        JMenuItem jmi1 = new JMenuItem("新游戏");
        jmi1.setFont(tFont);
        JMenuItem jmi2 = new JMenuItem("退出");
        jmi2.setFont(tFont);
        //jmi1 jmi2添加到菜单项“游戏”中
        jMenu1.add(jmi1);
        jMenu1.add(jmi2);
        
        JMenuItem jmi3 = new JMenuItem("操作帮助");
        jmi3.setFont(tFont);
        JMenuItem jmi4 = new JMenuItem("胜利条件");
        jmi4.setFont(tFont);
        //jmi13 jmi4添加到菜单项“游戏”中
        jMenu2.add(jmi3);
        jMenu2.add(jmi4);
        
        jmb.add(jMenu1);
        jmb.add(jMenu2);
        
        mainFrame.setJMenuBar(jmb);
        
        //添加监听
        jmi1.addActionListener(this);
        jmi2.addActionListener(this);
        jmi3.addActionListener(this);
        jmi4.addActionListener(this);
        //设置指令
        jmi1.setActionCommand("restart");
        jmi2.setActionCommand("exit");
        jmi3.setActionCommand("help");
        jmi4.setActionCommand("win");
    }

此时直接把这个代码加入到GamePanel中,发现是会报错的,需要实现ActionListener,并重写actionPerformed 方法。

此时GamePanel的代码如下:

package main;

import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.plaf.FontUIResource;

/*
 * 画布类
 */
public class GamePanel extends JPanel implements ActionListener{
    private JFrame mainFrame=null;
    private GamePanel panel = null;
    //构造里面初始化相关参数
    public GamePanel(JFrame frame){
        this.setLayout(null);
        this.setOpaque(false);
        this.mainFrame=frame;
        this.panel =this;
        
        //创建菜单
        createMenu();
    }
    
    private Font createFont(){
        return new Font("思源宋体",Font.BOLD,18);
    }
    //创建菜单
    private void createMenu() {
        //创建JMenuBar
        JMenuBar jmb = new JMenuBar();
        //取得字体
        Font tFont = createFont(); 
        //创建游戏选项
        JMenu jMenu1 = new JMenu("游戏");
        jMenu1.setFont(tFont);
        //创建帮助选项
        JMenu jMenu2 = new JMenu("帮助");
        jMenu2.setFont(tFont);
        
        JMenuItem jmi1 = new JMenuItem("新游戏");
        jmi1.setFont(tFont);
        JMenuItem jmi2 = new JMenuItem("退出");
        jmi2.setFont(tFont);
        //jmi1 jmi2添加到菜单项“游戏”中
        jMenu1.add(jmi1);
        jMenu1.add(jmi2);
        
        JMenuItem jmi3 = new JMenuItem("操作帮助");
        jmi3.setFont(tFont);
        JMenuItem jmi4 = new JMenuItem("胜利条件");
        jmi4.setFont(tFont);
        //jmi13 jmi4添加到菜单项“游戏”中
        jMenu2.add(jmi3);
        jMenu2.add(jmi4);
        
        jmb.add(jMenu1);
        jmb.add(jMenu2);
        
        mainFrame.setJMenuBar(jmb);
        
        //添加监听
        jmi1.addActionListener(this);
        jmi2.addActionListener(this);
        jmi3.addActionListener(this);
        jmi4.addActionListener(this);
        //设置指令
        jmi1.setActionCommand("restart");
        jmi2.setActionCommand("exit");
        jmi3.setActionCommand("help");
        jmi4.setActionCommand("win");
    }
    @Override
    public void actionPerformed(ActionEvent e) {
        String command = e.getActionCommand();
        UIManager.put("OptionPane.buttonFont", new FontUIResource(new Font("思源宋体", Font.ITALIC, 18)));
        UIManager.put("OptionPane.messageFont", new FontUIResource(new Font("思源宋体", Font.ITALIC, 18)));
        if ("exit".equals(command)) {
            Object[] options = { "确定", "取消" };
            int response = JOptionPane.showOptionDialog(this, "您确认要退出吗", "",
                    JOptionPane.YES_OPTION, JOptionPane.QUESTION_MESSAGE, null,
                    options, options[0]);
            if (response == 0) {
                System.exit(0);
            } 
        }else if("restart".equals(command)){
        }else if("help".equals(command)){
            JOptionPane.showMessageDialog(null, "通过键盘的上下左右来移动,相同数字会合并!",
                    "提示!", JOptionPane.INFORMATION_MESSAGE);
        }else if("win".equals(command)){
            JOptionPane.showMessageDialog(null, "得到数字2048获得胜利,当没有空卡片则失败!",
                    "提示!", JOptionPane.INFORMATION_MESSAGE);
        }
    }
}
 


创建Card
建立Card类

package main;

import java.awt.Graphics;

public class Card {
    private int x = 0;// x坐标
    private int y = 0;// y坐标
    private int w = 80;// 宽
    private int h = 80;// 高
    private int i = 0;//下标i
    private int j = 0;//下标j
    private int start=10;//偏移量(固定值)
    private int num=0;//显示数字
    private boolean merge=false;//当前是否被合并过,如果合并了,则不能继续合并,针对当前轮
    
    public Card(int i,int j){
        this.i=i;
        this.j=j;
    }
    //根据i j计算x y坐标
    private void cal(){
        this.x = start + j*w + (j+1)*5;
        this.y = start + i*h + (i+1)*5;
    }
    //绘制方法
    public void draw(Graphics g) {
        cal();
        g.fillRoundRect(x, y, w, h, 4, 4);
    }
    
    
    public int getNum() {
        return num;
    }
    public void setNum(int num) {
        this.num = num;
    }
    public boolean isMerge() {
        return merge;
    }
    public void setMerge(boolean merge) {
        this.merge = merge;
    }
}

在GamePanel中加入相关参数

private final int COLS=4;//列
private final int ROWS=4;//行
private Card cards[][] = new Card[ROWS][COLS];
private String gameFlag = "start";//游戏状态
1
2
3
4
实例化Card对象

//初始化
private void init() {
    Card card;
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            card = new Card(i,j);
            cards[i][j]=card;
        }
    }
}

在构造方法中调用

//构造里面初始化相关参数
public GamePanel(JFrame frame){
    this.setLayout(null);
    this.setOpaque(false);
    this.mainFrame=frame;
    this.panel =this;
    
    //创建菜单
    createMenu();
    
    //初始化
    init();
}

在GamePanel中重写paint方法,并在此方法中绘制这些卡片。

@Override
public void paint(Graphics g) {
    super.paint(g);
    //绘制卡片
    drawCard(g);
}
//绘制卡片
private void drawCard(Graphics g) {
    Card card;
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            card = cards[i][j];
            card.draw(g);
        }
    }        
}
 

这个黑色不是我们想要的,要根据不同的数字来设置不同的颜色,于是我们在Card修改一下。

//获取color
private Color getColor(){
    Color color=null;
    //根据num设定颜色
    switch (num) {
    case 2:
        color = new Color(238,244,234);
        break;
    case 4:
        color = new Color(222,236,200);
        break;
    case 8:
        color = new Color(174,213,130);
        break;
    case 16:
        color = new Color(142,201,75);
        break;
    case 32:
        color = new Color(111,148,48);
        break;
    case 64:
        color = new Color(76,174,124);
        break;
    case 128:
        color = new Color(60,180,144);
        break;
    case 256:
        color = new Color(45,130,120);
        break;
    case 512:
        color = new Color(9,97,26);
        break;
    case 1024:
        color = new Color(242,177,121);
        break;
    case 2048:
        color = new Color(223,185,0);
        break;

    default://默认颜色
        color = new Color(92,151,117);
        break;
    }
    
    return color;
}

加入数字的显示和颜色的修改代码,修改draw方法。

//绘制方法
public void draw(Graphics g) {
    cal();
    //获取旧的颜色
    Color oColor = g.getColor();
    //获取要用的颜色
    Color color = getColor();
    //设置画笔颜色
    g.setColor(color);
    g.fillRoundRect(x, y, w, h, 4, 4);
    
    if(num!=0){
        //设置字的颜色
        g.setColor(new Color(125,78,51));
        Font font = new Font("思源宋体", Font.BOLD, 35);
        g.setFont(font);
        //转换成String
        String text = num+"";
        //计算该字体文本的长度
        int wordWidth = getWordWidth(font, text);
        //计算出字体居中位置的X坐标
        int sx = x+(w-wordWidth)/2;
        //绘制
        g.drawString(text, sx , y+50);
    }
    
    //恢复画笔颜色
    g.setColor(oColor);
}

//得到该字体字符串的长度  
public static int getWordWidth(Font font, String content) {
    FontDesignMetrics metrics = FontDesignMetrics.getMetrics(font);
    int width = 0;
    for (int i = 0; i < content.length(); i++) {
        width += metrics.charWidth(content.charAt(i));
    }
    return width;
}
1
 

修改一下Card默认的数字,试试效果

随机创建一个数字,2或者4
先把Card类中 num 默认改成0
因为2跟4出现的比例是1:4,所以采用随机出1-5的数字,当是1的时候就表示,当得到2、3、4、5的时候就表示要出现数字2.
随机获取i,j 就可以得到卡片的位置,割接i,j取到card实例,如果卡片没有数字,就表示可以,否则就递归继续取,取到为止。
把刚才取到的数字,设置到card实例对象中就好了。
代码如下:

//在随机的空卡片创建数字2或者4
private void createRandomNumber() {
    int num = 0;
    Random random = new Random();
    int index = random.nextInt(5)+1;//这样取出来的就是1-5 之间的随机数
    //因为2和4出现的概率是1比4,所以如果index是1,则创建数字4,否则创建数字2(1被随机出来的概率就是1/5,而其他就是4/5 就是1:4的关系)
    
    if(index==1){
        num = 4;
    }else {
        num = 2;
    }
    //判断如果格子已经满了,则不再获取,退出
    if(cardFull()){
        return ;
    }
    //获取随机卡片,不为空的
    Card card = getRandomCard(random);
    //给card对象设置数字
    if(card!=null){
        card.setNum(num);
    }
}
//获取随机卡片,不为空的
private Card getRandomCard(Random random) {
    int i = random.nextInt(ROWS);
    int j = random.nextInt(COLS);
    Card card = cards[i][j];
    if(card.getNum()==0){//如果是空白的卡片,则找到了,直接返回
        return card;
    }
    //没找到空白的,就递归,继续寻找
    return getRandomCard(random);
}
//判断格子满了
private boolean cardFull() {
    Card card;
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            card = cards[i][j];
            if(card.getNum()==0){//有一个为空,则没满
                return false;
            }
        }
    }        
    return true;
}

构造中调用,表示打开游戏默认一个数字

加入键盘事件
记得在构造中调用这个方法哦。

//添加键盘监听
private void createKeyListener() {
    KeyAdapter l = new KeyAdapter() {
        //按下
        @Override
        public void keyPressed(KeyEvent e) {
            if(!"start".equals(gameFlag)) return ;
            int key = e.getKeyCode();
            switch (key) {
                //向上
                case KeyEvent.VK_UP:
                case KeyEvent.VK_W:
                    moveCard(1);//向上
                    break;
                    
                //向右    
                case KeyEvent.VK_RIGHT:
                case KeyEvent.VK_D:
                    moveCard(2);//向右
                    break;
                    
                //向下
                case KeyEvent.VK_DOWN:
                case KeyEvent.VK_S:
                    moveCard(3);//向上下
                    break;
                    
                //向左
                case KeyEvent.VK_LEFT:
                case KeyEvent.VK_A:
                    moveCard(4);//向左
                    break;
            }
        
        }
        //松开
        @Override
        public void keyReleased(KeyEvent e) {
        }
        
    };
    //给主frame添加键盘监听
    mainFrame.addKeyListener(l);
}

加入鼠标移动逻辑处理代码

//卡片移动的方法
protected void moveCard(int dir) {
    //将卡片清理一遍,因为每轮移动会设定合并标记,需重置
    clearCard();
    
    if(dir==1){//向上移动
        moveCardTop(true);
    }
    //移动后要创建新的卡片
    createRandomNumber();
    //重绘
    repaint();
}

//将卡片清理一遍,因为每轮移动会设定合并标记,需重置
private void clearCard() {
    Card card;
    for (int i = 0; i < ROWS; i++) {//i从1开始,因为i=0不需要移动
        for (int j = 0; j < COLS; j++) {
            card = cards[i][j];
            card.setMerge(false);
        }
    }
}

//向上移动
private boolean moveCardTop(boolean bool) {
    boolean res = false;
    Card card;
    for (int i = 1; i < ROWS; i++) {//i从1开始,因为i=0不需要移动
        for (int j = 0; j < COLS; j++) {
            card = cards[i][j];
            if(card.getNum()!=0){//只要卡片不为空,要移动
                if(card.moveTop(cards,bool)){//向上移动
                    res = true;//有一个为移动或者合并了,则res为true
                }
            }
        }
    }
    return res;
}
1

在Card类中加入向上移动的处理逻辑

从第2行开始移动,因为第一行不需要移动。
只要卡片的数字不是0,就表示要移动。
根据 i-1 可以获取到上一个卡片,如果上一个卡片是空,则把当前卡片交换上去,并且递归,因为可能要继续往上移动。
如果当前卡片与上一个卡片是相同数字的,则要合并。
以上两种都不是,则不做操作。
//卡片向上移动
public boolean moveTop(Card[][] cards,boolean bool) {
    //设定退出条件
    if(i==0){//已经是最上面了
        return false;
    }
    //上面一个卡片
    Card prev = cards[i-1][j];
    if(prev.getNum()==0){//上一个卡片是空
        //移动,本质就是设置数字
        if(bool){//bool为true才执行,因为flase只是用来判断能否移动
            prev.num=this.num;
            this.num=0;
            //递归操作(注意这里是要 prev 来 move了)
            prev.moveTop(cards,bool);
        }
        return true;
    }else if(prev.getNum()==num && !prev.merge){//合并操作(如果已经合并了,则不运行再次合并,针对当然轮)
        if(bool){bool为true才执行
            prev.merge=true;
            prev.num=this.num*2;
            this.num=0;
        }
        return true;
    }else {//上一个的num与当前num不同,无法移动,并退出
        return false;
    }
}

看看效果


加入其他3个方向的代码,首先是在GamePanel中加入几个代码。

//向右移动
private boolean moveCardRight(boolean bool) {
    boolean res = false;
    Card card;
    for (int i = 0; i < ROWS; i++) {
        for (int j = COLS-1; j >=0 ; j--) {//j从COLS-1开始,从最右边开始移动递减
            card = cards[i][j];
            if(card.getNum()!=0){//只要卡片不为空,要移动
                if(card.moveRight(cards,bool)){//向右移动
                    res = true;//有一个为移动或者合并了,则res为true
                }
            }
        }
    }
    return res;
}

//向下移动
private boolean moveCardBottom(boolean bool) {
    boolean res = false;
    Card card;
    for (int i = ROWS-1; i >=0; i--) {//i从ROWS-1开始,往下递减移动
        for (int j = 0; j < COLS; j++) {
            card = cards[i][j];
            if(card.getNum()!=0){//只要卡片不为空,要移动
                if(card.moveBottom(cards,bool)){//下移动
                    res = true;//有一个为移动或者合并了,则res为true
                }
            }
        }
    }
    return res;
}

//向左移动
private boolean moveCardLeft(boolean bool) {
    boolean res = false;
    Card card;
    for (int i = 0; i < ROWS; i++) {
        for (int j = 1; j < COLS ; j++) {//j从1开始,从最左边开始移动
            card = cards[i][j];
            if(card.getNum()!=0){//只要卡片不为空,要移动
                if(card.moveLeft(cards,bool)){//向左移动
                    res = true;//有一个为移动或者合并了,则res为true
                }
            }
        }
    }
    return res;
}

在Card加入其他几个方向的方法

//向下移动
public boolean moveBottom(Card[][] cards,boolean bool) {
    //设定退出条件
    if(i==3){//已经是最下面了
        return false;
    }
    //上面一个卡片
    Card prev = cards[i+1][j];
    if(prev.getNum()==0){//上一个卡片是空
        //移动,本质就是设置数字
        if(bool){//bool为true才执行,因为flase只是用来判断能否移动
            prev.num=this.num;
            this.num=0;
            //递归操作(注意这里是要 prev 来 move了)
            prev.moveBottom(cards,bool);
        }
        return true;
    }else if(prev.getNum()==num && !prev.merge){//合并操作(如果已经合并了,则不运行再次合并,针对当然轮)
        if(bool){bool为true才执行
            prev.merge=true;
            prev.num=this.num*2;
            this.num=0;
        }
        return true;
    }else {//上一个的num与当前num不同,无法移动,并退出
        return false;
    }

    
}
//向右移动
public boolean moveRight(Card[][] cards,boolean bool) {
    //设定退出条件
    if(j==3){//已经是最右边了
        return false;
    }
    //上面一个卡片
    Card prev = cards[i][j+1];
    if(prev.getNum()==0){//上一个卡片是空
        //移动,本质就是设置数字
        if(bool){//bool为true才执行,因为flase只是用来判断能否移动
            prev.num=this.num;
            this.num=0;
            //递归操作(注意这里是要 prev 来 move了)
            prev.moveRight(cards,bool);
        }
        return true;
    }else if(prev.getNum()==num && !prev.merge){//合并操作(如果已经合并了,则不运行再次合并,针对当然轮)
        if(bool){bool为true才执行
            prev.merge=true;
            prev.num=this.num*2;
            this.num=0;
        }
        return true;
    }else {//上一个的num与当前num不同,无法移动,并退出
        return false;
    }
}
//向左移动
public boolean moveLeft(Card[][] cards,boolean bool) {
    //设定退出条件
    if(j==0){//已经是最左边了
        return false;
    }
    //上面一个卡片
    Card prev = cards[i][j-1];
    if(prev.getNum()==0){//上一个卡片是空
        //移动,本质就是设置数字
        if(bool){//bool为true才执行,因为flase只是用来判断能否移动
            prev.num=this.num;
            this.num=0;
            //递归操作(注意这里是要 prev 来 move了)
            prev.moveLeft(cards,bool);    
        }
        return true;
    }else if(prev.getNum()==num && !prev.merge){//合并操作(如果已经合并了,则不运行再次合并,针对当然轮)
        if(bool){bool为true才执行
            prev.merge=true;
            prev.num=this.num*2;
            this.num=0;
        }
        return true;
    }else {//上一个的num与当前num不同,无法移动,并退出
        return false;
    }
}


修改一下moveCard方法

//卡片移动的方法
protected void moveCard(int dir) {
    //将卡片清理一遍,因为每轮移动会设定合并标记,需重置
    clearCard();
    
    if(dir==1){//向上移动
        moveCardTop(true);
    }else if(dir==2){//向右移动
        moveCardRight(true);
    }else if(dir==3){//向下移动
        moveCardBottom(true);
    }else if(dir==4){//向左移动
        moveCardLeft(true);
    }
    //移动后要创建新的卡片
    createRandomNumber();
    //重绘
    repaint();
}
 


做到这里就基本完成了,加入其他一下辅助的东西就行了,比如重新开始、游戏胜利,游戏结束等,也就不多说了。

看到这里的大佬,动动发财的小手 点赞 + 回复 + 收藏,能【 关注 】一波就更好了。
————————————————
版权声明:本文为CSDN博主「编程界明世隐」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/dkm123456/article/details/120138845

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值