Main status | Dev status |
---|---|
El Presidente is a Tropico / Reigns game like where you are a dictator on a island.
- El Presidente
- Links
- Installation
- Usage
- Latest version
- Code
- Contributions
Project syllabus : SyllabusDuProjet.pdf
JavaFX for java 11 and above : https://openjfx.io/
How to parse Json file in Java : https://www.baeldung.com/jackson-object-mapper-tutorial
Refactoring : https://refactoring.guru/fr
In both case, once you download / generate the jar file, the executable is standalone and you only need to have Java 11 installed.
Download the latest release of the game at : https://github.com/Nouuu/el-presidente/releases
To build from code, maven 3 is needed.
Simply run mvn install
at the root of the project to generate the latest jar file.
It will be create a jar file named el_presidente-VERSION.jar
and el_presidente_VERSION_standalone.jar
at /root/folder/target/
⚠️ You have to use the standalone version if the environment don't have javafx or fasterxml installed.
to use the app in GUI mode run the .jar by double-click or with command line
If you wan't to run the app in cli mode run the .jar by cli with the option --cli
java -jar el_presidente_1.2.jar --cli
Once you have started the app in GUI mode, a javaFX window will show up and ask you the first thing to do : Choose the difficulty and the scenario.
Try to select Niveau JDG for more fun 😉
Then the game begin !
You can see on the sidebar real-time informations about the chosen scenario, the difficulty, the resources and faction state.
It also show the minimum satisfaction you have to keep at the end of the year or you will loose.
On the right side, an event is displayed with choice to do. Each choice have consequences so be careful !
At the end of the year, depending of the agriculture and industry occupation, you will earn money and food to help you with the next year.
It is the time where you can buy some extra food to avoid famine and bribe some faction to help being above minimum satisfaction.
When you go on next year, you have three case :
- You didn't loose so the scenario continue
- You didn't loose but the scenario is ended (AKA you win)
- You loose because of your satisfaction.
Added
- launch of the project in its initial version
Added
- Easter egg in hardcode
- Refacto of all JavaFX package for better readable
- Add Play/Pause music button
- Fix scenario details not showing if long text
- Add app icon
Added
- CLI option to choice Difficulty and Scenario
- Add a lot of tests for better coverage
This project use Maven 3.
Maven dependencies | Version | Description |
---|---|---|
Junit | 4.13.1 | Used for unit testing |
Cucumber | 6.9.0 | Used for unit testing |
AssertJ | 3.18.1 | Used for unit testing |
JavaFX | 15.0.1 | Used for GUI app |
Apache Commons | 3.11 | Useful libraries functions |
Jackson | 2.12.1 | Help to parse JSON file into Java object |
Jacoco plugin | 0.8.6 | Generate code coverage report from unit tests |
Maven shade plugin | 3.2.4 | Compile code with dependencies to make standalone jar file |
JavaFX plugin | 0.0.5 | Add configuration to assign main class on launch |
We currently added two workflows on our project.
One is triggered when pushing or making a pull request on dev branch, the other works on the same way on main branch.
name: Build & Test Dev
on:
push:
branches: [ dev ]
pull_request:
branches: [ dev ]
name: Build & Test Main
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
Those two workflow do the same thing : build, test and report test coverage.
Once the workflow is triggered, it will :
- Pull the project on the right branch
- Setup JDK 11 on Windows platform
- Search for maven cache from precedent build
- Retrieve missing maven packages
- Run the command
mvn verify
which will build and run unit tests - Upload Jacoco report on Codecov
jobs:
build:
runs-on: windows-latest
steps:
- name: Pull project
uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- name: Cache Maven packages
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Build with Maven
run: mvn --batch-mode --update-snapshots verify
- name: Upload test coverage report on Codecov
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: target/site/jacoco/jacoco.xml
This will generate a badge that indicate if build and tests were successful :
When test coverage report is uploaded on codecov, it analyze our code coverage and generate badge and chart we can analyze to see our efficient is our code coverage :
We can see on each file if each line has been covered, which is just a indicator and not the assurance that our unit test are fine but still very helpful !
There is a config file codecov.yml
that do some check on pull request and push and determine if our code coverage is conform.
It is set to minimum 90% of code coverage with a maximum of 5% variation of coverage between two push
codecov:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "70...100"
status:
project:
default:
# basic
target: 90%
threshold: 5%
base: auto
flags:
- unit
paths:
- "src"
# advanced settings
branches:
- main
- dev
if_ci_failed: error #success, failure, error, ignore
informational: false
only_pulls: false
patch:
default:
# basic
target: 90%
threshold: 5%
base: auto
flags:
- unit
paths:
- "src"
# advanced settings
branches:
- main
- dev
if_ci_failed: error #success, failure, error, ignore
informational: false
only_pulls: false
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: "reach,diff,flags,files,footer"
behavior: default
require_changes: no
Testing is a central point of the project we have tested the whole Core part of the project.
We didn't test the CLI and GUI parts because it's the presentation and we consider that it's bound to evolve too often, there's also a time issue that came into the equation. We preferred to refocus the tests on the logic of the game.
- Classic unit test (Junit + AssertJ) We used 2 patterns for the tests :
- Gerhkin cucumber
In summary the Gerhkin tests use a Given When Then structure to enter the tests readable for the non-technical part of the team they also ensure a good separation in the test code.
Scenario: test food price
Given a game with test scenario in easy
When i get food price
Then the food price should be 8
We use Jacoco and codcov to follow our progress on the tests
The core is the central part of the game. It is the non-graphic part. It is the heart of the game, its logic. It's the core that will manage the events that come from the different interfaces (GUI / CLI).
An event is is something that happens, it choice choices of el-president and these choices will have consequences that can be negative or positive.
An event can have 1 choice (so no choice in fact) to 4 choices.
The events must be able to influence all the parameters of the game (satisfaction of factions, global satisfaction, the money, industry, agriculture or the number of partisans).
private final Season season;
private final String eventDetails;
private final List<EventChoice> eventChoices;
To build our events we use JSON files that describe the details of the event and the possible choices (EventChoice) as well as the impacts it have.
The event choice have potentially implications on several factions (EventFactionEffect)
{
"eventDetails": "Un parseur de fichier fait apparition dans le programme.\nCelui-ci est capable de transformer un fichier json en véritable objet java\n",
"eventChoices": [
{
"choiceName": "Tu saute par dessus bord",
"industryEffect": 7,
"agricultureEffect": -5,
"foodEffect": 8,
"financeEffect": 600,
"factionEffects": [
{
"factionType": "",
"partisansPercentEffect": 1,
"satisfactionEffect": -2
},
]
},
{
"choiceName": "Tu reste dans le bato",
"financeEffect": 60,
"factionEffects": [
{
"factionType": "religieux",
"partisansPercentEffect": 1,
"satisfactionEffect": 1
}
]
}
]
}
when factionType is set to "" is that the action conserns all factions
Events are managed by the EventManager
The residents of the island are separated into seven factions. They are all enumerated with FactionType :
public enum FactionType {
capitalist("capitalistes"),
communist("communistes"),
liberal("liberaux"),
religious("religieux"),
ecologist("écologistes"),
nationalist("nationalistes"),
loyalist("loyalistes");
private final String type;
FactionType(String type) {
this.type = type;
}
@Override
public String toString() {
return type;
}
@JsonCreator
public static FactionType fromString(String string) {
return Arrays.stream(FactionType.values()).filter(o -> o.type.equalsIgnoreCase(string)).findFirst().orElse(null);
}
}
All of these factions have some partisans and a satisfaction percentage.
If a satisfaction percentage hit 0, it will stick to 0 and will never increase again.
Faction manager handle all the faction of the island.
It is an interface that will interact with the faction list and help when some random/batch actions is needed.
public class FactionManager {
private List<Faction> factionList;
...
}
Game is the link between the GUI / CLI and the concrete actions on the resources or factions it also adds the management of time (seasons, years), the triggering of events, it is also here that we will check if the game is lost or not.
This is the last piece of core. It's on the top of the dependency tree.
Game.java
public class Game {
private Difficulty difficulty;
private TimeManager timeManager;
private Scenario scenario;
private FactionManager factionManager;
private RessourceManager ressourceManager;
private Event currentEvent;
private int satisfactionLimit;
It is due to this class that we can use the same code for 2 totally different interface systems.
Helpers are utility classes that encapsulate the complexity of an operation and give it a name.
We have 2 of them, one for interacting with files, the other for doing mathematical operations.
They include only static methodes
MathHelper.java
public class MathHelper {
private MathHelper() {}
public static int multiplyIntDoubleToFloor(int intV, double doubleV) {
return (int) Math.floor(((double) intV) * doubleV);
}
public static int divideIntDoubleToFloor(int intV, double doubleV) {
return (int) Math.floor(((double) intV) / doubleV);
}
public static int restrictValue(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
}
The resources are divided into 3 parts:
- Agriculture
- Industry
- Finance
Agriculture and finance are parts of the island. They are measured as a percentage of the island and the sum of all parts of the island may not exceed 100.
agriculture generates a number of food units each year corresponding to 40 times the percentage of island dedicated to agriculture
The industry generates money at the end of each year corresponding to 10 times the percentage of the island dedicated to industry.
Finance represents the money available in the coffers of the republic. This money can be used to buy food supplements to protect against famine, or to pay bribes to factions.
A Resource Manager is used to make an interface between the game and the different resources. It also allows to manage the different resources and the interactions between them.
The scenario is one of the most important part of the game. It load everything when a new game is started.
It contain the initial money, food, partisans, satisfaction, events, etc...
To be more flexible, each scenario are saved into JSON files that are parsed when you chose the right one at the beginning of the game.
Sandbox is inherit of Scenario and the difference is that include a lot of more events than a classic scenario, and each turn it will pick one randomly according to the current Season.
Where a Scenario is completed when you reach the last event, a Sandbox finish only when you loose.
We use jackson-databind library to map JSON file into instantiated object.
To do that, we use our FileHelper to get the content of the given JSON file, then with some annotations on the object constructor, we map the properties of JSON into properties for constructor.
protected Scenario(@JsonProperty("introduction") String introduction,
@JsonProperty("partisansSatisfaction") int partisansSatisfaction,
@JsonProperty("partisans") int partisans,
@JsonProperty("loyalistPartisansSatisfaction") int loyalistPartisansSatisfaction,
@JsonProperty("loyalistPartisans") int loyalistPartisans,
@JsonProperty("events") List<String> events,
@JsonProperty("initialMoney") int initialMoney,
@JsonProperty("initialFood") int initialFood,
@JsonProperty("initialIndustrialization") int initialIndustrialization,
@JsonProperty("initialAgriculture") int initialAgriculture) throws JsonProcessingException {
this.introduction = introduction;
this.initialPartisansSatisfaction = partisansSatisfaction;
this.initialLoyalistPartisansSatisfaction = loyalistPartisansSatisfaction;
this.initialPartisans = partisans;
this.initialLoyalistPartisans = loyalistPartisans;
this.initialMoney = initialMoney;
this.initialFood = initialFood;
this.initialIndustrialization = initialIndustrialization;
this.initialAgriculture = initialAgriculture;
this.eventManager = new EventManager(getEvents(events));
}
If the JSON file is incorrect, it will throw a JsonProcessingException
that we catch.
One season corresponds to one round of the game
The season is linked to an event because all season have an event
Seasons is represented by an enum, which lists the 4 seasons of a year, we use it for loop over it in a year
public enum Season {
spring("printemps"),
summer("été"),
autumn("automne"),
winter("hiver");
private final String season;
Season(String season) {
this.season = season;
}
@Override
public String toString() {
return season;
}
@JsonCreator
public static Season fromString(String string) {
return Arrays.stream(Season.values()).filter(o ->
o.season
.equalsIgnoreCase(string))
.findFirst().orElse(null);
}
}
The CLI is considered to be the development mode. The CLI is java vanilla, we didn't want to add a library for this part.
The CLI is not very verbose on the possible options for event of end of year actions At the end of the year you get optional reports with the Resources info and the Factions info
The CLI is based on Scanner You must write exactly what is expected
Two build our graphical interface we had two major choices : Swing or JavaFX. We choose JavaFX because it look more recent and more beautiful (Swing 🤢).
Since Java 11, JavaFX is no longer bundled with Java and we had to use maven dependency openjfx to make it works.
To build our interface, we use SceneBuilder which is a WYSIWYG tool that generate FXML file (sound like familiar ... 🤔).
This is the main FX class that contain the static void launchApp()
and make the controller interact with game. This is that method that is called by the launcher when you launch the game.
This is this class that will load our FXML file and link it to FxController.
FxController link the FXML file (and its widgets) to the code. It basically represent our windows, but with attributes and methods.
We just add some annotation to indicate it represent a named widget and ET VOILA :
@FXML
private SplitPane splitPane1;
@FXML
private AnchorPane leftPane;
@FXML
private StackPane rightStackPane;
/////////// LEFT PANE /////////////////////////////////
@FXML
private Text scenarioName;
@FXML
private Text scenarioDetails;
@FXML
private Label difficultyLabel;
@FXML
private ImageView jdgImage;
@FXML
private VBox statusVbox;
@FXML
private ListView<String> gameInfos;
@FXML
private ListView<String> factionsInfos;
@FXML
private void onChooseGameMode() throws JsonProcessingException {
ScenarioList scenario = scenarioComboBox.getValue();
Difficulty difficulty = difficultyComboBox.getValue();
if (scenario != null && difficulty != null) {
fxApp.chooseGameMode(scenario, difficulty, scenarioName, scenarioDetails, difficultyLabel);
setVisibleStackPane(eventPane);
jdgImage.setVisible(difficulty.equals(Difficulty.HARDCORE));
statusVbox.setVisible(true);
getNextEvent();
}
}
This class wrap Game object and add some specific getters and setters (and some exotics methods) to make game compatible with our graphical interface.
public void refreshGameInfos() {
gameInfosObservable.clear();
gameInfosObservable.add("Argent : " + game.getRessourceManager().getMoney() + "$");
gameInfosObservable.add("Nourriture : " + game.getRessourceManager().getFoodReserves());
gameInfosObservable.add("Occupation industrie : " + game.getRessourceManager().getIndustryPart() + "%");
gameInfosObservable.add("Occupation agriculture : " + game.getRessourceManager().getAgriculturePart() + "%");
gameInfosObservable.add("Satisfaction globale minimum : " + game.getSatisfactionLimit() + "%");
gameInfosObservable.add("Satisfaction globale actuelle : " + game.getFactionManager().getGlobalSatisfaction() + "%");
gameInfosObservable.add("Population totale : " + game.getFactionManager().getTotalPartisan());
factionsInfosObservable.clear();
game.getFactionManager().getFactionList().forEach(f ->
factionsInfosObservable.add(
StringUtils.capitalize(f.getFactionType().toString())
+ "\nPartisans : " + f.getPartisans()
+ "\nSatisfaction : " + f.getSatisfaction() + "%"));
}
FxMusic is a class that is part of GUI (made in JavaFX) that contains functions related to sound management in the interface.
It contains an audio player and a playlist that loops
public class FxMusic {
private MediaPlayer player;
private final List<Media> playlist;
FxMusicList is an enum that contains all the music and the path to the file
public enum FxMusicList {
GLORIOUS_MORNING("/music/glorious-morning.mp3"),
GLORIOUS_MORNING_2("/music/glorious-morning-2.mp3");
Noé LARRIEU-LACOSTE | |
Swann HERRERA |