24点游戏

  • 前言
  • 1、 算法分析
  •  1 .1 接收玩家结果输入与结果判定。
  •  1.2 工具类TimeUtils、CardUtils。
  •  1.3 数据生成与结果计算。
  • 2、 概要设计
  • 2.1 结构设计
  • 2.2 算法流程
  • 3、 测试
  • 4、 调试
  • 总结


前言

24点游戏是经典的纸牌益智游戏。
常见游戏规则:
   从扑克中每次取出4张牌。使用加减乘除,第一个能得出24者为赢。
   其中,J代表11,Q代表12,K代表13,A代表1
基本要求: 
   随机生成4个代表扑克牌牌面的数字字母,程序自动列出所有可能算出24的表达式,用Java实现程序解决问题。

1、 算法分析

将玩家的输入保存到一个string变量中,与计算机自动产生的数据进行匹配,如果结果包含则认为用户答对,将用户积分递增1,如果输入为空或null或者不匹配则认为玩家答案错误,将玩家血量递减1。
TimeUtils负责整个游戏的定时计算,是一个后台线程,可以调用主线程中的数据进行分析,看是否达到退出游戏的条件,如果满足则直接进行调用主线程的退出方法,进行线程的关闭并结束进程。否则查看当前的时间计数单位是否超时,如果超时,则调用主线程的outTime()方法进行玩家血量的递减,否则继续进行游戏数据的监听更新。
CardUtils有两个字典映射num2card、card2num,负责转换牌面 1-K 对应的数字,方便游戏的计算
由generateNumbers()方法对数据进行生成,先生成 1-13 的4个数字再将数字转换为对应的牌面对用户进行展示,如果当前的4个数不能满足24点的规律则会从新生成,直到至少有一个答案为止。
calculateResult()方法对生成的4个随机数进行穷举所有可能,并计算相应的结果,如果满足24点规律则将当前的排列进行保存,否则舍弃掉。运算规则是先从4个数中抽取两个数进行四则运算,再将结果和剩下的数据进行合并再次计算,直到只有一个数据为止,防止出现数据重合的现象,采取不回头的方式进行,从左到右开始数据的选取,如果已经有了数据的组合,则后面的运算就不在进行。结果生成之后进行清洗,去除多余的括号,并检查相似的结构将其提出。

2、 概要设计

2.1 结构设计

App类,为算法的入口类

包含10个成员变量(random、faceContent、trueResult、blood、score、playingInfo、flag、timer、timeUtils)分别负责随机数的生成,牌面结果保存,玩家输入结果保存,计算结果保存,玩家的血量和积分,玩家游戏中产生的数据信息,及3个定时器相关变量;
19个成员方法(startGame()、savePlayingInfo()、generateNumbers()、outTime()、calculateResult()、clearn()、calcute()、saveResult)()。。。)分别用于程序的入口,游戏的管理,数据生成,结果计算,游戏数据的本地保存,玩家交互,及各个变量所需的getter、setter方法。
package com.beordie;

import com.beordie.utils.CardUtils;
import com.beordie.utils.TimeUtils;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.*;

/**
 * @Classname App
 * @Description 游戏实现主类
 * @Date 2021/5/8 22:30
 * @Created 30500
 */
public class App {
    // 随机生成器
    private static Random random = new Random();
    // 牌面保存
    private static List<String> faceContent = new ArrayList<>();
    // 存储用户输入的运算表达式
    private static String buffer;
    // 存储正确结果
    private static List<String> trueResult = new ArrayList<>();
    // 玩家血量 积分
    private static int blood = 3;
    private static int score = 0;
    // 保存玩家的数据信息
    private static List<String> playingInfo = new ArrayList<>();
    // 定时器
    private static Integer flag = 0;
    private static Timer timer = new Timer();
    private static TimeUtils timeUtils = new TimeUtils();

    /**
     * @description 入口函数
     * @author 30500
     * @date 2021/5/9 9:47
     * @type [java.lang.String[]]
     * @return void
     */
    public static void main(String[] args) {
        // 开始游戏
        startGame();
    }

