Skip to content

Commit

Permalink
Create MVP with CLI game loop
Browse files Browse the repository at this point in the history
  • Loading branch information
kimgoetzke committed Oct 29, 2023
1 parent c891996 commit 3bb22e1
Show file tree
Hide file tree
Showing 146 changed files with 8,028 additions and 511 deletions.
110 changes: 110 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Procedural Generation Project 1

This project was my first attempt to procedurally generate, well, anything really. I didn't know anything about this
topic but ended up with an old-school text-based adventure game world where the player can travel between locations,
interact with non-player characters, and engage in combat.

### Features

#### Procedural generation

- All objects below are generated procedurally, using a seeded `Random` object and handled by a `WorldHandler` instance
- The core object is `World` which holds `Chunk[][]`, with the player starting in the centre chunk
- Each `Chunk` holds an `int[][]`:
- Based on `int density`, a number of `Location`s are placed in the `Chunk`
- `Location`s are connected using various, configurable strategies which result in the `WorldHandler`
s `Graph<Location>`
- A `Location` (interface) contains reference to neighbouring locations, points of interest inside it, and its location
within the chunk and world
- The only `Location` available at this stage is a `Settlement`
- For generation, the most important feature of a `Location` is its `Size` which determines the `PointOfInterest`
count and, for a `Settlement`, also the `Inhabitant` count
- Each `Location` holds a `List<PointOfInterest>` which the player can visit
- The key object a player interacts with/within is a `PointOfInterest` (**POI**):
- A POIs features are determined by its `Type`
- Currently implemented are the following `Type`s: `ENTRANCE`, `MAIN_SQUARE`, `SHOP`, `QUEST_LOCATION` and `DUNGEON`
- At a POI, the player can engage in dialogues with non-player characters (**NPC**) other events (e.g. "delivery"
or "kill" quests), engage in combat, or take other actions
- Each object of each layer (i.e. `World`, `Chunk`, `Location` and `PointOfInterest`) can be located using `Coordinate`
- The web of connections and the distance between each (both of which stored in the `WorldHandler`s `Graph<Location>`)
play an important role e.g. where a player can travel to and how long it takes

#### Player loop

- When playing the game via a CLI, the `Player`'s `State` determines the player loop that is being executed
- Each state (e.g. `PoiLoop` or `CombatLoop`) inherits from `AbstractLoop`
- A loop follows a simple sequence such as this example from the `DialogueLoop`:

```java
public class DialogueLoop extends AbstractLoop {

@Override
public void execute(List<Action> actions) {
printInteraction(); // Shows the current `Interaction` from the NPCs `Dialogue`
prepareActions(actions); // Reads the available `Action`s for current `Dialogue` from `Event`
promptPlayer(actions); // Shows the above as a `List<Action>` & executes the selection `Action`
postProcess(); // Handles side effects of the outcome e.g. updating the `Event`
}
}

```

- Almost every loop will `prepareActions()` and `promptPlayer()`
- The `Action` interface is the way through which the `Player` affects the world - examples:
- Move to a different `Location` using `LocationAction`
- Move to a different `PointOfInterest` using `PoiAction`
- Start with an `EventAction`
- Changing the `Player`s `State` using `StateAction`

#### Other technical features

- Folder scanning (`FolderReader`) to read available event file names by category (e.g. events) which works inside a JAR
and when running via an IDE
- Processing of Yaml files (`YamlReader`) which allows customising event `Participant`s based on their role (
e.g. `eventGiver` or `eventTarget`), `List<Dialogue>` for each based on event status, `List<Reward>`, etc.
- Processing of Txt files (`TxtReader`) which is used to generate `Location`, `PointOfInterest` and `Npc` names
- Processing of `String` placeholders in Yaml or Txt files (`PlaceholderProcessor`) e.g. `&L` for location name
or `&TOF` for the target owner's first name through which events are tailored to the current places and characters
involved

### Technologies used

- Core: Java 19, Spring Boot 3 + Lombok, Gradle
- `guava` for String manipulation
- `snakeyaml` for Yaml processing
- `google-java-format` for formatting

### Other notes

#### More documentation

- [How to create event YAML files](docs/HOW_TO_YAML_EVENTS.md)

#### Random ideas for next steps

- **(User interface)**: Implement Restful API and a web interface as alternative for CLI-based player loop
- **(Procedural generation)**: Implement biomes which:
- Determine difficulty of events, attitude towards player, etc. based on environmental factors
- Determine object characteristics such as names and types of events
- **(Game design)**: Come up with a key objective for the player and an actual (i.e. fun) game loop
- **(Game design)**: Create more `Location` types such as `Castle`
- **(Game design)**: Create more amenities (`PointOfInterest`) with specific functions i.e. shops
- **(Game design)**: Implement player equipment, inventory, and item drops
- **(Game design)**: Implement a trade/currency system and the ability to buy/sell equipment

### Notes

#### Formatter

This project uses `google-java-format`. See https://github.com/google/google-java-format for more details on how to set
it up and use it.

#### How to run JAR

```shell
cd build\libs
java -jar -D"spring.profiles.active"=cli-prod procedural_generation_1-0.1.jar
```

Alternatively, in IntelliJ create new run configuration with path to JAR and with VM
options `-Dspring.profiles.active=cli-prod`.
14 changes: 12 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = 'com.hindsight'
version = '0.0.1-SNAPSHOT'
version = '0.1'

