笨办法学 Java(四)

2024-01-26 10:41:09 浏览数 (1)

原文:Learn Java The Hard Way 译者:飞龙 协议:CC BY-NC-SA 4.0

练习 55:记录数组

记录很棒,数组更好,但是当你把记录放入数组时,这个生活中几乎没有你不能编码的东西。

代码语言:javascript复制
 1 class Student
 2 {
 3     String name;
 4     int credits;
 5     double gpa;
 6 }
 7 
 8 public class StudentDatabase
 9 {
10     public static void main( String[] args )
11     {
12         Student[] db;
13         db = new Student[3];
14 
15         db[0] = new Student();
16         db[0].name = "Esteban";
17         db[0].credits = 43;
18         db[0].gpa = 2.9;
19 
20         db[1] = new Student();
21         db[1].name = "Dave";
22         db[1].credits = 15;
23         db[1].gpa = 4.0;
24 
25         db[2] = new Student();
26         db[2].name = "Michelle";
27         db[2].credits = 132;
28         db[2].gpa = 3.72;
29 
30         for ( int i=0; i<db.length; i   )
31         {
32             System.out.println("Name: "   db[i].name);
33             System.out.println("tCredit hours: "   db[i].credits);
34             System.out.println("tGPA: "   db[i].gpa   "n");
35         }
36 
37         int max = 0;
38         for ( int i=1; i<db.length; i   )
39             if ( db[i].gpa > db[max].gpa )
40                 max = i;
41 
42         System.out.println(db[max].name   " has the highest GPA.");
43     }
44 }
你应该看到什么
代码语言:javascript复制
Name: Esteban
Credit hours: 43
GPA: 2.9

Name: Dave
Credit hours: 15
GPA: 4.0

Name: Michelle
Credit hours: 132
GPA: 3.72

Dave has the highest GPA.

当你看到变量定义中某物的右侧有方括号时,那就是“某物的数组”。实际上,由于这本书快要结束了,也许我应该解释一下public static void main的业务。至少部分地。

代码语言:javascript复制
public static void main( String[] args )

这一行声明了一个名为 main 的函数。该函数需要一个参数:名为 args 的字符串数组(缩写为“arguments”)。该函数不返回任何值;它是void

无论如何。

第 12 行声明了db作为一个可以容纳“学生数组”的变量。还没有数组,只是一个可能容纳数组的变量。就像我们说…

代码语言:javascript复制
int n;

…还没有整数。变量n可能容纳一个整数,但它里面还没有数字。n被声明但未定义。同样,一旦第 12 行执行完毕,db是一个可能指向学生数组的变量,但仍未定义。

幸运的是,我们不必等太久;第 13 行通过创建一个实际的具有三个槽的学生数组来初始化 db。此时,db 被定义,db.length3,db 有三个合法索引:012

好吧,在这一点上,db是一个学生记录的数组。除了它不是。db是一个学生变量的数组,每个变量都可能容纳一个学生记录,但没有一个变量是这样的。数组中的所有三个槽都未定义。

(从技术上讲,它们包含值null,这是 Java 中引用变量在其中没有对象时具有的特殊值。)

因此,在第 15 行,重要的是创建一个学生对象并将其存储到数组的第一个槽(索引0)中。然后在第 16 行,我们可以将一个值存储到数组 db 中索引0的学生记录的名字字段中。

让我们从外到内追踪它:

表达式

类型

描述

db

students[]

一组学生记录

db[0]

students

一个单独的学生记录(第一个)

db[0].name

String

数组中第一个学生的name字段

db.name

错误

整个数组没有一个名字字段

因此,第 16 行将一个值存储到数组中第一个记录的name字段中。第 17 和 18 行将值存储到该记录中的其余字段中。第 20 到 28 行创建并填充数组中的其他两个记录

尽管在第 30 到 34 行,我们使用循环在屏幕上显示所有的值。

然后,第 37 到 42 行找到了 GPA 最高的学生。这值得更详细解释。在第 37 行,定义了一个名为 max 的int。但 max 不会保存最高 GPA 的值;它只会保存它的索引。

所以当我把0放入 max 时,我的意思是“在代码的这一点上,就我所知,最高分的学生

在槽0中。”这可能不是真的,但由于我们还没有查看数据库中的任何值,这是一个很好的起点。

然后在第 38 行,我们设置循环来查看数组的每个槽。然而,请注意,循环从索引1(第二个槽)开始。为什么?

因为 max 已经是0。所以如果 i 也从0开始,那么if语句将进行以下比较:

