
简介这是一套基于 Java 开发《大富翁》游戏的完整工程源码面向 Java 学习者、课程设计及游戏开发入门者。代码完整覆盖经典大富翁核心流程双人角色与头像设置、地图选择、游戏天数与胜利金额设定、初始金钱配置、破产失败判定以及掷骰子随机效果和角色按步数移动的呈现不同格子还实现独立的策略算法可帮助理解游戏循环、事件响应与面向对象设计地图购地、升级、过路费扣款等常见机制也均有对应实现。压缩包共 576 个文件包含 68 个 Java 源文件、90 个 class 文件、341 张 PNG 图片以及 jpg、xml、gif 等素材与工程配置整包 26.03 MB目录结构清晰导入开发环境即可阅读或二次开发。目前已有 2265 人学习下载适合需要完整项目源码作为实训参考、快速上手桌游类游戏开发的学习者。1. 一套能跑通的《大富翁》Java 源码到底值不值得读“大富翁”三个字听起来像新手玩具但真正动手用 Java 完整实现一遍你才会发现它把对象建模、状态机、事件分发、随机数和线程协作全压在一套源码里。标题里这整套源码的价值不在游戏本身而是提供一个能逐步对照学习的完整程序骨架哪些类负责数据、哪些类负责规则、哪些类只负责渲染分清楚这三层比游戏跑通更重要。这里以一次课堂上完成的模拟项目X为参照从类设计讲到主循环再落到 Swing 图形界面最后把新手最容易翻车的五个坑单独列出来适合想把 Java 面向对象能力落到一个完整可玩的棋盘游戏上的开发者。2. 对象模型设计棋盘、格子和玩家的抽象从哪开始2.1 棋盘与格子的抽象为什么每个格子要自成对象大富翁棋盘上有起点、普通地产、命运卡、监狱等多种格子行为完全不同。如果用一个 int 数组存格子类型再用 switch 在移动逻辑里分发前期写起来很快一旦要加地产升级、租金联动或者突发事件switch 分支会膨胀到难以维护。常见做法是先抽象一个 Cell 基类让每种格子各自实现同一个 arrive 入口棋盘只需要知道“玩家到达某个格子就把控制权交给那个格子”。public abstract class Cell { protected final String name; protected final int position; public Cell(String name, int position) { this.name name; this.position position; } public String getName() { return name; } public int getPosition() { return position; } public abstract void arrive(Player player, GameBoard board); }这个抽象基类把每个格子的公共属性收进来名字和位置。name 负责界面显示和日志输出position 用于棋盘顺序查询。arrive 是唯一的业务入口所有格子的具体行为都从这里进入。代码里没有实现体由具体格子覆盖。设计成抽象类而不是接口是因为所有格子确实共享 name 和 position 这两个字段把它们放到基类里可以让每个子类少写两行重复代码后续要加公共属性也只需要改一处。格子类型核心职责需要的关键属性起点格经过或到达时发放工资工资数额地产格买卖、收租、升级价格、基础租金、等级事件格随机抽卡并执行效果卡组列表惩罚格扣款或暂停回合罚款金额接下来看最核心的地产格子它承担了买卖、租金、升级三个业务。具体实现public class EstateCell extends Cell { private Player owner; private final int price; private final int baseRent; private int level; public EstateCell(String name, int position, int price, int baseRent) { super(name, position); this.price price; this.baseRent baseRent; this.level 0; } Override public void arrive(Player player, GameBoard board) { if (owner null) { if (player.getCash() price) { System.out.println(player.getName() 到达无主地产 name 价格 price); board.getBuyStrategy().shouldBuy(player, this); } } else if (owner ! player) { player.payTo(owner, getRent()); } } public void setOwner(Player newOwner) { this.owner newOwner; } public int getPrice() { return price; } public int getLevel() { return level; } public void upgrade() { level; } public int getRent() { return baseRent * (level 1); } }arrive 方法里分三种情况无主地产、自己地产、别人地产。无主地产先判断现金够不够够则交给购买策略决定别人地产直接用 payTo 完成租金转账。注意 getRent() 不是字段而是实时计算这样升级后所有使用方自动拿到新租金不会出现缓存不一致。这里的关键参数是 price 和 baseRentprice 控制能不能买baseRent 控制基础租金level 是升级次数。把租金写成 baseRent * (level 1) 是简化版规则适合做可玩原型之后要改阶梯租金只需要动 getRent。2.2 玩家与骰子资金、位置、破产判断怎么建模玩家的状态比看起来多名字、现金、当前位置、持有的地产列表、破产标记。把现金和破产判断都放进 Player 类比在主循环里反复查询更可靠因为游戏里多个地方会触发扣款收租、罚款、事件卡如果每处都写 if (cash 0) 很容易漏。集中到一个方法里所有扣款行为就都走同一条路径。import java.util.ArrayList; import java.util.List; public class Player { private final String name; private int cash; private int position; private final ListEstateCell estates new ArrayList(); private boolean bankrupt; public Player(String name, int startCash) { this.name name; this.cash startCash; this.position 0; this.bankrupt false; } public void move(int steps, int boardSize) { position (position steps boardSize) % boardSize; } public void payTo(Player target, int amount) { if (cash amount) { cash - amount; target.cash amount; } else { bankrupt true; cash 0; System.out.println(name 破产欠 target.getName() amount 元); } } public void addEstate(EstateCell cell) { cell.setOwner(this); estates.add(cell); } public int getCash() { return cash; } public boolean isBankrupt() { return bankrupt; } public String getName() { return name; } public int getPosition() { return position; } public int getEstateCount() { return estates.size(); } }move 里写成 position (position steps boardSize) % boardSize这里的加 boardSize 是为了防止负数取模。如果某个格子事件把玩家 position 减到负值直接取模会得到负数索引board.getCell 就会越界。加一次 boardSize 再用 % 包住无论 position 之前是正是负结果都会落回 0 到 boardSize-1 区间。这是个小细节但很多人的源码就翻车在这个地方。payTo 是资金流转的唯一入口amount 是转账金额目标必须是另一个玩家。现金不足时直接把 bankrupt 置 true 并把现金清零而不是留下负数余额。负数余额会让后面“是否购买”的判断全部错乱因为 getCash() price 在负数时恒不成立但 cash 还在被继续扣整个账目就乱了。addEstate 接收一个 EstateCell同时把 owner 写进格子这一步要配对好漏了 setOwner 后面收租判断就会把地主自己当成租客。骰子类比较简单但随机数生成方式要选对import java.util.Random; public class Dice { private final Random random new Random(); private int total; public int roll() { int first random.nextInt(6) 1; int second random.nextInt(6) 1; total first second; return total; } public int getTotal() { return total; } }两个 nextInt(6)1 加总分布是 2 到 12 的类正态分布7 点出现概率最高。如果只用单个 nextInt(12)1得到的 1 到 12 是均匀分布游戏里玩家会频繁被高点数碾压节奏失真。total 字段保留当次结果供界面显示本轮点数下一次 roll 会覆盖它不会污染上一轮数据。startCash 建议统一设在 8000 到 15000 之间太少了玩家刚起手买两块地就破产太多了后期地产等级拉满也看不出紧张感。2.3 GameBoard 的职责与地图构造GameBoard 是棋盘的总持有者它管着格子列表、玩家列表和购买策略。设计时不要让 GameBoard 参与具体规则它只做两件事按索引返回格子、把玩家和格子组装起来。import java.util.ArrayList; import java.util.List; public class GameBoard { private final ListCell cells new ArrayList(); private final ListPlayer players new ArrayList(); private final BuyStrategy buyStrategy; public GameBoard(BuyStrategy buyStrategy) { this.buyStrategy buyStrategy; } public void addCell(Cell cell) { cells.add(cell); } public void addPlayer(Player player) { players.add(player); } public Cell getCell(int index) { if (index 0 || index cells.size()) { throw new IllegalArgumentException(格子索引越界: index); } return cells.get(index); } public int getSize() { return cells.size(); } public ListPlayer getPlayers() { return players; } public BuyStrategy getBuyStrategy() { return buyStrategy; } }getCell 里的越界检查不是摆设。调试阶段如果出现索引异样这里能第一时间暴露问题而不是让程序带着错误索引继续运行到很后面才崩。addCell 和 addPlayer 的顺序很重要玩家的加入顺序就是游戏回合顺序不要中途随意调整。getSize 返回格子总数主循环移动时要用它做取模边界。buyStrategy 在这里被集中持有格子只需要通过 board.getBuyStrategy() 拿到策略即可不关心策略具体是手动还是 AI。3. 主循环与回合控制让游戏从启动到结束自动推进3.1 回合循环的标准写法while 加一回合一步回合控制是大富翁逻辑的主干顺序是掷骰子、移动、到达格子触发事件、检查游戏是否结束、切到下一个玩家。如果按传统思路写一个巨大的 while(true) 循环把所有规则堆在里面代码会越来越难读。更清晰的写法是把“走一回合”拆成独立方法谁需要谁调用控制台模式用 while 调它GUI 模式用 Timer 调它。import java.util.List; public class GameController { private final GameBoard board; private final ListPlayer players; private final Dice dice; private int currentIndex 0; public GameController(GameBoard board) { this.board board; this.players board.getPlayers(); this.dice new Dice(); } public void start() { while (playOneTurn()) { // 循环直到 playOneTurn 返回 false } announceWinner(); } public boolean playOneTurn() { if (isGameOver()) { return false; } Player current players.get(currentIndex); if (!current.isBankrupt()) { int steps dice.roll(); System.out.println(current.getName() 掷出 steps); current.move(steps, board.getSize()); Cell cell board.getCell(current.getPosition()); cell.arrive(current, board); } else { System.out.println(current.getName() 已破产跳过回合); } currentIndex (currentIndex 1) % players.size(); return true; } public boolean isGameOver() { long alive players.stream().filter(p - !p.isBankrupt()).count(); return alive 1; } private void announceWinner() { players.stream() .filter(p - !p.isBankrupt()) .findFirst() .ifPresent(w - System.out.println(胜者 w.getName())); } }playOneTurn 每次只执行一个玩家的一个回合返回布尔值表示游戏是否还能继续。控制台版本的 start 用 while 接收返回值GUI 版本直接把 playOneTurn 丢给 Timer 的回调同一个方法两种用法都能覆盖。isGameOver 统计存活玩家数存活数小于等于 1 就结束。currentIndex 用取模从列表尾部回到头部形成环形轮转。注意破产玩家在 else 分支被跳过不会掷骰也不会触发格子事件这跟后面排查“破产玩家还在继续交易”直接相关。dice.roll 返回 2 到 12 的点数current.move 里传的 board.getSize() 是格子总数确保移动后位置不出界。如果以后要加“经过起点发工资”这种规则需要在 move 前后记录旧位置和新位置判断是否发生跨圈这属于后续扩展点。3.2 买卖地产、租金结算与事件卡处理上一章的 EstateCell 里提到 board.getBuyStrategy()这个设计把“玩家要不要买地”从格子逻辑里拆出去。原因很实际手动模式下要弹窗或控制台读取AI 模式下要按资金策略自动决定如果写在 EstateCell 里每次加新玩法都要改格子类。拆成策略接口之后买地决策可以随时替换。FunctionalInterface public interface BuyStrategy { boolean shouldBuy(Player player, EstateCell cell); } public class SimpleAiBuyStrategy implements BuyStrategy { Override public boolean shouldBuy(Player player, EstateCell cell) { // 现金超过价格两倍才买留出应急资金 return player.getCash() cell.getPrice() * 2; } }SimpleAiBuyStrategy 里的乘以 2 是保底参数避免 AI 在资金紧张时还强行买地。实际调参时建议设成 1.5 到 3 之间设太大会导致 AI 全程贫穷设太小 AI 容易在买地后马上破产。这个值可以在试玩时从日志里观察 AI 现金曲线再调整。手动模式则让 shouldBuy 弹出确认框返回值由用户决定两种模式共用同一套格子逻辑。事件卡格子是棋盘上另一类典型结构。它自己不持有具体效果而是从卡池中随机抽一张再执行import java.util.List; import java.util.Random; public class EventCell extends Cell { private final ListEventCard cards; private final Random random new Random(); public EventCell(String name, int position, ListEventCard cards) { super(name, position); this.cards cards; } Override public void arrive(Player player, GameBoard board) { EventCard card cards.get(random.nextInt(cards.size())); card.apply(player); } }cards 是外部传入的卡组列表比如“前进三步”“交税 500”“获得 2000”。random.nextInt(cards.size()) 从 0 到 cards.size()-1 之间均匀取一个索引保证每张卡被抽中的概率一致。card.apply(player) 是事件卡接口的统一调用效果差异被封装在各自实现里替换、删卡都不影响格子逻辑。EventCard 接口定义如下FunctionalInterface public interface EventCard { void apply(Player player); } public class MoneyCard implements EventCard { private final int amount; public MoneyCard(int amount) { this.amount amount; } Override public void apply(Player player) { // amount 为正则加钱为负则扣钱 if (amount 0) { player.receive(amount); } else { player.payToBank(-amount); } } }这里引入 receive 和 payToBank 两个方法是 Player 类里适合补的小工具。receive 直接加现金payToBank 在现金不足时走破产逻辑它们的实现和 payTo 类似只是收款方从玩家变成银行。到这里核心循环已经能完整跑通一局多个玩家轮流掷骰、落地触发格子、买卖地产、收租金、抽事件卡而且完全不依赖界面可以直接在控制台用 System.out 调试。下一步再加图形界面时这些核心代码一行都不用改。4. 叠上 Swing 图形界面UI 与逻辑分离的落地做法4.1 为什么先跑通控制台版再写窗口界面很多第一次做完整游戏源码的人都想把界面放在第一步但结果往往是花在“窗口画布怎么画棋子”上的时间远超写逻辑本身最后核心规则反而只写了一半。常见做法是先把控制台版跑通让游戏能在纯文本模式下从开局到结束这样至少能证明业务逻辑是对的。控制台的输出像“张三 掷出 7张三 到达 商业街商业街 当前拥有者 李四租金 160”这就是最好的调试日志也是之后 GUI 版本验证功能的基准。控制台版验证要点有两个一是每个玩家行动顺序是否轮转正确二是破产玩家的流程是否被跳过。把 System.out 的输出保存下来跑完一整局人工扫一遍比直接调试界面代码快得多。核心逻辑没问题之后GUI 就变成一个渲染器和事件转发器复杂度立刻降下来。这里顺手给了 SimpleAiBuyStrategy 配一套默认代码控制台模式下第一个玩家走 AI 策略第二个玩家走手动策略就能边打边观察界面表现。控制台版本常见的问题还有 Scanner 阻塞。如果在主循环里用 Scanner.readLine() 等玩家输入又没加超时机制一局游戏可能卡在等待输入上。建议手动模式下把输入放在回合开始前并用“输入 y 买 / n 不买”这种单字指令减少输入出错概率。调试阶段可以全程用 AI 策略跑避免人工输入干扰流程验证。4.2 Swing 单线程模型与 Timer 驱动的回合推进Swing 是单线程模型所有界面更新只能在 EDT 上执行。如果把上一章的 start() 里的 while 循环直接放进主线程执行程序会一直占住主线程界面永远画不出来窗口直接变成“未响应”。我第一次写时就翻过这个车控制台游戏跑得好好的套上 JFrame 之后窗口卡死关闭都点不动原因就是 while 循环阻塞了 EDT。正确做法是把回合推进交给 javax.swing.Timer。Timer 每经过指定毫秒数触发一次回调回调里只执行一步 playOneTurn然后请求重绘import javax.swing.*; import java.awt.*; public class GamePanel extends JPanel { private final GameController controller; private final JLabel statusLabel; public GamePanel(GameController controller) { this.controller controller; this.statusLabel new JLabel(游戏进行中); setLayout(new BorderLayout()); add(statusLabel, BorderLayout.NORTH); Timer timer new Timer(800, e - { if (controller.isGameOver()) { ((Timer) e.getSource()).stop(); statusLabel.setText(游戏结束); repaint(); return; } controller.playOneTurn(); repaint(); }); timer.start(); } Override protected void paintComponent(Graphics g) { super.paintComponent(g); drawBoard(g); drawPlayers(g); } private void drawBoard(Graphics g) { // 从 GameBoard 读取格子列表按坐标数组逐格绘制 // 坐标映射见下方说明 } private void drawPlayers(Graphics g) { // 按玩家 position 取格子坐标绘制棋子圆点 } }Timer 第一个参数 800 是每个回合间隔的毫秒数。设太短比如 100游戏会快到来不及看出谁动了设太长比如 3000观感又太拖沓。建议回合间隔设在 500 到 1000 之间开发调试时可以临时设成 100 加速跑完整局验证逻辑。e.getSource() 拿到触发定时器的对象在游戏结束时 stop 掉它避免 Timer 继续空转。这里的关键是回调函数里不要做耗时操作一帧只推进一个回合。棋盘绘制最容易踩的坑是坐标映射。如果棋盘是方形一圈的布局可以把一维格子索引映射到二维画布坐标。常见做法是准备两个数组 cellX 和 cellY按格子索引依次填入矩形四边的坐标绘制时直接取数组值。玩家棋子则根据 position 取坐标同一格多个玩家时加上固定偏移避免棋子全部叠在一起。private int[] cellX new int[12]; private int[] cellY new int[12]; private int offset 6; // 假设是 12 格方形棋盘初始化时按顺序填充坐标 private void initCellPositions(int panelWidth, int panelHeight, int cellSize) { // 上边一行、右边一列、下边一行、左边一列依次赋值 }paintComponent 里的绘制代码只负责“画当前状态”不要放任何游戏逻辑。这样重绘速度不会影响回合推进两者解耦后想加骰子动画或者小地图都容易得多。注意 Swing 里不要用 Thread.sleep 做回合等待它会阻塞 EDT 导致界面冻结。所有延时都通过 Timer 完成这是 Swing 开发里最基础也最重要的一条纪律。5. 大富翁 Java 源码避坑指南从随机数到界面卡死的五个问题5.1 骰子随机数不均匀游戏节奏失真现象日志里经常连续出现大点数玩家一回合绕半圈地产还没来得及买就回到起点整体节奏偏差很大测试几局发现游戏结束时间很不稳定。原因random.nextInt(12)1 会产生 1 到 12 的均匀分布。真实双骰子得到 2 到 12且 7 出现概率最高两种分布下游戏进程完全不一样。均匀分布会让极端点数频繁出现拉高移动速度破坏地产积累的过程。解决按两个骰子分别掷再相加。上文 Dice 类已经采用 first nextInt(6)1 再 second nextInt(6)1得到 2 到 12 的类正态分布。如果想让玩家更容易破产可以在事件卡或租金规则上加难度系数但不要直接改随机数生成方式那会毁掉分布特性。5.2 破产玩家还在被询问“是否购买”现象玩家现金已经被扣到负数日志里却还在打印“是否购买某某地产”甚至负资产玩家还能继续交租金、升级房屋。原因payTo 方法里只做了 cash - amount没有判断现金是否不足也没有设置破产标记主循环拿到破产玩家后照常调用格子事件格子事件还在尝试触发购买流程。两处遗漏互相掩盖只在主循环里判断不够只在付款方法里判断也不够。解决在 payTo 里加上现金不足时的破产分支把 bankrupt 置 true、cash 清零主循环的 playOneTurn 里用 if (!current.isBankrupt()) 包裹掷骰和事件触发逻辑。两个修复缺一不可只改一个会在另一个地方复现问题。调试时可以在每回合结束时打印所有玩家的 cash 和 bankrupt 状态快速确认修复是否到位。提示破产判断不要写成 cash 0因为某些规则下现金恰好为 0 但还没破产是合法的。用独立的 boolean 标记最稳妥。5.3 Swing 窗口打开后立刻卡死按钮无响应现象JFrame 显示后窗口能画出来但鼠标点任何按钮都没反应窗口拖动也卡顿控制台反而还在持续输出回合日志。原因游戏主循环直接放在主线程里无限循环Swing 的 EDT 永远等不到执行机会所有事件都堆积在队列里。控制台有输出说明逻辑还在跑但界面已经和逻辑失联。解决把 while 循环改掉回合推进交给 Timer每触发一次只执行一步 playOneTurn。界面刷新走 repaint()由 Swing 内部调度。主线程只负责创建和显示窗口不做任何长时间占用的工作。如果还有别的耗时操作比如文件读取也要放到独立线程或 SwingWorker 里不要堵在 EDT 上。5.4 移动计算出现负索引棋盘数组越界现象玩家在某个格子上抽到“后退八步”的事件卡日志显示 position 变成 -2随后 board.getCell(-2) 抛 ArrayIndexOutOfBoundsException。原因move 里直接写 position (position steps) % boardSize当 position 加 steps 变成负数时Java 的 % 结果也可能是负数。-2 % 12 等于 -2取不到正索引。很多教程代码里没考虑回退类事件卡这里的坑被隐藏了。解决改成 position (position steps boardSize) % boardSize。加一次 boardSize 后负数取模也能回落到正区间。这个写法对正数也不影响结果可以直接放在 move 里统一处理。同时要在 GameBoard.getCell 里保留越界检查双保险能够快速暴露其他路径传入的非法索引。5.5 地产升级后租金没有变化现象玩家购买地产后连续升级日志里显示升级成功但别人踩到该地产时扣的钱还和初始值一样。原因租金被存在字段里比如 rent 属性升级时改了 rent但付款方读取的是另一个缓存字段或者某个局部变量或者升级代码只改了 level而读取租金的地方还在用旧的 baseRent。两处数据不同步升级动作看起来成功了但实际没生效。解决不存租金字段只在需要时实时计算 getRent()所有读取方都走这个方法。升级只需要改 level租金自然跟着变。这套模式下不存在缓存副本也就不可能出现两处数据不一致的问题。如果以后租金规则变复杂可以把租金计算抽成单独的规则类但实时计算的原则不变。6. 把源码改造成自己的AI 策略、自定义地图与一轮调试法进阶方向一是让 AI 更聪明。最简单的买地策略是现金超过价格两倍就买但玩多了会发现这种 AI 只会傻买不考虑后续现金压力。可以改成综合判断地产越多越敢出手资金紧张时主动放弃。public class BalancedAiBuyStrategy implements BuyStrategy { Override public boolean shouldBuy(Player player, EstateCell cell) { if (player.getCash() cell.getPrice()) { return false; } double score 0.3 player.getEstateCount() * 0.05; return player.getCash() cell.getPrice() * (1 score); } }这个策略用 score 充当风险系数地产数量越多AI 越敢押注但对现金的要求不是固定倍数而是动态浮动。调参时注意 score 过大时 AI 会越来越谨慎小地图里不容易成交。进阶方向二是自定义地图。不想每次硬编码几十个格子就把地图数据放到外部配置文件。按行读取空白行和 # 开头的注释直接跳过每一行用逗号分隔单元格属性。public static GameBoard loadBoard(String path, BuyStrategy strategy) throws IOException { GameBoard board new GameBoard(strategy); for (String line : java.nio.file.Files.readAllLines(java.nio.file.Paths.get(path))) { if (line.isBlank() || line.startsWith(#)) { continue; } String[] parts line.split(,); // parts[0] 位置索引, parts[1] 类型, parts[2] 名称, // parts[3] 价格, parts[4] 基础租金 // 按类型分支构造 EstateCell / EventCell / StartCell 并 addCell } return board; }验证方面我推荐“一轮调试法”写一个测试入口把玩家人数设成三四个、棋盘缩到 8 格、Timer 间隔调到 100 毫秒开局后只观察前两轮日志。如果两轮内出现顺序错乱、资金异常或位置越界问题一定集中在回合控制或格子事件如果两轮内都正常再放到完整地图上跑。这比开一局完整游戏等几分钟再发现异常要快得多。这套源码的落地路径本质上是先把核心逻辑做成不依赖界面的纯 Java 类再用控制台验证一轮最后用 Swing 包一层视图。我完成自己版本时最深的感受是开头越急着做界面后面越容易返工。先跑通控制台版再谈画布和按钮所有功能都会变得有条理如果再遇到布局问题只需要关心绘制那一小块不会牵连到业务逻辑。希望帮到你。本文还有配套的精品资源点击获取