java {
sourceCompatibility = '19'
Expand All @@ -25,8 +25,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.flywaydb:flyway-core'
implementation 'com.google.guava:guava:32.1.2-jre'
implementation 'org.yaml:snakeyaml:2.2'
implementation 'de.codeshelf.consoleui:consoleui:0.0.13'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand All @@ -35,3 +37,11 @@ dependencies {
tasks.named('test') {
useJUnitPlatform()
}

jar {
manifest {
attributes(
'Main-Class': 'com.hindsight.king_of_castrop_rauxel.Application'
)
}
}
121 changes: 121 additions & 0 deletions docs/HOW_TO_YAML_EVENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# How to create event YAML files

This document describes how to create event YAML files. The project uses YAML files to define the content for events.
YAML files stored in `/src/main/resources/events` are read by `YamlReader` and loaded at runtime by the application.

## Structure

YAML files are read to `EventDto` objects. They require the following elements:

1. `eventDetails` -> `EventDetails`
2. `participantData` -> `Map<Role, List<Dialogue>>`

### Event details

The `eventDetails` element must contain the `eventType` which is parsed to `EventType` enum (see `Event` class).
All other fields of the class are optional. An `id` is generated automatically and should not be provided.

```yaml
eventDetails:
eventType: REACH # Event.Type enum
aboutGiver: a delivery # Optional, type String
aboutTarget: the parcel of &O # Optional, type String
rewards: # Optional, type List<Reward>
- type: GOLD # Reward.Type enum
minValue: 2
maxValue: 15
```
### Participant data
The `participantData` element must contain a `Map` of `Role` to `List<Dialogue>`. The `Role` is parsed to `Role` enum (
see `Role` class).

Example:

```yaml
participantData:
EVENT_GIVER: # Role enum
- !dialogue # Indicate mapping to Dialogue class
state: AVAILABLE # Event.State enum
interactions: # List<Interaction> class
- text: Hello! # An interaction; type String
i: 0 # Optional, NOT parsed; type int; interaction number to make it easier to link actions to interactions
- text: How are you?
i: 1
actions:
- !action # Indicate mapping to Action class
name: (Accept) Alright, I'll do it # Action name, type String
eventState: NONE # Event.State enum
nextInteraction: 1 # The next interaction to display after selection;
# this example will lead to an infinite loop
```

### Actions

- The dialogue of an event can only be exited through an action
- An action can change the state of the event (`eventState`) e.g. from `AVAILABLE` to `COMPLETED`
- An action can also change the state of the player (`playerState`) e.g. from `IN_DIALOGUE` to `AT_POI`, marking the end
of the dialogue
- Both `eventState` and `playerState` can be changed in a single action
- If an action should only advance the dialogue to a different interaction, you must set `eventState` to `NONE` and set
`nextInteraction` to the index of the next interaction

### Examples

#### How to create a simple dialogue

```yaml
eventDetails:
eventType: DIALOGUE
# Add Event.Type DIALOGUE enum
# ...and skip all other eventDetails
participantDetails:
EVENT_GIVER:
- ...
```

#### How to branch a dialogue

```yaml
- !dialogue
state: AVAILABLE
interactions:
- text: Can you do this for me?
i: 0
actions: # Add an action
- !action # Indicate mapping to Action class
name: (Accept) Alright, I'll do it
eventState: NONE # Set eventState to NONE
nextInteraction: 1 # Set nextInteraction to anything you want
- !action
name: (Decline) I'm sorry, I can't
eventState: NONE
nextInteraction: 2
- text: Great, thank you!
i: 1
- text: That's a shame!
i: 2
```

Note that in the example above, "That's a shame!" would be displayed right after "Great, thank you!". When doing
branching like this, make sure to use additional actions to change the event state or end the dialogue to avoid this.

#### How to end a dialogue

```yaml
- !dialogue
state: DECLINED
interactions:
- text: Alright, see you around.
actions: # Add an action
- !action # Indicate mapping to Action class
name: Goodbye! # Give it any text/name
playerState: AT_POI # Set playerState to AT_POI
```

## Other notes

Due to the scope and purpose of this project, there is currently no pre-processing of the YAML files. This means that
the YAML files are read when they are assigned to an NPC. This means that any errors in the YAML files will only be
detected at that point.
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1 @@
rootProject.name = 'king_of_castrop_rauxel'
rootProject.name = 'procedural_generation_1'
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package com.hindsight.king_of_castrop_rauxel;

import com.hindsight.king_of_castrop_rauxel.cli.NewGame;
import com.hindsight.king_of_castrop_rauxel.cli.CliGame;
import com.hindsight.king_of_castrop_rauxel.configuration.AppProperties;
import lombok.extern.slf4j.Slf4j;
import org.fusesource.jansi.AnsiConsole;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@Slf4j
@SpringBootApplication
@EnableConfigurationProperties(AppProperties.class)
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
var newGame = new NewGame();
newGame.start();
public static void main(String[] args) {
var context = SpringApplication.run(Application.class, args);
if (Boolean.TRUE.equals(AppProperties.getIsRunningAsJar())) {
AnsiConsole.systemInstall();
}

var newGame = context.getBean(CliGame.class);
newGame.play();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
package com.hindsight.king_of_castrop_rauxel.action;

import static com.hindsight.king_of_castrop_rauxel.characters.Player.*;
import static com.hindsight.king_of_castrop_rauxel.cli.CliComponent.*;

import com.hindsight.king_of_castrop_rauxel.characters.Player;

public interface Action {

int getIndex();

void setIndex(int index);

String getName();
int getNumber();

void setName(String name);

State getNextState();

default void execute(Player player) {
nextState(player);
}

default void nextState(Player player) {
player.setState(getNextState());
}

default String print() {
return "%s[%s%s%s]%s %s"
.formatted(FMT.WHITE, FMT.CYAN, getIndex(), FMT.WHITE, FMT.RESET, getName());
}
}
Loading

0 comments on commit 3bb22e1

Please sign in to comment.