    /**
     * @description 游戏控制器
     * @author 30500
     * @date 2021/5/9 12:37
     * @type []
     * @return void
     */
    public static void startGame() {
        while (blood > 0) {
            // 生成随机数
            generateNumbers();
            // 定时器初始化
            if (flag == 0) {
                timer.schedule(timeUtils, 0 , 1000);
                flag = 1;
            }
            // 玩家输入
            input();
            // 检查玩家输入是否正确
            if (checkInput()) {
                System.out.println("答对了,加一分");
                playingInfo.add(buffer + "  正确;");
                score++;
            } else {
                System.out.println("答错了,血量减一");
                playingInfo.add(buffer + "  错误;");
                blood--;
            }
            timeUtils.setTime(15);
        }
        // 取消定时器任务
        timer.cancel();
        exit();
    }

    /**
     * @description 追加添加玩家的游戏信息
     *              一行表示一个玩家记录
     * @author 30500
     * @date 2021/5/9 13:03
     * @type []
     * @return void
     */
    public static void savePlayingInfo() {
        try{
            BufferedWriter br = new BufferedWriter(new FileWriter("src/TopList.txt", true));       //数据保存在本地
            for(int i = 0;i< playingInfo.size();i++){
                br.write(playingInfo.get(i)+"\t");
            }
            br.write("\n");
            br.close();
        }catch(Exception e){
            e.printStackTrace();
        }finally {
            // 游戏结束直接释放所有任务
            System.exit(0);
        }
    }

    /**
     * @description 生成四个代表牌面的随机数
     * @author 30500
     * @date 2021/5/8 22:51
     * @type []
     * @return void
     */
    public static void generateNumbers() {
        faceContent.clear();
        // 保存随机数临时变量
        int[] num = new int[4];
        // 生成四个随机数
        for (int i = 0; i < 4; i++) {
            // 生成 1-13 的随机数
            num[i] = random.nextInt(13) + 1;
            // 将随机数对应的牌面保存
            faceContent.add((String) CardUtils.num2card.get(num[i]));
        }
        // 输出结果给用户
        calculateResult(num[0], num[1], num[2], num[3]);
        // 判断当前牌面是否可解
        if (trueResult.size() > 0) {
            print();
            saveResult();
            return;
        }
        // 不可解重新递归生成
        generateNumbers();
    }

    /**
     * @description 输出牌面结果给用户
     * @author 30500
     * @date 2021/5/8 23:06
     * @type []
     * @return void
     */
    public static void print() {
        System.out.println("扑克牌牌面为");
        for (String key : faceContent) {
            System.out.printf("%-8s", key);
        }
        System.out.println();
    }

    /**
     * @description 接收用户输入
     * @author 30500
     * @date 2021/5/8 23:06
     * @type []
     * @return void
     */
    public static void input() {
        // 输入设备句柄
        Scanner input = new Scanner(System.in);
        // 接收用户输入
        System.out.println("请输入:");
        buffer = input.nextLine();
    }

    /**
     * @description 游戏超时更新数据
     * @author 30500
     * @date 2021/5/9 12:40
     * @type []
     * @return void
     */
    public static void outTime() {
        blood--;
        playingInfo.add("******" + "  超时;");
        System.out.println("游戏超时,血量减一");
    }