代码语言:javascript复制
if ( db[0].gpa > db[0].gpa )

…这是浪费。因此,通过从1开始,第一次循环时,if语句将进行以下比较:

代码语言:javascript复制
if ( db[1].gpa > db[0].gpa )

“如果戴夫的 GPA 大于埃斯特万的 GPA,则将 max 从0更改为 i(1)的当前值。”

因此,当循环结束时,max包含具有最高 GPA 的记录的索引。这正是我们在第 42 行显示的内容。

学习演练
  1. 将数组的容量更改为4而不是 3。不改变任何其他内容,编译并运行程序。你明白为什么程序会崩溃吗?
  2. 现在添加一些代码,将值放入新学生的字段中。给这个新学生一个比“Dave”更高的 GPA,并确认代码正确地将他们标记为具有最高的 GPA。
  3. 更改代码,使其查找具有最少学分的人,而不是具有最高 GPA 的人。

练习 56:从文件中读取记录的数组(温度重访)

这个练习从互联网上的一个文件中填充了一个记录数组。到目前为止,您应该知道您是否需要下载此文件的副本,还是您的计算机可以直接从互联网上打开它。

  • http://learnjavathehardway.org/txt/avg­daily­temps­with­dates­atx.txt

与本书中迄今为止使用的所有其他文件不同,这个数据文件正是我从戴顿大学的平均日温度档案中下载的。这意味着三件事:

  1. 文件的第一行没有数字告诉我们有多少记录。
  2. 除了温度之外,每个记录还包括样本的月份、日期和年份。
  3. 文件中有错误数据。特别是,“当数据不可用时,我们使用‘-99’作为无数据标志。”

因此,有些天的温度是-99。我们将不得不在代码中处理这个问题。

代码语言:javascript复制
 1 import java.util.Scanner;
 2 
 3 class TemperatureSample
 4 {
 5     int month, day, year;
 6     double temperature;
 7 }
 8 
 9 public class TemperaturesByDate
