diff --git a/.classpath b/.classpath new file mode 100644 index 0000000000000000000000000000000000000000..fb5011632c0ab8d6649a148c6fb5845a1b34c747 --- /dev/null +++ b/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..21a3013037921d96997bce6ed6f590be35cf4cbb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin +*.class \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000000000000000000000000000000000000..9772bd2069c0a5d67244ec1877b0098bb423bb72 --- /dev/null +++ b/.project @@ -0,0 +1,28 @@ + + + functional zork + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + + + 1667407459743 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/README.md b/README.md index 7a2575efcbca3d94bc617821a28f1298694c8048..62d4de2e4b3194c8b0a6b9a091cd1dc421832f62 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,35 @@ -# fpzork +# 1024 程序员开源挑战赛项目 —— fpzork -用于练习 Java 函数式编程的一个命令行小游戏 \ No newline at end of file +--- + + + +## 1. 项目概述 + +`fpzork` 是一个用于练习 `Java` 函数式编程的一个命令行小游戏,是 `functional programming: zork` 的简称。核心代码改编自 2015 年 `Packt` 出版设出版的《Learning Java Functional Programming》开源示例项目。 + + + +## 2. 运行方式 + +按普通 `Java` 项目导入您熟悉的 `IDE` 后(如 `Eclipse`),无需下载其它第三方依赖,即可直接运行项目。运行方式至少有以下三种方式:(以 `Eclipse` 为例) + +1. 选中项目后,右键菜单 Run As :arrow_right: Java Application   Alt+Shift+X,J; +2. 打开 `FunctionalZork.java`,运行 `main` 方法; +3. 导出项目为可执行 `jar` 文件(如 `zork.jar`)后,连同 `data.txt` 文件一同复制到某文件夹下(如桌面 `demo` 文件夹);通过命令行进入 `demo`,运行命令 `java -jar zork.jar`。 + +无论以哪种方式运行成功后,都会看到像如下所示的命令行界面:(这里以 `Windows Terminal` 为例) + +![Running Zork Game](img/running-game.png) + + + +## 3 游戏操作命令 + +- 捡起物品:`pickup [ [ ]]`:拾起 `Item1`、`Item2` 和 `Item3`。`pickup` 也可写为 `Pickup`; +- 放下物品:`drop [ [ ]]`:放下 `Item1`、`Item2` 和 `Item3`。`drop` 也可写为 `Drop`; +- 前往某个方向:`go `:前往 `Direction1`。`go` 也可换作 `walk` 或 `Walk`; +- 查看当前位置:`look`; +- 查看可以前往的方向:`dir | direction`; +- 查看拥有的物品:`inv | inventory`; +- 结束游戏:`quit`。 \ No newline at end of file diff --git a/data.txt b/data.txt new file mode 100644 index 0000000000000000000000000000000000000000..99ef962c184e11f2c127fb3a2540af3dcd7b4329 --- /dev/null +++ b/data.txt @@ -0,0 +1,50 @@ +Location +House +You are standing outside a house sitting in the middle of a meadow. There is a path leading north to the woods and a path leading east to a lake. +Direction +North +Woods +Direction +East +Emerald Blue Lake +Item +Axe +A shape wooden handle axe +Item +Stone +A small round rock +Location +Woods +You are standing in the middle of a dense woods. There is a path leading south to a meadow and a path leading east to some small hills. +Direction +South +House +Direction +East +Small Hills +Location +Small Hills +You are standing on the top of a small hill. To the west you see a path leading to a vast forest. To the south is a path leading to an emerald blue lake. +Direction +West +Woods +Direction +South +Emerald Blue Lake +Item +Key +A small bronze key +Location +Emerald Blue Lake +You are standing on the shores of the lake you are impressed by it deep emerald blue color. There is a path leading north to some small hills and a path leading west to a meadow. +Direction +West +House +Direction +North +Small Hills +NPC +Orge +A fierce ugly orge lurks a few meters away. He looks like he is about to attack. +StartingLocation +House \ No newline at end of file diff --git a/img/running-game.png b/img/running-game.png new file mode 100644 index 0000000000000000000000000000000000000000..1ca6e5944259bf6bb3f53f04cf56614faadce935 Binary files /dev/null and b/img/running-game.png differ diff --git a/src/fpzork/Character.java b/src/fpzork/Character.java new file mode 100644 index 0000000000000000000000000000000000000000..2d3c82c03e955e4118d0473156b79f5bbfe68715 --- /dev/null +++ b/src/fpzork/Character.java @@ -0,0 +1,105 @@ +package fpzork; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class Character { + + private final List items = new ArrayList<>(); + private Location location; + + public Character(Location currentLocation) { + this.location = currentLocation; + } + + public boolean pickup(Command command) { + List arguments = command.getArguments(); + arguments.stream() + .filter(itemName -> { + if (this.location.getItems().contains(itemName)) { + return true; + } else { + System.out.println("Cannot pickup " + itemName); + return false; + } + }) + .forEach(itemName -> { + items.add(itemName); + this.location.getItems().remove(itemName); + System.out.println("Picking up " + itemName); + }); + return true; + } + + public boolean drop(Command command) { + List arguments = command.getArguments(); + if (arguments.isEmpty()) { + System.out.println("Drop what?"); + return false; + } else { + boolean droppedItem = false; + + // Functional solution - does not modify droppedItem +// arguments.stream() +// .map(itemName -> { +// if (items.remove(itemName)) { +// this.location.addItem(itemName); +// return "Dropping " + itemName; +// } else { +// return "Cannot drop " + itemName; +// } +// }) +// .forEach(System.out::println); + + for (String itemName : arguments) { + droppedItem = items.remove(itemName); + if (droppedItem) { + this.location.addItem(itemName); + System.out.println("Dropping " + itemName); + droppedItem = true; + } else { + System.out.println("Cannot drop " + itemName); + } + } + return droppedItem; + } + } + + public boolean walk(Command command) { + List directions = command.getArguments(); + if (directions.isEmpty()) { + System.out.println("Go where?"); + return false; + } else { + directions.forEach((direction) -> { + Optional locationName = GameElements.currentLocation.getLocation(direction); + System.out.print(locationName + .map(name -> { + Location newLocation = GameElements.locations.get(name); + this.location = newLocation; + GameElements.currentLocation = newLocation; + GameElements.displayView(GameElements.currentLocation); + return ""; + }) + .orElse("However, you can't go " + direction + "\n")); + }); + return true; + } + } + + public boolean inventory(Command command) { + //List arguments = command.getArguments(); + if (items.isEmpty()) { + System.out.println("You are holding nothing"); + } else { + System.out.print("You are holding:"); + this.items.forEach((item) -> { + System.out.print(" " + item); + }); + System.out.println(); + } + return true; + } +} diff --git a/src/fpzork/Command.java b/src/fpzork/Command.java new file mode 100644 index 0000000000000000000000000000000000000000..e59e6b7d04d595e772677ddd4bb91ce55fff4bd7 --- /dev/null +++ b/src/fpzork/Command.java @@ -0,0 +1,31 @@ +package fpzork; + + +import java.util.ArrayList; +import java.util.List; + +public class Command { + private String command; + private final List arguments = new ArrayList<>(); + + public void setCommand(String command) { + this.command = command; + } + + public String getCommand() { + return command; + } + + public void addArgument(String argument) { + this.arguments.add(argument); + } + + public List getArguments() { + return arguments; + } + + public void clear() { + this.command = ""; + this.arguments.clear(); + } +} diff --git a/src/fpzork/Direction.java b/src/fpzork/Direction.java new file mode 100644 index 0000000000000000000000000000000000000000..fdcbd2eeb0f58bb441ac0b304aad2b0e115728de --- /dev/null +++ b/src/fpzork/Direction.java @@ -0,0 +1,34 @@ +package fpzork; + + +public class Direction { + private String direction; + private String location; + + public Direction() { + this.direction = ""; + this.location = ""; + } + + public Direction(String direction, String location) { + this.direction = direction; + this.location = location; + } + public String getDirection() { + return direction; + } + + public Direction direction(String direction) { + this.direction = direction; + return this; + } + + public String getLocation() { + return location; + } + + public Direction location(String location) { + this.location = location; + return this; + } +} diff --git a/src/fpzork/FunctionalCommands.java b/src/fpzork/FunctionalCommands.java new file mode 100644 index 0000000000000000000000000000000000000000..22a0e55696176a07ce06cae308f6cfa4fb386fb3 --- /dev/null +++ b/src/fpzork/FunctionalCommands.java @@ -0,0 +1,19 @@ +package fpzork; + + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public class FunctionalCommands { + private final List> commands = new ArrayList<>(); + + public void addCommand(Supplier command) { + commands.add(command); + } + + public void executeCommand() { + commands.forEach(Supplier::get); + commands.clear(); + } +} diff --git a/src/fpzork/FunctionalZork.java b/src/fpzork/FunctionalZork.java new file mode 100644 index 0000000000000000000000000000000000000000..05b1b6c88732ce5a7f7873498cecc900b1a08628 --- /dev/null +++ b/src/fpzork/FunctionalZork.java @@ -0,0 +1,241 @@ +package fpzork; + + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static fpzork.GameElements.commands; + +public class FunctionalZork { + + private final Scanner scanner; + private Character character = null; + private final FunctionalCommands fc; + private final Command command = new Command(); + + private Supplier dropCommand = () -> character.drop(command); + private Supplier pickupCommand = () -> character.pickup(command); + private Supplier walkCommand = () -> character.walk(command); + private Supplier inventoryCommand = () -> character.inventory(command); + private Supplier lookCommand = () -> { + GameElements.displayView(GameElements.currentLocation); + return true; + }; + + private Supplier directionsCommand = () -> { + GameElements.currentLocation.displayPaths(); + return true; + }; + private final Supplier quitCommand = () -> { + System.out.println("Thank you for playing!"); + return true; + }; + + public void initializeCommands() { + commands.put("drop", dropCommand); + commands.put("Drop", dropCommand); + commands.put("pickup", pickupCommand); + commands.put("Pickup", pickupCommand); + commands.put("walk", walkCommand); + commands.put("go", walkCommand); + commands.put("inventory", inventoryCommand); + commands.put("inv", inventoryCommand); + commands.put("look", lookCommand); + commands.put("directions", directionsCommand); + commands.put("dir", directionsCommand); + commands.put("quit", quitCommand); + } + + public static void main(String[] args) { + String command = ""; + Stream commandStream; + FunctionalZork game = new FunctionalZork(); + while (!"Quit".equalsIgnoreCase(command)) { + System.out.print(">> "); + commandStream = game.getCommandStream(); + game.parseCommandStream(commandStream); + command = game.executeCommand(); + } + } + + public FunctionalZork() { + scanner = new Scanner(System.in); + fc = new FunctionalCommands(); + initializeGame(); + character = new Character(GameElements.currentLocation); + } + + public void initializeGame() { + System.out.println("Welcome to Functional Zork!\n"); + File file = new File("data.txt"); + try (FileInputStream fis = new FileInputStream(file); + BufferedReader br = new BufferedReader(new InputStreamReader(fis))) { + String line = br.readLine(); + while ("Location".equalsIgnoreCase(line)) { + Location location = new Location() + .name(br.readLine()) + .description(br.readLine()); + line = br.readLine(); + while ("Direction".equalsIgnoreCase(line)) { + // Add direction + location.addDirection(new Direction() + .direction(br.readLine()) + .location(br.readLine()) + ); + line = br.readLine(); + } + while ("Item".equalsIgnoreCase(line)) { + // Add items + Item item = new Item() + .name(br.readLine()) + .description(br.readLine()); + location.addItem(item.getName()); + GameElements.items.put(item.getName(), item); + line = br.readLine(); + } + while ("NPC".equalsIgnoreCase(line)) { + // Add NPC + // Imperative solution + NPC npc; +// npc = new NPC(); +// npc.setName(br.readLine()); +// npc.setDescription(br.readLine()); + // Functional solution + npc = new NPC() + .name(br.readLine()) + .description(br.readLine()); + location.addNPC(npc.getName()); + GameElements.npcs.put(npc.getName(), npc); + line = br.readLine(); + } + GameElements.locations.put(location.getName(), location); + } + if ("StartingLocation".equalsIgnoreCase(line)) { + GameElements.currentLocation = GameElements.locations.get(br.readLine()); + GameElements.displayView(GameElements.currentLocation); + } else { + System.out.println("Missing Starting Location"); + } + initializeCommands(); + // Test imperative partial solution +// executeCommandImperative("drop axe and rope"); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + public String additionalProcessing(String token) { + if (token.equalsIgnoreCase("drop")) { + // Additional processing + } + return token; + } + + public List processCommand(String commandLine) { + // Imperative solution + // Stop words + List toRemove = Arrays.asList("a", "an", "the", "and"); + + List tokens = new ArrayList<>(); + for (String token : commandLine.split("\\s+")) { + if (!toRemove.contains(token)) { +// tokens.add(token); + tokens.add(token.toLowerCase()); + + // First approach + String tmp = token.toLowerCase(); + tmp = additionalProcessing(tmp); + tokens.add(tmp); + + // Second approach + tokens.add(additionalProcessing(token.toLowerCase())); + } + } + return tokens; + } + + public Stream getCommandStream() { + String commandLine = scanner.nextLine(); +// processCommand(commandLine); + + // Stop words + List toRemove = Arrays.asList("a", "an", "the", "and"); + Stream commandStream = Pattern + .compile("\\s+") + .splitAsStream(commandLine) + .map(s -> additionalProcessing(s)) + //.map(s -> s.toLowerCase()) + .filter(s -> !toRemove.contains(s)); + + return commandStream; + } + + public void parseCommandStream(Stream tokens) { + command.clear(); + tokens + .map(token -> { + if (commands.containsKey(token)) { + // Make sure this is done only once + command.setCommand(token); + } else { + command.addArgument(token); + } + return token; + }) + .allMatch(token -> !"quit".equalsIgnoreCase(token)); + } + + public void executeDropCommand(String command, String arguments[]) { + // Execute command + System.out.print("Command: " + command); + for (String arg : arguments) { + System.out.print(" " + arg); + } + System.out.println(); + } + + public void executeCommandImperative(String commandLine) { + // Imperative solution variation + String tokens[] = commandLine.split("\\s+"); + String arguments[] = new String[tokens.length-1]; + int index = 0; + String cmd = tokens[0]; + if (cmd.equalsIgnoreCase("drop")) { + // Setup drop command + while (index+1 < tokens.length) { + arguments[index] = tokens[index+1]; + index++; + } + executeDropCommand(cmd, arguments); + } else if (cmd.equalsIgnoreCase("pickup")) { + // Setup drop command + } else if (cmd.equalsIgnoreCase("go")) { + // Setup drop command + } else { + // Bad command + } + } + + public String executeCommand() { + Supplier nextCommand = commands.get(command.getCommand()); + if (nextCommand != null) { + fc.addCommand(nextCommand); + fc.executeCommand(); + return command.getCommand(); + } else { + System.out.println("Bad commmand"); + return ""; + } + + } +} diff --git a/src/fpzork/GameElements.java b/src/fpzork/GameElements.java new file mode 100644 index 0000000000000000000000000000000000000000..4b6d73e920c453f768aa3ff76764a541fedbc616 --- /dev/null +++ b/src/fpzork/GameElements.java @@ -0,0 +1,38 @@ +package fpzork; + + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class GameElements { + + public static Map locations = new HashMap<>(); + public static Map items = new HashMap<>(); + public static Map npcs = new HashMap<>(); + public static final Map> commands = new HashMap<>(); + public static Location currentLocation; + + public static void displayLocation(Location location) { + System.out.println("-------------------"); + System.out.println("Name: " + location.getName()); + System.out.println("Description: " + location.getDescription()); + System.out.println("Items -------- "); + + location.getItems().forEach(name -> System.out.println(GameElements.items.get(name))); + + location.getNPCs().forEach(name -> System.out.println(GameElements.npcs.get(name))); + System.out.println(); + } + + public static void displayView(Location location) { + System.out.println(location.getDescription()); + GameElements.currentLocation.displayItems(); + GameElements.currentLocation.displayNPCs(); + } + + public static void displayDirections(Location location) { + GameElements.currentLocation.displayPaths(); + } + +} diff --git a/src/fpzork/Item.java b/src/fpzork/Item.java new file mode 100644 index 0000000000000000000000000000000000000000..9b30c47a32b66960311e1220a5fd3ba6e872955f --- /dev/null +++ b/src/fpzork/Item.java @@ -0,0 +1,30 @@ +package fpzork; + + +public class Item { + private String name; + private String description; + + public String getName() { + return name; + } + + public Item name(String name) { + this.name = name; + return this; + } + + public String getDescription() { + return description; + } + + public Item description(String description) { + this.description = description; + return this; + } + + @Override + public String toString() { + return "Name: " + this.name + " Description: " + this.description; + } +} diff --git a/src/fpzork/Location.java b/src/fpzork/Location.java new file mode 100644 index 0000000000000000000000000000000000000000..34d2ba2ef7d52e4993e0c2e6fd3f93f25e791299 --- /dev/null +++ b/src/fpzork/Location.java @@ -0,0 +1,92 @@ +package fpzork; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class Location { + + private String name; + private String description; + private final List items = new ArrayList<>(); + private final List npcs = new ArrayList<>(); + private final Map directions = new HashMap<>(); + + public String getName() { + return name; + } + + public Location name(String name) { + this.name = name; + return this; + } + + public String getDescription() { + return description; + } + + public Location description(String description) { + this.description = description; + return this; + } + + public List getItems() { + return this.items; + } + + public void addItem(String item) { + this.items.add(item); + } + + public List getNPCs() { + return npcs; + } + + public void addNPC(String npc) { + this.npcs.add(npc); + } + + public Optional getLocation(String direction) { + if (this.directions.containsKey(direction)) { + return Optional.of(this.directions.get(direction).getLocation()); + } else { + return Optional.empty(); + } + } + + public void addDirection(Direction direction) { + directions.put(direction.getDirection(), direction); + } + + public void displayItems() { + if (items.isEmpty()) { + // Do nothing + } else { + System.out.print("On the ground you see:"); + items.stream() + .forEach(item -> { + System.out.print(" " + item); + }); + System.out.println(); + } + } + + public void displayNPCs() { + if (npcs.isEmpty()) { + // Do nothing + } else { + npcs.forEach(System.out::println); + } + } + + public void displayPaths() { + directions.forEach((way, direction) -> { + System.out.print("If you go " + way); + System.out.println(" you can get to " + direction.getLocation()); + }); + } + +} diff --git a/src/fpzork/NPC.java b/src/fpzork/NPC.java new file mode 100644 index 0000000000000000000000000000000000000000..9619ac7ce9dd100754ad9b38ca068769c89fd211 --- /dev/null +++ b/src/fpzork/NPC.java @@ -0,0 +1,38 @@ +package fpzork; + + +public class NPC { + private String name; + private String description; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public NPC name(String name) { + this.name = name; + return this; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public NPC description(String description) { + this.description = description; + return this; + } + + @Override + public String toString() { + return "Name: " + name + " Description: " + description; + } +}