    /**
     * @description 计算可能性结果
     *              采取不回头的方法进行,每个数据从头到尾,一次展开,不回头计算
     * @author 30500
     * @date 2021/5/9 12:41
     * @type [int, int, int, int]
     * @return void
     */
    public static void calculateResult(int num1, int num2, int num3, int num4) {
        // 表示四个牌面
        StringBuffer face1 = new StringBuffer(CardUtils.num2card.get(num1));
        StringBuffer face2 = new StringBuffer(CardUtils.num2card.get(num2));
        StringBuffer face3 = new StringBuffer(CardUtils.num2card.get(num3));
        StringBuffer face4 = new StringBuffer(CardUtils.num2card.get(num4));
        int resultNum = 0;
        for (int i = 0; i < 4; i++) {
            // 取出运算符
            char operator1 = CardUtils.arithmetic[i];
            // 第1次计算,先从4个数中任意选择2个进行计算,将计算结果再次参加运算
            // 先选第一,和第二个数进行计算
            int firstResult = calcute(num1, num2, operator1);
            // 先选第二和第三两个数进行计算
            int secondResult = calcute(num2, num3, operator1);
            // 先选第三和第四俩个数进行计算
            int thirdResult = calcute(num3, num4, operator1);
            for (int j = 0; j < 4; j++) {
                // 取出运算符
                char operator2 = CardUtils.arithmetic[j];
                // 第2次计算,从3个数中选择2个进行计算
                int firstMidResult = calcute(firstResult, num3, operator2);
                int firstTailResult = calcute(num3, num4, operator2);
                int midFirstResult = calcute(num1, secondResult, operator2);
                int midTailResult = calcute(secondResult, num4, operator2);
                int tailMidResult = calcute(num2, thirdResult, operator2);
                for (int k = 0; k < 4; k++) {
                    // 取出运算符
                    char operator3 = CardUtils.arithmetic[k];
                    //第3次计算,计算两个数的结果
                    if(calcute(firstMidResult, num4, operator3) == 24)
                        trueResult.add("((" + face1 + operator1 + face2 + ")" + operator2 + face3 + ")" + operator3 + face4);
                    if(calcute(firstResult, firstTailResult, operator3) == 24)
                        trueResult.add("(" + face1 + operator1 + face2 + ")" + operator3 + "(" + face3 + operator2 + face4 + ")");
                    if(calcute(midFirstResult, num4, operator3) == 24)
                        trueResult.add("(" + face1 + operator2 + "(" + face2 + operator1 + face3 + "))" + operator3 + face4);
                    if(calcute(num1,midTailResult, operator3) == 24)
                        trueResult.add("" + face1 + operator3 + "((" + face2 + operator1 + face3 + ")" + operator2 + face4 + ")");
                    if(calcute(num1,tailMidResult,operator3) == 24)
                        trueResult.add("" + face1 + operator3 + "(" + face2 + operator2 + "(" + face3 + operator1 + face4 + "))");
                }
            }
        }
        // 进行数据清洗
        clearn();
    }

    /**
     * @description 清洗多余的数据结果
     * @author 30500
     * @date 2021/5/9 12:43
     * @type []
     * @return void
     */
    private static void clearn() {
        List<String> tempResult = new ArrayList<>();
        String temp = null;
        // 清除数据结构中多余的()符号
        for (int i = 0; i < trueResult.size(); i++) {
            temp = trueResult.get(i);
            if ((temp.indexOf('-') == -1 && temp.indexOf('+') == -1) || (temp.indexOf('*') == -1 && temp.indexOf('/') == -1)) {
                temp = temp.replaceAll("[()]", "");
            }
            if (tempResult.indexOf(temp) == -1) {
                tempResult.add(temp);
            }
        }
        trueResult.clear();
        trueResult = tempResult;

    }

    /**
     * @description 判断用户输入是否正确
     * @author 30500
     * @date 2021/5/9 9:03
     * @type []
     * @return boolean
     */
    public static boolean checkInput() {
        // 检查数据是否合理
        if (buffer == null) {
            return false;
        }
        // 判断是否是正确答案
        if (trueResult.indexOf(buffer.trim()) == -1) {
            return false;
        }
        return true;
    }

    /**
     * @description 两个数之间的四则运算
     * @author 30500
     * @date 2021/5/9 9:46
     * @type [int, int, char]
     * @return int
     */
    public static int calcute(int count1, int count2, char operator) {
        // 加
        if (operator == '+') {
            return count1 + count2;
        }
        // 减
        else if (operator == '-') {
            return count1 - count2;
        }
        // 乘
        else if (operator == '*') {
            return count1 * count2;
        }
        // 除
        else if (operator == '/' && count2 != 0)  {
            return count1 / count2;
        }
        return -1;
    }