10 {
11     public static void main(String[] args) throws Exception
12     {
13         String url = 
"http://learnjavathehardway.org/txt/avg­daily­temps­with­dates­atx.txt";
14         Scanner inFile = new Scanner((new java.net.URL(url)).openStream());
15 
16         TemperatureSample[] tempDB = new TemperatureSample[10000];
17         int numRecords, i = 0;
18 
19         while ( inFile.hasNextInt() && i < tempDB.length )
20         {
21             TemperatureSample e = new TemperatureSample();
22             e.month = inFile.nextInt();
23             e.day   = inFile.nextInt();
24             e.year  = inFile.nextInt();
25             e.temperature = inFile.nextDouble();
26             if ( e.temperature == ­99 )
27                 continue;
28             tempDB[i] = e;
29             i  ;
30         }
31         inFile.close();
32         numRecords = i;
33 
34         System.out.println(numRecords   " daily temperatures loaded.");
35 
36         double total = 0, avg;
37         int count = 0;
38         for ( i=0; i<numRecords; i   )
39         {
40             if ( tempDB[i].month == 11 )
41             {
42                 total  = tempDB[i].temperature;
43                 count  ;
44             }
45         }
46 
47         avg = total / count;
48         avg = roundToOneDecimal(avg);
49         System.out.println("Average daily temperature over "   count   " days 
in November: "   avg);
50     }
51 
52     public static double roundToOneDecimal( double d )
53     {
54         return Math.round(d*10)/10.0;
55     }
56 }
你应该看到什么
代码语言:javascript复制
6717 daily temperatures loaded.
Average daily temperature over 540 days in November: 59.7

第 3 到 7 行声明了我们的记录,它将存储单个平均日温度值(一个

double),还有月份、日期和年份的字段。

第 16 行定义了一个记录数组。但是我们有一个问题。我们无法在不提供容量的情况下定义数组,而在看到文件中有多少记录之前,我们不知道需要多大的容量。这个问题有三种可能的解决方案:

  1. 不要使用数组。使用其他东西,比如一个可以在添加条目时自动增长的数组。这实际上可能是正确的解决方案,但是“其他东西”超出了本书的范围。
  2. 读取文件两次。首先只计算记录的数量,然后使用完美大小创建数组。然后再次读取文件将所有值读入数组。这样做很慢,但有效。
  3. 不要担心使数组的大小合适。只需使其“足够大”。然后在读取它们时计算实际拥有的记录数量,并在任何循环中使用该计数,而不是数组的容量。这并不完美,但它有效且简单。编写软件有时需要妥协,这就是其中之一。

因此,第 16 行声明了数组并定义为有一万个槽位:“足够大”。

在第 19 行,我们开始一个循环,读取文件中的所有值。我们使用索引变量i来跟踪数组中下一个需要填充的槽位。因此,只要文件中还有更多整数,并且我们的数组容量还没有用完,我们的循环就会继续。

仅仅因为我们通过使数组“足够大”来节省了一些步骤,并不意味着我们会对此感到愚蠢。如果文件最终比我们的数组容量大,我们希望尽早停止读取文件,而不是因为 ArrayIndexOutOfBounds 异常而使程序崩溃。

21 行定义了一个名为e的 TemperatureSample 记录。22 到 25 行将文件中的下几个值加载到该记录的适当字段中。

但是!请记住,我们的文件中有“缺失”的值。有些天的温度读数是

-99,所以我们在第 26 行放置了一个if语句来检测它,然后将它们放入我们的数据库中。

然后在第 27 行有一些新东西:Java 关键字continuecontinue只能在循环体内合法。它的意思是“跳过循环体中剩余的代码行,然后返回顶部进行下一次迭代。”

这实际上丢弃了当前(无效)记录,因为它跳过了第 28 和 29 行,这两行将当前记录存储在数组中的下一个可用槽位中,然后增加索引。

有些人不喜欢使用continue,他们会这样写:

代码语言:javascript复制
if ( e.temperature != ­99 )
{
    tempDB[i] = e; i  ;
}

这也完全没问题。只有当温度不是-99时,才将此条目放入数组中。我更喜欢使用continue,因为这样的代码对我来说更清晰,但是理智的人可能会有不同意见。选择对你来说最有意义的方式。

一旦在第 31 行完成循环,我们确保关闭文件,然后将最终索引存储到 numRecords 中,以便我们可以在任何循环中使用它,而不是tempDB.length。毕竟,我们使数组比我们需要的大,最后的 3283 个槽(在这个例子中)是空的。仅循环到 numRecords 会更有效一些,我们可以通过这种方式避免检查任何无效的记录。

在第 34 行,我们在屏幕上显示记录的数量,这可以帮助您查看是否在读取时出现了任何问题。

第 36 至 45 行循环遍历所有我们的记录。任何月份字段为11(11 月)的记录都会被添加到一个运行总数中,我们也在此过程中计算匹配记录的总数。

然后,当循环结束时,我们可以通过将总和除以计数来获得数据库中所有 11 月份每日温度的平均值。

现在,我的程序的第一个版本的整体平均温度是59.662962962963。这不仅看起来不好,而且不正确:所有输入温度只精确到十分之一度。因此,显示具有十几个有效数字的结果看起来比实际更准确。

因此,在第 52 至 55 行,您将找到一个小小的函数,用于将数字四舍五入到小数点后一位。据我所知,Java 没有内置的此功能,但它确实有一个内置的将数字四舍五入到最接近的整数的函数:Math.round()。所以我将数字乘以十,四舍五入,然后再除以十。也许有更好的方法,但我喜欢这样做。

第 48 行将平均温度作为参数传递给我的函数,然后取舍返回值并将其存储为avg的新值。

学习演练
  1. 访问戴顿大学的温度档案,并下载一个附近城市的温度数据文件!让你的代码从该文件中读取数据。
  2. 更改代码以查找其他内容,比如二月份的最高温度或其他你感兴趣的内容。
  3. 尝试在屏幕上打印整个 TemperatureSample 记录。类似于这样:
代码语言:javascript复制
TemperatureSample ts = tempDB[0]; System.out.println( ts );

请注意,它不会打印像 ts.year 这样的整数或像ts.temperature这样的双精度;它试图在屏幕上显示整个记录。编译并运行文件。屏幕上显示了什么?

尝试更改索引以从数组中提取不同的值,并查看它如何改变打印出来的内容。

练习 57:一副扑克牌

在这本书结束之前,我需要向你展示如何使用记录数组来模拟一副扑克牌。

代码语言:javascript复制
 1 class Card
 2 {
 3     int value;
 4     String suit;
 5     String name;
 6 
 7     public String toString()
 8     {
 9         return name   " of "   suit;
10     }
11 }
12 
13 public class PickACard
14 {
15     public static void main( String[] args )
16     {
17         Card[] deck = buildDeck();
18         // displayDeck(deck);
19 
20         int chosen = (int)(Math.random()*deck.length);
21         Card picked = deck[chosen];
22 
23         System.out.println("You picked a "   picked   " out of the deck.");
24         System.out.println("In Blackjack your card is worth "   picked.value   " 
points.");
25     }
26 
27     public static Card[] buildDeck()
28     {
29         String[] suits = { "clubs", "diamonds", "hearts", "spades" };
30         String[] names = { "ZERO", "ONE", "two", "three", "four", "five", "six",
31             "seven", "eight", "nine", "ten", "Jack", "Queen", "King", "Ace" };
32 
33         int i = 0;
34         Card[] deck = new Card[52];
35 
36         for ( String s: suits )
37         {
38             for ( int v = 2; v <= 14 ; v   )
39             {
40                 Card c = new Card();
41                 c.suit = s;
42                 c.name = names[v];
43                 if ( v == 14 )
44                     c.value = 11;
45                 else if ( v > 10 )
46                     c.value = 10;
47                 else
48                     c.value = v;
49 
50                 deck[i] = c;
51                 i  ;
52             }
53         }
54         return deck;
55     }
56 
57     public static void displayDeck( Card[] deck )
58     {
59         for ( Card c : deck )
60             System.out.println(c.value   "t"   c);
61     }
62 }
你应该看到什么
代码语言:javascript复制
You picked a seven of hearts out of the deck. In Blackjack your card is worth 7 points.

当然,即使这几乎是最后一个练习,我也忍不住加入了一些新东西。你想学点新东西,不是吗?

首先,我在记录中偷偷加了一个函数。(实际上,因为这个函数在一个类中,它不是一个函数,而是一个“方法”。)

这个方法被命名为 toString。它没有参数,并返回一个String。在这个方法的主体中,我们通过连接名称字段、花色字段和单词“of”来创建一个字符串。这个方法不需要任何参数,因为它可以访问记录的字段。(事实上,这就是它成为“方法”而不是“函数”的原因。)

否则,Card记录应该是你期望的:它有卡的值(2-11)、花色名称和卡本身的名称的字段。

在第 17 到 24 行,你可以看到main(),它真的很短。第 17 行声明了一个卡片数组,并使用buildDeck()函数的返回值进行初始化。

第 18 行被注释掉了,但当我最初编写这个程序时,我使用了displayDeck()

确保buildDeck()函数是否正常工作。

第 20 行选择了一个介于0deck.length - 1之间的随机数。你可能会注意到这恰好是数组中合法索引的范围,这不是巧合。

实际上,你也可以说第 20 行选择了数组中的一个随机索引,或者第 20 行随机选择了数组的一个槽位。

然后在第 21 行,我们声明了一个新的 Card 变量picked,并给它一个从数组中随机选择的值。

第 23 行看起来相当无聊,但实际上发生了魔法。picked是什么类型的变量?它是一张卡。通常当你尝试像这样在屏幕上打印整个记录时,Java 不知道你想要打印哪些字段或以什么顺序打印,所以它只是在屏幕上打印垃圾。(你在上一个练习的学习中看到了吧?)

但是,如果你在记录中提供了一个名为toString()的方法,它返回一个String并且没有参数,那么在这种情况下,Java 将在幕后调用该方法。它将获取返回值并打印出来,而不是垃圾。

因此,第 23 行将在屏幕上打印出运行所选卡的toString()方法的结果。相比之下,第 24 行确实很无聊。它打印出所选卡的值字段。

在我们开始buildDeck(),这是这个练习中最复杂的部分之前,让我们跳到displayDeck()函数。displayDeck()期望你传入一个Card数组作为参数。

然后在第 59 行,我们看到了一些我们在前几个练习中没有见过的东西:一个 foreach 循环。这表示“对于牌组中的每张卡……”由于这个for循环的主体中只有一行代码,我省略了花括号。

第 60 行显示了当前卡片的值,一个制表符,然后调用toString()的结果。

代表 Card c的方法。

好吧,让我们来解决这个buildDeck()函数。buildDeck()不需要任何参数,因为它只是从无中创建牌组。不过它确实返回一个值:一组卡片。

在第 29 到 31 行,我们创建了两个字符串数组。第一个(第 29 行)包含了花色的名称。第二个包含了卡片的名称。

你可能会注意到我有一张叫做"ZERO"的卡片,另一张叫做"ONE"的卡片。为什么?这是为了我可以把这个数组当作“查找表”来使用。我将写我的循环,使得我的卡片值从214,我希望单词"two"在这个数组中的索引是2。所以我需要把一些字符串放到槽位01中来占用空间。

最初我只是放了两个空字符串,如下所示:

代码语言:javascript复制
String[] names = { "", "", "two", "three", "four", "five", "six",

…但后来我担心如果我的代码有 bug,那么很难判断是没有打印任何内容还是names[0](或names[1])的值。因此,我为这两个索引放入了单词,但将它们全部大写,这样如果它们被打印出来,我就会注意到。

在第 33 行,我们创建了 i,它将跟踪下一个需要放入卡片的索引。第 34 行定义了我们的 52 张卡片的数组(从 0 到 51 索引)。

第 36 行是另一个 foreach 循环。变量 s 将被设置为"clubs",然后

“方块”,然后“红心”,最后“黑桃”。

第 38 行是另一个for循环,但这个循环是嵌套的。记住这意味着这个循环将进行

在外部循环改变 s 的值之前,v 会从 2 到 14 变化。

第 40 行定义了一个名为 c 的 Card。在第 41 行,我们将这张卡的花色字段设置为当前 s 中的任何值(一开始是"clubs")。

根据循环的次数,v 将是 2 到 14 之间的某个值,所以在第 42 行,我们使用 v 作为 names 数组的索引。也就是说,当 v 是 5 时,我们进入数组的第六个位置,那里会找到字符串"five"。我们将这个值的副本放入当前卡片的名称字段。

第 43 到 48 行将一个从 2 到 11 的整数存储到当前卡片的值字段中。我们需要 v 从 2 到 14 进行查找表,但现在已经完成了,我们需要确保没有卡片的值为 12 到 14。

第 14 张卡是 A,所以我们使用 11 作为卡的值。然后第 11、12 和 13 张卡是花牌,所以它们的卡值都是 10。其他卡的值都可以不变。

最后,我们将这张卡存储到deck的下一个可用槽中(用i索引),并使i增加 1。

当嵌套循环结束时,我们已经成功创建了标准牌组中的所有 52 张卡,并为它们赋予了与二十一点中使用方式相匹配的卡值。如果您想要确保,可以取消注释第 18 行上的displayDeck()调用。

buildDeck()的最后一步是return现在已经填满的 Cards 数组,这样它就可以存储到main()第 17 行的 deck 变量中。

学习演练
  1. 添加一个名为shuffleDeck()的函数。它应该以一组卡片的数组作为参数,并返回一组卡片。一种洗牌的方法是从 0 到 51 选择两个随机数,并“交换”这些槽中的卡片。然后将该代码放入一个重复大约 1000 次的循环中。这有点难以做到正确。

练习 58:最终项目-文本冒险游戏

如果您已经完成了到目前为止的所有练习,那么您应该准备好进行这个最终项目了。它比您之前做过的任何练习都要长,但比最近几个练习并不难。

您的最终练习是基于文本的冒险游戏引擎。通过引擎,我的意思是代码对冒险本身一无所知;游戏的进行完全取决于文件中的内容。更改文件就会改变游戏的进行。

所以首先要下载游戏数据文件的副本,并将其保存到与您要放置代码的相同文件夹中。

  • learnjavathehardway.org/txt/text­adventure­rooms.txt
代码语言:javascript复制
  1 import java.util.Scanner;
  2 
  3 class Room
  4 {
  5     int roomNumber;
  6     String roomName;
  7     String description;
  8     int numExits;
  9     String[] exits = new String[10];
 10     int[] destinations = new int[10];
 11 }
 12 
 13 public class TextAdventureFinal
 14 {
 15     public static void main( String[] args )
 16     {
 17         Scanner keyboard = new Scanner(System.in);
 18 
 19         // initialize rooms from file
 20         Room[] rooms = loadRoomsFromFile("text­adventure­rooms.txt");
 21 
 22         // showAllRooms(rooms); // for debugging
 23 
 24         // Okay, so let's play the game!
 25         int currentRoom = 0;
 26         String ans;
 27         while ( currentRoom >= 0 )
 28         {
 29             Room cur = rooms[currentRoom];
 30             System.out.print( cur.description );
 31             System.out.print("> ");
 32             ans = keyboard.nextLine();
 33 
 34             // See if what they typed matches any of our exit names
 35             boolean found = false;
 36             for ( int i=0; i<cur.numExits; i   )
 37             {
 38                 if ( cur.exits[i].equals(ans) )
 39                 {
 40                     found = true;
 41                     // if so, change our next room to that exit's room number
 42                     currentRoom = cur.destinations[i];
 43                 }
 44             }
 45             if ( ! found )
 46                 System.out.println("Sorry, I don't understand.");
 47         }
 48 
 49     }
 50 
 51     public static Room[] loadRoomsFromFile( String filename )
 52     {
 53         Scanner file = null;
 54         try
 55         {
 56             file = new Scanner(new java.io.File(filename));
 57         }
 58         catch ( java.io.IOException e )
 59         {
 60             System.err.println("Sorry, I can't read from the file '"   
filename   "'.");
 61             System.exit(1);
 62         }
 63 
 64         int numRooms = file.nextInt();
 65         Room[] rooms = new Room[numRooms];
 66 
 67         // initialize rooms from file
 68         int roomNum = 0;
 69         while ( file.hasNext() )
 70         {
 71             Room r = getRoom(file);
 72             if ( r.roomNumber != roomNum )
 73             {
 74                 System.err.println("Reading room # "   r.roomNumber   ", but 
"   roomNum   " was expected.");
 75                 System.exit(2);
 76             }
 77             rooms[roomNum] = r;
 78             roomNum  ;
 79         }
 80         file.close();
 81 
 82         return rooms;
 83     }
 84 
 85     public static void showAllRooms( Room[] rooms )
 86     {
 87         for ( Room r : rooms )
 88         {
 89             String exitString = "";
 90             for ( int i=0; i<r.numExits; i   )
 91                 exitString  = "t"   r.exits[i]   " ("   r.destinations[i]   
")";
 92             System.out.println( r.roomNumber   ") "   r.roomName   "n"   
exitString );
 93         }
 94     }
 95 
 96     public static Room getRoom( Scanner f )
 97     {
 98         // any rooms left in the file?
 99         if ( ! f.hasNextInt() )
100             return null;
101 
102         Room r = new Room();
103         String line;
104 
105         // read in the room # for error­checking later
106         r.roomNumber = f.nextInt();
107         f.nextLine();   // skip "n" after room #
108 
109         r.roomName = f.nextLine();
110 
111         // read in the room's description
112         r.description = "";
113         while ( true )
114         {
115             line = f.nextLine();
116             if ( line.equals("%%") )
117                 break;
118             r.description  = line   "n";
119         }
120 
121         // finally, we'll read in the exits
122         int i = 0;
123         while ( true )
124         {
125             line = f.nextLine();
126             if ( line.equals("%%") )
127                 break;
128             String[] parts = line.split(":");
129             r.exits[i] = parts[0];
130             r.destinations[i] = Integer.parseInt(parts[1]);
131             i  ;
132         }
133         r.numExits = i;
134 
135         // should be done; return the Room
136         return r;
137     }
138 
139 }
你应该看到什么
代码语言:javascript复制
This is the parlor.
It's a beautiful room.
There looks to be a kitchen to the "north".
And there's a shadowy corridor to the "east".
> north
There is a long countertop with dirty dishes everywhere. Off to one side
there is, as you'd expect, a refrigerator. You may open the "refrigerator"
or "go back".
> go back
This is the parlor.
It's a beautiful room.
There looks to be a kitchen to the "north".
And there's a shadowy corridor to the "east".
> east
The corridor has led to a dark room. The moment you step inside, the door
slams shut behind you. There is no handle on the interior of the door.
There is no escaping. Type "quit" to die.
> quit

在我开始讨论代码之前,让我花点时间谈谈冒险游戏的“文件格式”。

游戏由几个“房间”组成。每个房间都有一个房间号和一个房间名称;这些只用于游戏引擎,玩家看不到。

每个房间还有一个描述和一个或多个“出口”,这是通往另一个房间的路径。

冒险游戏文件以一个数字开头:游戏中的位置(房间)的总数。之后是每个房间的记录。这是一个例子:

代码语言:javascript复制
1
KITCHEN
There is a long countertop with dirty dishes everywhere. Off to one side there is, as you'd expect, a refrigerator. You may open the "refrigerator" or "go back".
%%
fridge:3 refrigerator:3 go back:0 back:0
%%

这个记录的第一行是房间号,所以这是房间号 1。记录的第二行是房间名称,我们只用于调试。

从记录的第三行开始是房间的描述,一直到有一行只有%%的行为止。描述中允许有空行。

在第一个双百分号之后是一个出口列表。每一行都有出口的名称(玩家输入的内容)后跟一个冒号,再跟着出口通往的房间号。

例如,在这个房间,如果玩家输入"fridge",游戏引擎将把他们从这个房间(房间#1)移动到房间#3。如果他们输入"go back",他们将“旅行”到房间#0。您可能会注意到,为了让玩家更容易决定输入什么,我在列表中有重复的出口。无论是"fridge"还是"refrigerator"都会把他们带到房间#3。

出口列表以另一行只包含%%的行结束。这就是记录的结尾。

好的,现在让我们转向代码。第 3 到 11 行声明了一个房间的记录。您可以看到我们为冒险游戏文件中的每个字段都有字段。您可能没有猜到的唯一一件事是,出口字符串数组(出口)和目的地房间号数组(目的地)的任意容量为10,然后有一个 numExits 字段来跟踪这个房间实际上有多少出口。如果您认为一个房间需要超过 10 个出口,请随时将此容量增加。

进入main(),第 20 行声明了房间数组并从中初始化。

loadRoomsFromFile()函数,稍后我会解释。

第 22 行有一个注释掉的showAllRooms()函数调用,我用于调试。

在第 25 行,您将看到我们当前房间变量的定义,它保存了玩家所在房间的房间号。他们从房间0开始,这是文件中的第一个房间。在第 26 行是String ans 的声明,它将保存玩家输入的内容。

第 27 行是主游戏循环的开始。只要 currentRoom 变量为0或更多,它就会重复。因此,我们将使用它来停止游戏:当玩家死亡(或获胜)时,我们将 currentRoom 设置为-1

数组 rooms 包含游戏中所有位置的列表。包含玩家的房间号的房间的变量 currentRoom 存储在变量中。因此,rooms[currentRoom]是整个房间的记录…嗯,当前房间。在第 29 行,我们将这个房间的副本存储到Room变量 cur 中。(我这样做只是因为我懒,想要输入像cur.description而不是rooms[currentRoom].description这样的东西。)

说到这一点,第 30 行打印出当前房间的描述,它存储在

描述字段。

在第 31 和 32 行,我们打印出一个小提示,并让玩家输入他们想去的地方的字符串。

第 36 到 44 行搜索这个房间的出口数组,看看它们是否与玩家输入的内容匹配。请记住,出口数组的容量为10,但实际上这个房间可能并没有那么多出口。因此,在for循环中,我们计数到 numExits 字段的值,而不是10

如果我们找到与玩家命令匹配的出口,我们将标志设置为true(这样我们就知道如果他们最终输入了我们列表中没有的内容,我们应该抱怨)。然后,由于出口数组中的单词与目的地数组中的房间号相对应,我们从目的地数组的相应槽中取出房间号,并将其作为我们的新房间号。这样,当主游戏循环再次重复时,我们将自动前往新的房间。

在第 45 行,我们检查我们的标志。如果它仍然是false,这意味着用户输入了我们在出口列表中从未找到的东西。我们可以礼貌地抱怨。因为当前房间没有改变,所以在主游戏循环中再次循环将只是再次打印出他们已经在的房间的描述。

这就是主游戏循环的结束,也是main()的结束。剩下的就是从冒险游戏文件中实际填充房间数组。

第 51 行是loadRoomsFromFile()函数的开始,它以要打开的文件名作为参数,并返回一个Room数组。

(我决定在这个文件中不想有throws Exception,所以这里有一个 try-catch 块。它打开文件。

如果我们成功到达第 64 行,这意味着文件已成功打开。我们读取文件的第一行,告诉我们有多少个房间。然后第 65 行定义了一个具有适当容量的 Room 记录数组。

在第 68 行,我创建了一个名为 roomNum 的变量,它有双重作用。首先:它是房间数组中下一个可用槽的索引。但其次,它用于双重检查文件中的房间号和房间的槽号是否相同。如果不是,游戏数据文件中可能存在某种错误。如果我们在这里检测到这样的错误(在第 72 行),我们会抱怨并结束程序。(System.exit()结束程序,即使是在函数调用内部。)

第 69 行是“读取所有房间”的循环的开始。只要文件中还有未见过的内容,它就会继续进行。这里存在潜在的错误:如果数据文件顶部的房间数量是错误的,那么这个循环可能会在数组中走得太远并导致错误。(例如,如果文件的第一行说你只有 7 个房间,但实际上有 8 个房间记录,那么这个循环将重复太多次。)

在第 71 行,我们使用getRoom()函数读取单个房间记录,我稍后会解释。

第 72 到 76 行是我已经提到的房间号健全性检查,然后第 77 行只是将这个新房间存储到房间数组的下一个可用槽中。第 78 行增加了房间索引。

循环结束后,所有房间都已从文件中读取并存储在数组的各自位置。因此,在第 82 行,我们可以将房间数组返回到main()的第 20 行。

第 85 到 94 行是我用于调试的showAllRooms()函数。它只是在屏幕上显示数组中的所有房间,并且对于每个房间,它还显示所有的出口以及它们的目的地。

我们的最后一个函数是getRoom(),它期望传入一个 Scanner 对象作为参数,并返回一个单独的 Room 对象。

在第 99 和 100 行,如果数据文件格式不正确,会进行简单的健全性检查。如果下一个

如果文件中的东西不是整数,那么只需返回null(未初始化对象的值)。在这里放置一个return将立即从函数中返回,而不必运行剩下的代码。

在第 102 行定义了空房间对象。第 103 行创建了一个名为line的字符串,我用它来做一些不同的事情。

第 106 行从文件中读取房间号。房间号是房间记录的第一部分。这个函数的其余部分将只使用 Scanner 对象的nextLine()方法,而在nextInt()之后的nextLine()通常不起作用,因为它只读取刚刚读取的整数后面的行尾。

因此,第 107 行调用nextLine()方法,但不必在任何地方存储它的返回值,因为它不会读取任何值值得保存。

第 109 行从文件中读取房间名称。我们只在调试时使用这个。

在第 112 行,我们首先将这个房间的描述字段设置为空字符串。这样我们就可以在不出错的情况下添加内容。(就像我们在循环中将“总数”变量设置为0一样,然后再进行累加。)

好吧。我喜欢写无限循环。告我吧。第 113 行是一个无限循环的开始。这是因为我们不知道房间描述中会有多少行;它会一直持续,直到我们看到一行什么都没有的%%。还有其他方法可以做到这一点,但我喜欢“写一个无限循环,然后在找到你要找的东西时跳出它”的方法。就像我以前说过的,理智的人意见不一。

一旦我们进入“无限”循环,我们就会将描述的一行读入 line 变量中。然后,在第 116 行,我们检查刚刚读取的内容是否为%%。如果是的话,我们就不想将其添加到描述中,所以我们跳出循环。break 有点像 continue 的相反;continue 跳回到循环的条件,而 break 直接跳到末尾并停止循环。

如果我们仍然在第 118 行附近,这意味着我们读入了一行描述,而且它不是%%。所以我们使用 =将该行(和一个n)添加到描述字段的末尾。然后循环重复。(无论如何。)

最终,我们希望碰到%%,循环就会停止。

第 122 行定义了 i,我用它来表示 exits 和 destinations 数组中我们要放入下一个值的槽的索引。然后从第 123 行开始又是一个无限循环。我使用了一个非常类似的方法来读取所有的出口。

第 125 行读取整行,这意味着该行包含类似于“‘refrigerator:3’”的内容。(如果不是这样,而实际上是%%,则第 126 行和第 127 行停止循环。)

所以现在我们需要将这行分成两部分。幸运的是,String 类有一个名为 split()的内置方法。

line.split(“:”)在字符串 line 中搜索并在每次看到:(冒号)时将其分割开。然后它返回一个字符串数组。例如,如果 line 包含 thisXisXaXtest,那么 line.split(“X”)将返回一个包含{“this”,“is”,“a”,“test”}的数组。在我们的情况下,line 中只有一个冒号,所以它返回类似于{“refrigerator”,“3”}的内容。

因此,在第 128 行之后,parts[0]包含出口词(如“refrigerator”),parts[1]包含目的地房间号的字符串(如"3")。这对我们来说不太适用,因为我们需要房间号是整数,而不是字符串。

对我们来说(再次),Java 的标准库来拯救我们。有一个内置函数可以将字符串转换为整数:Integer.parseInt()。我们在第 130 行使用了这个函数。

回想一下,i 是我们需要存储下一个值的出口数组中的槽的索引。因此,第 129 行将 parts[0](出口的名称)存储到出口数组的适当槽中。第 130 行将 parts[1](要移动到的房间号)从字符串转换为 int,并将其存储在目的地数组的相同槽中。然后第 131 行增加下一轮的出口索引。

最终我们会碰到%%,这个循环也会停止循环。然而,这里存在一个潜在的错误。出口数组只有十个槽。如果数据文件中有一个房间有超过十个出口,这个循环将继续超出数组的末端,并导致程序崩溃。所以不要这样做。

循环结束后,我们的索引 i 将包含我们读入的房间的真实数量。所以我们将其存储到第 133 行当前房间的 numExits 字段中。

然后就是这样了。房间中的所有字段都已经被赋值,我们返回这个 Room。

对象到loadRoomsFromFile()函数的第 71 行。

学习演练
  1. 写你自己的文字冒险。如果你觉得它变得相当不错,就把它发给我!
  2. 添加一个保存游戏的功能,这样玩家可以输入一些内容来停止游戏,游戏将把他们当前的房间号存储到一个文本文件中,然后在游戏重新开始时加载它。

0 人点赞