题目:基于Java GUI编程实现“21点”游戏
开发平台:JKD1.6,Eclipse,Microsoft Windows XP Professional SP2
1 需求分析
本系统主要提供人机对玩21点牌游戏,实现系统自动判断输赢,计算玩家金钱,并保存用户名称,金钱,头像等信息。另附带在玩游戏时提供背景音乐欣赏。
21点游戏基本规则为:每个人最多可拿5张牌,牌的点数在21内(包括21点),玩家点数大的赢,点数相同时庄家赢,玩家点数大于21时不管庄家的点数是什么,玩家都输。A牌可以当1点或11点。当玩家的点数为21点是,其输或着赢的金钱都为点数的双倍。
- 功能需求描述
本系统要求采用Java GUI程序实现一个21点游戏,主要包括如下功能:
- 系统发牌:游戏初始时又系统给电脑方发牌,并给用户初始发2张牌。
- 用户发牌:用户点击发牌按钮,只要用户的牌的点数和小于21就可以获取一张由系统从整副牌剩下的牌中产生的一张随机的牌。
- 开牌:当用户点击开牌按钮后,系统自动计算电脑方和游戏玩家方的牌的点数,判断输赢,计算赌注,游戏金钱出入。
- 用户信息设置:游戏玩家可以从菜单中打开用户设置,初始化游戏金钱,设置昵称,头像等信息
- 用户信息保存:各玩家的姓名,游戏金钱,头像图片等信息都保存在一个xml文件中,游戏开始时有系统自动读取,游戏中数据变更时有系统自动保存,并在游戏中更新。
- 游戏背景音乐:游戏中,用户可以从菜单中选择开启或关闭游戏背景音乐
2 总体设计
-
- 系统类设计与算法描述
- 系统类图
图2.1
图2.2
- 游戏中主要算法
发牌算法:
- 创建一副牌
- 给电脑发牌,只要牌的总点数小于16,牌的张数小于5就继续为电脑发牌,每张牌都是有i中创建的整副牌中随即获取。
- 从i中创建好的牌中删除发出去的牌。
- 给玩家发2张牌,并从删除这2张牌。
点数计算算法:
- 计算出玩家手中的非A的牌的点数和count1,并统计A牌的个数countA。
- 如果A的牌的数量大于0,并且count1+11+countA-1小于等于21则取这个结尾为本步骤的结果;否则取count1+countA为本步骤结果。
- 算法结束。
- 系统功能模块设计
- 界面设计
- 程序设计
3.1Card类程序设计
package game;
public class Card {
static final long serialVersionUID = 1;
private String suit;//花色
private String name;//名称
private int value;//对应的值
private String ico;//牌对应的图片地址
//构造函数,初始化一张牌
Card(String s,String n,int v){
suit = s;
name = n;
value = v;
ico = this.toString() + ".png";
}
//私有属性的访问方法(略)
//重写ToString方法,返回suit和name组成的字符串
public String toString(){
return this.suit + this.name;
}
}
3.2 Cards类程序设计
//包倒入(略)
public class Cards {
static final long serialVersionUID = 1;
private String[] suits = {"红桃","方块","黑桃","梅花"};//花色名称数组
privateString[]names={"A","2","3","4","5","6","7","8","9","10","J","Q","K};
private Vector cards = new Vector();//一副牌
Cards(){
init();
}
//初始化一副整牌(出了大小王)
public void init(){
cards.clear();
for(int i = 0; i < 4; i++){
for( int j = 0; j < 13; j++){
cards.add(new Card(suits[i],names[j],(j>=10) ? 10:(j+1)));
}
}
}
//发牌
public Card deal(){
Random cardIndexRandom = new Random();
int cardIndex = cardIndexRandom.nextInt(cards.size()-1);
Card tempCard = (Card)cards.elementAt(cardIndex);
cards.remove(cardIndex);//从Cards中删去发过的牌
return tempCard;
}
//返回Cards
public Vector getCards(){
return cards;
}
}
3.3 User类程序设计
//包倒入(略)
public class User {
static final long serialVersionUID = 1;
private String name;//名字
private String ico;//头像图片地址
private int money;//金钱
private static String userInfomationFile = "config\\userInformation.xml";//保存信息的xml文件地址
User(){}
//构造函数,构造一个新的用户
User(String n, int m, String i){
name = n;
money = m;
ico = i;
}
//根据名字来初始化一个用户
public void init(String n){
name = n;
money = readUserMoney();
ico = readUserIco();
}
//由xml文件创建一个Document文档对象
private Document creatDoc(){
try{
File f = new File(userInfomationFile);
DocumentBuilderFactoryfactory=DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(f);
return doc;
}catch(Exception e){
System.out.println("ReadError");
return null;
}
}
//将Document文档对象写入磁盘一个xml文件中
public void writeDoc(Document doc){
TransformerFactory tFactory =TransformerFactory.newInstance();
try{
Transformer transformer = tFactory.newTransformer();
DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(new File(userInfomationFile));
transformer.transform(source, result);
}catch(Exception e){
System.out.println("Error");
}
}
//私有属性的访问及设置方法(略)
//从Document中读name
/*
private String readUserName(){
return getUserNode().getFirstChild().getNodeValue();
}
*/
//从Document中读money
private int readUserMoney(){
return Integer.parseInt(getUserNode().getNextSibling().getFirstChild().getNodeValue());
}
//从Document中读ico
private String readUserIco(){
return getUserNode().getNextSibling().getNextSibling().getFirstChild().getNodeValue();
}
//添加user
public void addUser(){
Document XMLDocument = creatDoc();
Element nameNode=XMLDocument.createElement("name");
nameNode.appendChild(XMLDocument.createTextNode(name));
Element moneyNode=XMLDocument.createElement("money");
moneyNode.appendChild(XMLDocument.createTextNode(money + ""));
Element icoNode=XMLDocument.createElement("ico");
icoNode.appendChild(XMLDocument.createTextNode(ico));
Element userNode=XMLDocument.createElement("user");
userNode.appendChild(nameNode);
userNode.appendChild(moneyNode);
userNode.appendChild(icoNode);
XMLDocument.getDocumentElement().appendChild(userNode);
writeDoc(XMLDocument);
}
//判断一个user是否存在
public boolean isUser(String n){
if( getUserNode() != null){
return true;
}
else{
return false;
}
}
//获取user所在的节点
public Node getUserNode(){
Document XMLDocument = creatDoc();
NodeList userList = XMLDocument.getElementsByTagName("user");
for(int i = 0;i < userList.getLength(); i++){if(name.equals(XMLDocument.getElementsByTagName("name").item(i).getFirstChild().getNodeValue())){
Node tempN = XMLDocument.getElementsByTagName("name").item(i);
return tempN;
}
}
return null;
}
//删除user
public void removeUser(){
Document XMLDocument = creatDoc();
XMLDocument.getDocumentElement().removeChild(getUserNode());
writeDoc(XMLDocument);
}
//保存user的数据,然后重新将user初始化一遍
public void save(){
Document XMLDocument = creatDoc();
NodeList userList = XMLDocument.getElementsByTagName("user");
for(int i = 0;i < userList.getLength(); i++){if(name.equals(XMLDocument.getElementsByTagName("name").item(i).getFirstChild().getNodeValue())){
Node tempN = XMLDocument.getElementsByTagName("name").item(i);
//tempN.getFirstChild().setNodeValue("mirong");
tempN.getNextSibling().getFirstChild().setNodeValue(money + "");//保存money
//tempN.getNextSibling().getNextSibling().getFirstChild().setNodeValue("mirong.png");
}
}
writeDoc(XMLDocument);
init(name);
}
}
3.4GameMain类设计
//包倒入(略)
public class GameMain extends JFrame {
static final long serialVersionUID = 1;//每局游戏的一副牌
private Cards gameCards = new Cards(); //玩家得到的牌
private Vector userCards = new Vector();//电脑方得到的牌
private Vector computerCards = new Vector();//牌的图片存放目录
private static String imageFile = "images/";//玩家得到的牌的点数总和
private int userCardsValue = 0;//电脑方得到的牌的点数总和
private int computerCardsValue = 0;//赌注
private int gameMoney = 0;
private JPanel computerCardsPanel = new JPanel();
private JPanel userCardsPanel = new JPanel();
private JPanel centerPanel = new JPanel();
private AudioStream as;
//构造函数
GameMain(){
//窗体界面初始化(略)
}
//私有数据访问方法(略)
public void gameStart(){
//初始化游戏牌
gameCards.init();
//清空原有牌
userCards.clear();
computerCards.clear();
//给电脑方发牌
for(int i = 0 ;i < 5;i++){
countCardsValue(0);
if(getCardsValue(0) < 16)
addCard(0);
}
//给玩家方初始发牌2张
addCard(1);
addCard(1);
}
// 发一张牌
public void addCard(int u){
if(u == 0)
//computerCards.add(new Card("红桃","A",1));
computerCards.add(gameCards.deal());
if(u == 1)
userCards.add(gameCards.deal());
}
// 计算牌的点数总和
public void countCardsValue(int u){
int Anumber = 0;
if(u == 0 ){
computerCardsValue = 0;
if(computerCards.size() > 0){
for(int i = 0;i < computerCards.size();i++){
if(((Card)computerCards.elementAt(i)).getName() == "A"){
Anumber = Anumber + 1;
}
else{
computerCardsValue += ((Card)computerCards.elementAt(i)).getValue();
}
}
if(Anumber > 0){
if((computerCardsValue + 11 + Anumber-1 ) <= 21){
computerCardsValue = computerCardsValue + Anumber-1 + 11;
}
else{
computerCardsValue = computerCardsValue+ Anumber;
}
}
}
}
if(u == 1 ){
userCardsValue = 0;
if(userCards.size() > 0){
for(int i = 0;i < userCards.size();i++){
if(((Card)userCards.elementAt(i)).getName() == "A"){
Anumber = Anumber + 1;
}
else{
userCardsValue += ((Card)userCards.elementAt(i)).getValue();
}
}
if(Anumber > 0){
if((userCardsValue + 11 + Anumber-1 )<= 21){
userCardsValue = userCardsValue + Anumber-1 + 11;
}
else{
userCardsValue = userCardsValue+ Anumber;
}
}
}
}
}
//设置游戏赌注
public void setGameMoney(int v){
gameMoney = v;
}
//显示电脑方的得牌图片
public void displayComputerCards(boolean o){
if(o){
for(int i = 0;i < computerCards.size();i++){
JLabel newLabel = new JLabel("",new ImageIcon(imageFile + ((Card)(computerCards.elementAt(i))).getIco()),JLabel.CENTER);
newLabel.setBounds(70+130*i, 20, 130, 180);
computerCardsPanel.add(newLabel);
}
}
else{
for(int i = 0;i < 5;i++){
JLabel newLabel = new JLabel("",new ImageIcon(imageFile + "backSide.png"),JLabel.CENTER);
newLabel.setBounds(70+130*i, 20, 130, 180);
computerCardsPanel.add(newLabel);
}
}
computerCardsPanel.setBounds(70,20,800,180);
}
//显示游戏玩家方的得牌图片
public void displayUserCards(){
for(int i = 0;i < userCards.size();i++){
JLabel newLabel = new JLabel("",new ImageIcon(imageFile + ((Card)(userCards.elementAt(i))).getIco()),JLabel.CENTER);
newLabel.setBounds(100+135*i, 400, 130, 180);
userCardsPanel.add(newLabel);
}
userCardsPanel.setBounds(100,400,800,180);
}
//播放背景音乐
public void soundPlay(){
try {
FileInputStream fileau=new FileInputStream("images/sound1.mid" );
as=new AudioStream(fileau);
AudioPlayer.player.start(as);
}
catch(Exception e){
System.out.println("读取失败!");
}
}
//关闭背景音乐
public void soundStop(){
AudioPlayer.player.stop(as);
}
}
3.5系统主函数程序设计
public static void main(String[] args){
//创建game,及相关参数创建及初始化(略)
/*--------------界面创建(略)---------------*/
/*--------------------关键事件处理(其余事件处理略)------------*/
//菜单栏按钮事件
//音乐开按钮事件
musicOn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
game.soundPlay();
}
});
//音乐关按钮事件
musicOff.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
game.soundStop();
}
});
// 工具栏按钮事件处理
//发牌按钮事件
b_dealCard.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
game.countCardsValue(1);
if(game.getUserCards().size() < 5 && game.getCardsValue(1)< 21){
game.addCard(1);
game.countCardsValue(1);
game.getUserCardsPanel().removeAll();
game.getUserCardsPanel().updateUI();
}
else if(game.getUserCards().size() < 5 && game.getCardsValue(1) == 21){
String message = "<html><p>你21点啊,还不开牌...</p></html>";
JOptionPane.showMessageDialog(game,message );
}
else if(game.getUserCards().size() < 5){
String message = "<html><p>不好意思!你的点数大于21</p><p>你挂了... -_-</p></html>";
JOptionPane.showMessageDialog(game,message );
}
else{
String message = "<html><p>已经发了5张牌了,不能再发了</p></html>";
JOptionPane.showMessageDialog(game,message );
}
}
});
//重新开局按钮事件
b_newGame.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
//发牌按钮可用
b_dealCard.setEnabled(true);
//开牌按钮可用
b_chechout.setEnabled(true);
//重新初始化游戏
game.gameStart();
//更新空间
game.getComputerCardsPanel(false).removeAll();
game.getComputerCardsPanel(false).updateUI();
game.getUserCardsPanel().removeAll();
game.getUserCardsPanel().updateUI();
//重新设置部分空间显示
l_userValue.setText("");
l_computerValue.setText("");
l_win.setVisible(false);
}
});
//开牌按钮事件
b_chechout.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
//将电脑方牌正面显示
game.getComputerCardsPanel(false).removeAll();
game.getComputerCardsPanel(true).updateUI();
game.countCardsValue(0);
game.countCardsValue(1);
//发牌按钮不可用
b_dealCard.setEnabled(false);
b_chechout.setEnabled(false);
//显示玩家得点
l_userValue.setText(game.getCardsValue(1) + "");
l_computerValue.setText(game.getCardsValue(0) + "");
//设置赌注
game.setGameMoney(Integer.parseInt(T_gameMoney.getText()));
//判断输赢
if(
(game.getCardsValue(1) < 21 && game.getCardsValue(0) < game.getCardsValue(1))
||
(game.getCardsValue(1) < 21 && game.getCardsValue(0) > 21)
){
//玩家赢单倍
l_win.setBounds(470, 340, 33, 44);
gameUser.setUserMoney(gameUser.getUserMoney() + game.getGameMoney());
computerUser.setUserMoney(computerUser.getUserMoney() - game.getGameMoney());
}
else if(game.getCardsValue(0) == 21 ){
//庄家得双倍
l_win.setBounds(470, 220, 33, 44);
computerUser.setUserMoney(computerUser.getUserMoney() + 2 * game.getGameMoney());
gameUser.setUserMoney(gameUser.getUserMoney() + game.getGameMoney());
}
else if(game.getCardsValue(1) == 21 && game.getCardsValue(0) != 21){
//玩家赢双倍
l_win.setBounds(470, 340, 33, 44);
gameUser.setUserMoney(gameUser.getUserMoney() + 2 * game.getGameMoney());
computerUser.setUserMoney(computerUser.getUserMoney() - 2 * game.getGameMoney());
}
else{
//庄家赢单倍
l_win.setBounds(470, 220, 33, 44);
computerUser.setUserMoney(computerUser.getUserMoney() + 2 * game.getGameMoney());
gameUser.setUserMoney(gameUser.getUserMoney() - game.getGameMoney());
}
l_computerMoney.setText(computerUser.getUserMoney() + "");
l_userMoney.setText(gameUser.getUserMoney() + "");
l_win.setVisible(true);
gameUser.save();
computerUser.save();
}
});
//赌注输入事件处理
T_gameMoney.addKeyListener(new KeyAdapter() {
public void keyReleased(KeyEvent arg0) {
int tempMoney = Integer.parseInt(T_gameMoney.getText());
if( tempMoney < 10){
T_gameMoney.setText("10");
}
else if(tempMoney < gameUser.getUserMoney() && tempMoney < computerUser.getUserMoney() ){
game.setGameMoney(tempMoney);
T_gameMoney.setText(tempMoney + "");
}
else if(tempMoney > gameUser.getUserMoney() || tempMoney > computerUser.getUserMoney()){
T_gameMoney.setText(Math.min(gameUser.getUserMoney(),computerUser.getUserMoney()) + "");
}
game.setGameMoney(Integer.parseInt(T_gameMoney.getText()));
}
});
//赌注加事件
b_moneyAdd.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
if((Integer.parseInt(T_gameMoney.getText()) + 10) < computerUser.getUserMoney() &&
(Integer.parseInt(T_gameMoney.getText()) + 10) < gameUser.getUserMoney()){
T_gameMoney.setText(Integer.parseInt(T_gameMoney.getText()) + 10 + "");
game.setGameMoney(Integer.parseInt(T_gameMoney.getText()));
}
}
});
//赌注减事件
b_moneySub.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
if((Integer.parseInt(T_gameMoney.getText()) - 10) > 0)
T_gameMoney.setText(Integer.parseInt(T_gameMoney.getText()) - 10 + "");
else
T_gameMoney.setText("10");
game.setGameMoney(Integer.parseInt(T_gameMoney.getText()));
}
});
game.setVisible(true);
}
- 小结
这个游戏系统比较难及复杂的地方在于游戏过程中的逻辑处理和和解决这些逻辑问题的有效的算法步骤。通过整个程序的代码量的分布也体现了这一点。其基础的几个类Card,Cards,User都只有少量的代码,大量的代码分布在发牌,开局,重新开局等事件处理上。界面的实现也费用了很多代码。而通过实现这个程序,我也发现了自己在逻辑问题处理上的弱势。比如在处理A牌改当1点算,还是当11点算时,我花费了很长时间来理清这个思路。而事实上这个逻辑处理比较简单。
整个系统的结构设计没有抽象好。GameMain这个类在这里是游戏功能的主要类的实现,但是跟mian函数写在一起,又跟界面混在一起,逻辑没有跟界面脱离好。导致这个类的文件代码过长,方法过多,容易产生紊乱,给维护修改带来困难。应该从中抽象出一个专门处理游戏业务逻辑的类,把界面完全分离出来。让系统更灵活些。
Java里的DOM不是完全是基于W3C规范的DOM。Java里的对XML的操作如果用DOM来实现的话,需要另外格式化数据,不然容易出现一些错误的假象,在未格式化的XML Document对象中,换行也会当作一个节点来处理。这让我一贯浏览器端Javascript中的DOM思维变得很无奈,通过断点,逐步查看变量的值才发现换行是个节点。这与规范的DOM不相符合,不过Java里一般会通过自有的方法先格式化XML Document,写入时也要将它格式化再写入磁盘文件。
User类的设计缺乏对xml数据写入的考虑。开始时只想到要从xml文件中取数据,而没有考虑到写入操作,所以User类中的方法很多都是私有的,读取数据的方法。在做写入操作是由creatDoc 方法创建了,Document而getUserNode 方法操作的是另外一个Document,导致数据不一致,所以调用写入方法就毫无意义了。而如果保存了全部数据的话又会导致游戏中创建的用户得不到数据的重新初始化。应该设一个Document私有属性来达到这个类内部的Document数据统一。
课程设计参考书
1 许文宪 懂子建. Java程序设计教程与实训 北京:北京大学出版社,2005.
2 辛运帏 .Java程序设计.北京:清华大学出版社, 2004
3 蔡翠平 .Java程序设计.北京:清华大学出版社,2003