ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java练手项目:控制台钓鱼游戏day01全流程踩坑记录

Java练手项目:控制台钓鱼游戏day01全流程踩坑记录 从零写一个Java钓鱼游戏day01的完整踩坑记录前两天翻代码仓库看到自己刚入门Java时写的day01练习项目忍不住笑出声。那是一个用纯控制台实现的“钓鱼模拟器”没什么高深技术却把我当时学的基础语法、面向对象、集合框架、异常处理全部串在了一起。很多人学Java最容易犯的毛病就是只啃语法不动手看视频觉得自己懂了一写代码就卡壳。这个小项目就是用来治这个毛病的——麻雀虽小五脏俱全特别适合零基础刚刚学完Java语法、想找个练手项目找找手感的人。我把当天的代码翻出来重新跑了一遍顺手修复了几个当年没发现的坑。这篇文章就按照我实际的动手顺序把这个day01从设计、写代码、调试到最终跑通的完整过程讲清楚。你照着抄一遍收获绝对比闷头刷十集视频大。1. 项目整体设计与思路拆解1.1 需求分析这个小项目到底要做什么刚开始学Java的时候很多人都是照着视频里老师的代码敲一遍。可等关掉视频自己面对一个空白页面就不知道该写什么了。我当时给自己的要求很简单做一个能运行、有逻辑、能玩起来的小东西把学过的知识点用上。钓鱼模拟器就是我想到的点子。玩家坐在电脑前输入指令开始钓鱼系统随机生成一条鱼玩家决定是留下还是放生最终目标是钓到一定重量或数量的鱼。这个需求听起来很小但拆开来看需要的Java知识点一点都不少变量与数据类型记录鱼的名字、重量、价格数组或集合存放鱼的种类、玩家的背包面向对象鱼可以设计成类玩家的背包和统计信息可以封装随机数决定钓到什么鱼、鱼有多大流程控制循环决定是否可以继续钓条件判断决定鱼的稀有程度异常处理玩家输入非法内容不能让程序崩溃刚学完Java基础的人这几块内容基本都接触过但又没有完整组合过。这个小项目刚好能让你把零散的知识串成一条线。1.2 方案选型为什么用控制台而不是图形界面有朋友可能会问现在写个游戏不都用Swing或者JavaFX吗控制台黑乎乎的多难看。我当时真试过直接用Swing写结果光是理解事件监听、布局管理器就花了一整天最后连一个按钮都没画明白代码量倒是暴涨了。控制台方案的优势在于它把复杂度集中在“核心玩法逻辑”上而不是被界面代码淹没。玩家输入数字、按回车程序输出对应结果这种交互模式虽然原始但学习和调试的效率极高。我的建议是第一个Java练手项目一定不要碰图形界面哪怕你会Swing也别用。先把控制台版本跑通跑顺后面真有需求和兴致再考虑加界面也不迟。这个选择背后还有一个更现实的原因控制台程序调试方便出问题直接看控制台报错就行不需要去排查什么组件刷新、线程阻塞的问题。对初学者来说能在半小时内跑通获得的成就感远比一个半成品图形界面来得实在。2. 核心细节解析与实操要点2.1 数据建模鱼可不能只存一个名字很多人第一次做这种小项目习惯性地用一个String字符串存放鱼的信息比如鲫鱼、草鱼这样。可是你想想钓鱼游戏里每条鱼除了名字还得有重量、数量、价格甚至稀有度。用字符串拼接这些东西不仅难处理逻辑写起来也是噩梦。我当时的第一版代码就是吃了这个亏用了一个数组分别存名字和重量结果每次判断鱼的价格都要去算下标代码丑得自己都看不下去了。后来果断改成面向对象思路定义了一个Fish类把鱼的属性全部封装在里面public class Fish { private String name; private double weight; private int price; private int rarityLevel; public Fish(String name, double weight, int price, int rarityLevel) { this.name name; this.weight weight; this.price price; this.rarityLevel rarityLevel; } // getter和setter方法略 }这样一来每条鱼都是一个独立对象想访问它的任何属性都很自然。代码的可读性提升了不止一个档次后面扩展功能也方便——比如加一个“鱼的长度”属性只需要在Fish类里加一个字段就行。这就是面向对象思维和面向过程思维最本质的区别。刚开始学的时候体会不到但踩过一次坑就会有感觉。你在设计类的时候不需要一开始就写得特别完美只要抓住了“数据应该和操作封装在一起”这个核心后面的路会顺很多。2.2 随机机制与权重分配怎么让稀有鱼不那么好钓游戏的核心是随机钓鱼。但随机随机也得有讲究。如果每竿都钓到稀有大鱼游戏很快就没意思了。反过来如果全是小鱼玩家也会无聊。这里就需要一个加权随机的设计。我当时的做法是准备好一个鱼池列表里面存着所有鱼的种类。每种鱼设置一个权重值比如“鲫鱼”权重是40“鲤鱼”权重是30“青鱼”权重是20“中华鲟”权重是8“锦鲤”权重是2。然后通过累加权重再生成一个随机数来判断落在哪个区间。private Fish getRandomFish() { ListFish fishPool new ArrayList(); int totalWeight 0; for (Fish fish : fishPool) { totalWeight fish.getRarityLevel(); } int randomValue (int) (Math.random() * totalWeight); int cumulative 0; for (Fish fish : fishPool) { cumulative fish.getRarityLevel(); if (randomValue cumulative) { return fish; } } return fishPool.get(0); }这里有个容易被忽略的细节Math.random()返回的是0.0到1.0之间的double值不包含1.0。所以你乘以totalWeight后强转成int得到的是0到totalWeight - 1的整数这个区间就能很好地映射到累计区间上。如果你不小心用了判断会导致概率分配不均明明低权重的鱼出现的频率会异常偏高。这种细小的边界问题书里一般不写但实际写代码处处都是。2.3 输入交互Scanner的坑你必须提前知道控制台程序里Scanner是和人交互最常见的方式。但Scanner用起来有不少坑。最常见的一个就是少写了nextLine()导致程序不往下走或者输入类型不匹配直接抛异常。比如你想让玩家输入一个数字表示“是否继续钓鱼”写成int choice sc.nextInt()然后玩家输入了一个字母程序会立刻抛出InputMismatchException崩溃掉。这在正式项目里属于低级错误但在学习阶段太常见了。我当时用一个笨办法解决了这个输入异常的问题把所有的输入都统一读成字符串再转换成你想要的类型。这样即使玩家乱输入程序也不会崩顶多提示一句“输入有误请重新输入”public static int readInt(Scanner sc) { while (true) { try { String input sc.nextLine().trim(); return Integer.parseInt(input); } catch (NumberFormatException e) { System.out.print(输入格式不对请重新输入一个数字); } } }这个小函数看着简单却解决了我那个项目里90%的交互异常问题。你后面写任何控制台小工具都可以把它当成一个公共方法复用。学习阶段写代码追求的从来不是代码多花哨而是能处理各种意外情况依然稳稳运行。3. 实操过程与核心环节实现3.1 工程结构规划别一个文件写到底不少初学者习惯把所有代码堆在一个java文件里包括我在内。这种写法不是不行但当你代码超过两三百行找问题就会很痛苦。我当时给自己定了一个规则一个类只干一件事。整个day01项目被我拆成了四个类Fish.java鱼的实体类负责描述一条鱼有什么属性Player.java玩家类负责记录玩家的名字、背包、总重量、金币Game.java游戏逻辑核心负责跑主循环、钓起鱼来、判断鱼去留Main.java程序入口只负责创建Game对象并启动它这样的分层会让代码结构清晰很多。Main永远是最简单的那个类Game承载核心逻辑Fish和Player只管描述数据和状态。这种“数据类”和“逻辑类”分离的思路是很多架构设计的基础。你要知道不是只有大项目才需要拆分规范小项目从第一天就养成好习惯后面才不会走弯路。3.2 核心玩法主循环实现钓鱼游戏的主循环其实就是一个while循环只要玩家还想继续钓就执行“钓鱼→展示结果→询问是否继续”这三个步骤。下面是我day01的初始架构public void start() { Scanner sc new Scanner(System.in); System.out.println(欢迎来到控制台钓鱼场); System.out.print(请输入你的名字); String playerName sc.nextLine().trim(); Player player new Player(playerName); while (true) { System.out.println(输入 1 开始钓鱼输入 2 查看背包输入 3 结束游戏并结算); int choice readInt(sc); if (choice 1) { Fish fish getRandomFish(); System.out.println(你钓到了一条 fish.getName() 重量为 fish.getWeight() 千克); System.out.print(输入 1 留下这条鱼输入 其他 放生); int keep readInt(sc); if (keep 1) { player.addFish(fish); System.out.println(已放入背包。); } else { System.out.println(你放生了这条鱼。); } } else if (choice 2) { player.showBackpack(); } else if (choice 3) { player.settle(); break; } else { System.out.println(无效选项请重新输入。); } } }这个主循环基本就是控制台交互程序的骨架。你需要特别留意的细节是break只会跳出当前的while循环如果外层还有循环或者后面还有代码要执行逻辑就完全不一样了。我见过不少初学者把break当成“结束整个程序”结果该执行的代码在循环结束之后又跑了一遍。在这个项目里由于break后面没有其他代码所以一切正常但你要养成习惯明确知道break和return的区别。3.3 背包管理集合的正确使用姿势钓到的鱼总得有个地方放。我选择了ArrayListFish作为背包的数据结构。为什么不用数组因为数组长度固定你不可能一开始就知道玩家会钓多少条鱼。ArrayList作为动态数组长度可以随时扩展非常适合这种“数量不可控”的场景。public class Player { private String name; private ListFish backpack new ArrayList(); private double totalWeight 0; private int totalPrice 0; public void addFish(Fish fish) { backpack.add(fish); totalWeight fish.getWeight(); totalPrice fish.getPrice(); } public void showBackpack() { if (backpack.isEmpty()) { System.out.println(背包空空如也快去钓鱼吧); return; } System.out.println(当前背包); for (int i 0; i backpack.size(); i) { Fish fish backpack.get(i); System.out.println((i 1) . fish.getName() - fish.getWeight() kg - fish.getPrice() 金币); } System.out.println(总重量 totalWeight kg总价值 totalPrice 金币); } }如果你用的是for-each循环来遍历列表然后直接在循环里做删除操作会触发ConcurrentModificationException。这个异常很隐蔽因为编译不报错运行到一半才会炸。我当年就着了这个道想把“卖掉所有低于1kg的鱼”做成功能结果每次都报错。后来才明白循环中做删除操作应该用迭代器Iterator的remove()方法或者干脆攒一个待删除列表循环结束后统一删。这个经验现在很多工作两三年的程序员还在踩。3.4 结算逻辑与数据展示玩家输入3结束游戏时要展示最终的战果。当时的愿望是把结算页面做得有点成就感于是我用了一个简单方式根据总价格输出不同的等级评价。比如总价在100金币以下是“新手钓手”500金币以上是“钓鱼达人”1000金币以上是“传说中的钓鱼大师”。public void settle() { System.out.println(本次钓鱼结束最终战绩如下); showBackpack(); if (totalPrice 1000) { System.out.println(评价传说中的钓鱼大师); } else if (totalPrice 500) { System.out.println(评价钓鱼达人); } else if (totalPrice 100) { System.out.println(评价新手钓手。); } else { System.out.println(评价还没入门下次再来吧。); } }这种“阈值判断”的逻辑简单直接但有一个小坑如果你把判断顺序写反了先判断totalPrice 100再判断totalPrice 1000那么一个1500金币的玩家永远只会得到“新手钓手”的评价。因为Java的if-else结构是互斥的一旦第一个条件满足后面的就不会再执行。如果你用过SQL的CASE WHEN会发现它们思考方式完全一致——判断顺序决定最终结果。这一点一定要记住调试的时候也优先检查这类逻辑。4. 常见问题与排查技巧实录4.1 输入类型不匹配导致的程序崩溃这是初学阶段遇到最频繁的问题。玩家输入了一个非数字内容nextInt()直接抛InputMismatchException程序闪退。处理思路我前面已经说过了统一用readInt()方法包一层。这一招在控制台程序里几乎是无敌的而且代码量很小。我更推荐你把这个方法理解清楚后亲手敲一遍而不是复制粘贴——它的核心是异常处理的三件套try-catch-finally学会这个才是目的。4.2 概率分布失衡随机鱼总钓到同一种有一次我调完加权随机连续钓了十几次全都是最普通的鲫鱼一度怀疑算法写错了。后来打印了随机数和累计区间做调试才发现问题出在Math.random()返回的最小值是0.0而最大值是0.99999...。如果你用randomValue cumulative来判断而鲫鱼的区间刚好覆盖了从0开始的较大范围那命中率自然异常高。解决方法是把区间左移一点比如用randomValue cumulative或调整权重比例。这里要说明的是随机概率本身是有波动性的连续几次相同结果不代表算法一定错要多次实验看整体分布是否合理。4.3 程序明明没写错但控制台中文乱码Windows系统下跑Java控制台程序窗口编码和源文件编码不一致中文会变成乱码。这个问题一百个初学者有九十个会遇见。最简单的解决办法是在源文件顶部统一使用UTF-8编码保存编译和运行时用-encoding UTF-8参数javac -encoding UTF-8 Main.java java Main如果你用IntelliJ IDEA一般在设置里搜“encoding”把所有编码统一成UTF-8就能解决。这个坑不怪你写得不对而是工具链的编码环境没配对知道怎么修一次以后就不慌了。4.4 程序陷入死循环无法退出主循环如果条件设置不对很容易出现玩家输入3退出后程序还在跑。我当时因为在循环末尾少写了一个break导致每轮询问结束之后又重新从头执行玩家陷入“钓鱼—放生—钓鱼—放生”的无限循环。排查死循环的标准姿势是在循环体的关键位置加打印语句观察每轮的状态变化。打印语句是最笨也最有效的调试方式哪怕你以后学会用IDEA断点调试打印的习惯也别丢它能帮你建立对程序执行流的感觉。下面我把实际运行中遇到的主要问题整理成一张速查表方便你在自己的项目里对照排查现象常见原因解决办法输入字母程序崩溃直接用nextInt读取输入包装成readInt统一处理异常稀有鱼几乎不出来权重区间判断边界出错检查和打印随机数验证分布背包遍历时删除报错循环内直接list.remove改用Iterator或先收集再删中文输出乱码编译与运行编码不一致统一UTF-8加-encoding参数循环退出后程序仍运行break位置不对或没触发在关键路径打印日志确认break归属评价等级永远是最低级if判断顺序把高阈值写后面了高阈值放前面把区间大的判断放后面nextLine被跳过不执行前面调用了nextInt导致换行符残留整段代码统一用nextLine读取再转换类型4.5 代码结构膨胀后难维护的预警虽然day01是个小项目但如果你不控制代码规模等写完几百行就已经可以感到明显的维护压力了。我给自己的经验是每个方法尽量控制在二十行以内如果一个方法里逻辑太多就拆成多个小方法。比如getRandomFish()负责概率计算showBackpack()负责展示settle()负责结算各干各的出了问题只需要盯着某一个方法找不需要翻遍整个文件。这个习惯是你未来写任何规模项目的基础越早养成越好。5. 从day01到day02还能怎么扩展这个项目写完了这个day01并不意味着这个项目就到此为止了。我后来陆陆续续给它加了不少功能每一次加功能都是一次重新学习的机会。这里我分享几个非常适合继续扩展的方向按难度从低到高排列第一加入时间限制或体力系统。给玩家每天设定一定体力值钓一次鱼消耗一点体力体力用完当天就不能钓了。这个功能的难点不在于逻辑本身而在于你需要用到java.time包下的日期处理类比如LocalDate来做“当天”和“跨天重置”的判断。这个练习能帮你把日期时间这块基础打牢。第二增加多种钓鱼地点。比如“河边”“湖泊”“深海”三个地方不同地方能钓到不同鱼种稀有鱼种类和概率也不同。这个功能会让你第一次体会到为什么设计模式里的“工厂模式”和“策略模式”能派上用场——当然如果你还没学到设计模式先用if-else分类也完全没问题关键是把功能跑起来。第三加入存档功能。把玩家的信息保存到文件里下次启动还能读出来。这个功能的实现会用到File类、FileWriter、BufferedReader甚至你想做得规范一点还可以研究Properties或JSON序列化。这是从控制台玩具迈向正经小工具的关键一步强烈推荐你尝试。第四制作一个简易排行榜。把所有玩家的最高战绩存下来排个名次。这虽然不是游戏的核心但能锻炼你操作集合排序、比较器Comparator的能力。而且成就感爆炸——看着自己的名字挂在榜首是真的会上瘾。我在自己的扩展过程中发现每加一个功能都要反复修改原有代码这其实就是重构的雏形。一开始你可能觉得改代码很烦但改多了你会发现清晰的代码结构能让你改起来特别顺手差的代码结构改一行坏三处。这个体感会反过来倒逼你注意命名、注意职责划分、注意封装。学习编程很多时候就是靠这种“自己写→自己踩坑→自己改”的循环推进的。最后说几句实在话回头再看这个day01代码谈不上漂亮逻辑也谈不上复杂但它帮我完成了从“看视频学习”到“独立产出”的关键一跳。很多人学编程卡住不是不够聪明而是太早追求复杂技术反而不愿意从一个完整的小项目开始。你只要确保自己亲手写过这么一个小东西能体会到设计→编码→调试→运行→改进的完整流程那你的Java基础就算是真正站稳了。希望这篇记录能帮到你也欢迎你在这个基础上做出属于自己的day01。
返回列表