    /**
     * @description 保存当前牌面的正确结果
     * @author 30500
     * @date 2021/5/9 12:43
     * @type []
     * @return void
     */
    public static void saveResult() {
        try{
            BufferedWriter br = new BufferedWriter(new FileWriter("src/result.txt"));       //数据保存在本地
            for(int i = 0;i< trueResult.size();i++){
                br.write(trueResult.get(i)+"\n");
            }
            br.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public static void exit() {
        System.out.println("游戏结束,玩家得分:" + score);
        savePlayingInfo();
    }
    public static int getBlood() {
        return blood;
    }

    public static void setBlood(int blood) {
        App.blood = blood;
    }

    public static int getScore() {
        return score;
    }

    public static void setScore(int score) {
        App.score = score;
    }

    public static Integer getFlag() {
        return flag;
    }

    public static void setFlag(Integer flag) {
        App.flag = flag;
    }
}
CardUtils类是游戏的工具类,负责数字与牌面点数的转换,主要有三个静态成员变量num2card、card2num保存数据间的映射关系,arithmetic负责存储游戏中需要使用的(+、-、*、/)四则运算,通过下标即可访问相应的运算符,提供一个私有构造方法不允许外部进行对象的实例化,所有变量通过类名直接进行数据的引用。
package com.beordie.utils;

import java.util.HashMap;
import java.util.Map;

/**
 * @Classname Cards
 * @Description 实现扑克牌点数的类
 * @Date 2021/5/8 22:32
 * @Created 30500
 */
public class CardUtils {
    // 存储 A-K 13张牌面
    public static Map<Integer, String> num2card = new HashMap<>();
    // 存储 1-13 13个牌面点
    public static Map<String, Integer> card2num = new HashMap<>();
    // 四则运算表达式
    public static char[] arithmetic;
    static {
        arithmetic = new char[]{'+', '-', '*', '/'};
        num2card.put(1, "A");
        num2card.put(2, "2");
        num2card.put(3, "3");
        num2card.put(4, "4");
        num2card.put(5, "5");
        num2card.put(6, "6");
        num2card.put(7, "7");
        num2card.put(8, "8");
        num2card.put(9, "9");
        num2card.put(10, "10");
        num2card.put(11, "J");
        num2card.put(12, "Q");
        num2card.put(13, "K");

        card2num.put("A", 1);
        card2num.put("2", 2);
        card2num.put("3", 3);
        card2num.put("4", 4);
        card2num.put("5", 5);
        card2num.put("6", 6);
        card2num.put("7", 7);
        card2num.put("8", 8);
        card2num.put("9", 9);
        card2num.put("10", 10);
        card2num.put("11", 11);
        card2num.put("12", 12);
        card2num.put("13", 13);
    }

    /**
     * @description 构造方法私有,不允许外部进行类的实例化
     * @author 30500
     * @date 2021/5/8 22:38
     * @type []
     * @return
     */
    private CardUtils() {};
}
TimeUtils类是游戏的计数线程,继承TimerTask类,作为App类的一个成员变量使用,包含一个成员变量用于设定游戏的规则时间,run()方法实现计数的功能并进行游戏的时间检查,每次调用该后台线程时将时间计数减一,当其小于等于0时,认为当前游戏时间已经结束,玩家的生命递减一但是不影响玩家的继续答题,当然如果玩家的生命已经低于1时将控制全部交给主线程进行管理,退出当前的游戏并保存相关的游戏信息和打印相关的游戏数据。
package com.beordie.utils;

import com.beordie.App;

import java.util.TimerTask;

/**
 * @Classname TimeUtils
 * @Description 定时器计算时间
 * @Date 2021/5/9 10:04
 * @Created 30500
 */
public class TimeUtils extends TimerTask {
    // 游戏时长 15s
    private Integer time;
    // 结束标记
    private Boolean exit;

    @Override
    public void run() {
        // 每隔一秒时间减一
        this.time--;
        if (this.time <= 0) {
            // 调用超时方法
            App.outTime();
            if (App.getBlood() > 0) {
                this.time = 15;
            } else {
                System.out.println("游戏结束");
                cancel();
                App.exit();
            }
        }
    }

    public TimeUtils() {
        this.time = 15;
        this.exit = false;
    }

    @Override
    public boolean cancel() {
        return super.cancel();
    }

    public void setExit(Boolean exit) {
        this.exit = exit;
    }

    public void setTime(Integer time) {
        this.time = time;
    }
}

2.2 算法流程



纸牌游戏java代码 java 纸牌游戏_算法

流程图


3、 测试

针对算法的回答超时、回答错误、回答正确三个交互功能进行测试



纸牌游戏java代码 java 纸牌游戏_算法_02

超时输入

纸牌游戏java代码 java 纸牌游戏_纸牌游戏java代码_03

错误输入

纸牌游戏java代码 java 纸牌游戏_java_04

正确输入

4、 调试



纸牌游戏java代码 java 纸牌游戏_数据_05

定时器跟踪

纸牌游戏java代码 java 纸牌游戏_数据_06

数据跟踪

总结

  最大的难点是对结果的计算,采取暴力穷举的办法对所有可能性进行一次计算,从中取出结果正确的排列组合,也有的数字组合去掉相应的括号之后是重复的也需要进行数据的筛除检查。通过对定时器的使用来达到整个算法的计时功能,增加了对多线程工作的理解。