This is my first java program. I'm coming from a python background. This is a text based combat arena game.
- Are there any ways I could better implement the overall code structure?
- How might I improve the math of the
attack()
function?
It prompts the user for a hero name, then generates hero and enemy objects of the character class with randomly generated Health, Defense, and Strength stats between 1-100 and prompts user for an option.
1 initiates battle
2 quits
Selection 1 starts an attack iteration where:
The game rolls for each character (random 1-6) to determine who strikes first and resolves the attack results, prints updated stats and returns to prompt the two options. Something slightly interesting happens when they tie the initiative roll.
When a character's health is reduced below 1, the victor is announced and the game quits.
Some announcer text is printed depending on different outcomes for interest.
Game.java
import java.util.Scanner;
import java.io.*;
public class Game {
public class Character {
int health;
int defense;
int strength;
String cname;
int init;
public Character(String name) {
health = getRandom(100, 1);
defense = getRandom(100, 1);
strength = getRandom(100, 1);
cname = name;
init = 0;
}
}
static int getRandom(int max, int min) {
int num = 1 + (int)(Math.random() * ((max - min) + 1));
return num;
}
static void cls() {
System.out.print("033円[H033円[2J");
System.out.flush();
}
static String printWelcome() {
cls();
System.out.println("Welcome to the arena!");
Scanner scanObj = new Scanner(System.in);
System.out.println("Enter your hero\'s name:");
String heroName = scanObj.nextLine();
cls();
return heroName;
}
static void printStats(Character c) {
Console cnsl = System.console();
String fmt = "%1$-10s %2$-1s%n";
System.out.println("\n" + c.cname + "\'s Stats:\n---------------");
cnsl.format(fmt, "Health:", c.health);
cnsl.format(fmt, "Defense:", c.defense);
cnsl.format(fmt, "Strength:", c.strength);
}
static void clash(Character h, Character e) {
System.out.println("\n" + e.cname + " took a cheapshot!\n(Croud gasps)\nBut " + h.cname + " blocked it in the nick of time!\n(Croud Chears)\n");
doBattle(h, e);
}
static Character roll(Character h, Character e) {
h.init = getRandom(6, 1);
e.init = getRandom(6, 1);
if (h.init > e.init) {
return h;
} else if (h.init < e.init) {
return e;
} else {
clash(h, e);
return e;
}
}
static void attack(Character a, Character d) {
int apts;
String aname = a.cname;
String dname = d.cname;
if (d.defense > a.strength) {
apts = 1;
d.defense = d.defense - ((d.defense % a.strength) + 1);
System.out.println("\n" + dname + " blocked " + aname + "\'s attack and took no damage!\n(Croud chears)\n");
} else {
apts = a.strength - d.defense;
d.health = d.health - apts;
System.out.println("\n" + aname + " strikes " + dname + " for " + apts + " points of damage!\n(Croud boos)\n");
}
if (d.health < 1) {
d.health = 0;
}
}
static void doBattle(Character h, Character e) {
Character goesFirst = roll(h, e);
System.out.println(goesFirst.cname + " takes initiative!\n");
Character defender;
if (h.cname == goesFirst.cname) {
defender = e;
} else {
defender = h;
}
attack(goesFirst, defender);
// System.out.println(defender.cname);
}
static int getOption() {
Scanner scanObj = new Scanner(System.in);
System.out.println("\nEnter option: (1 to battle, 2 to escape!)");
int option = scanObj.nextInt();
return option;
}
public static void main(String[] args) {
Game myGame = new Game();
Game.Character hero = myGame.new Character(printWelcome());
Game.Character enemy = myGame.new Character("Spock");
System.out.println("\nAvast, " + hero.cname + "! Go forth!");
// printStats(hero);
// printStats(enemy);
while (hero.health > 0 && enemy.health > 0) {
printStats(hero);
printStats(enemy);
int option = getOption();
cls();
if (option == 1) {
doBattle(hero, enemy);
} else if (option == 2) {
System.out.println("YOU COWARD!");
System.exit(0);
} else {
System.out.println("Invalid Option");
}
}
printStats(hero);
printStats(enemy);
if (hero.health < 1) {
System.out.println(enemy.cname + " defeated " + hero.cname + "!\n(Cround boos aggressively)\nSomeone from the croud yelled \"YOU SUCK!\"\n");
} else {
System.out.println(hero.cname + " utterly smote " + enemy.cname + "!\n(Croud ROARS)\n");
}
}
}
2 Answers 2
Make as many of the properties of Character
to be final
as possible. init
(which should be named initiative
) should not be a property at all.
Delete getRandom
. Pass in an instance of Random
for testability, and call its nextInt
. Make Character
a static
inner class - it won't be able to access properties of Game
, and that's a good thing - it's better to pass them in explicitly when needed.
Avoid static
abuse on your outer functions. Most of your Game
methods should be instance methods with access to the member variables they need.
cls
(clearing the screen) is an anti-feature. The user can clear the screen when they want through their own terminal controls.
Don't \'
escape apostrophes when that isn't needed.
There is no need for Console
in this application.
Make more use of printf
instead of string concatenation.
Don't abbreviate variables like h
(hero) and e
(enemy) to single letters; spell them out.
Various spelling issues like croud
(should be crowd
); enable spellcheck. Make your verb tenses agree - they should all be in present tense, instead of a mix of past and present tense.
clash
calling doBattle
is recursion, which is not a good idea; you almost certainly shouldn't be recursing here.
Don't nextInt
for your option. Since it's only used in a simple comparison, leave it as a string; it removes the potential for an exception and simplifies validation.
Whenever possible, construct your console formatting so that it produces logically-related paragraphs. Also, prefer accepting terminal input on the same line as the prompt.
Suggested
package com.stackexchange;
import java.util.Optional;
import java.util.Random;
import java.util.Scanner;
public class Game {
public record Hit(
boolean blocked,
int attack_points
) {
@Override
public String toString() {
if (attack_points == 0)
return "no damage";
if (attack_points == 1)
return "1 point of damage";
return "%d points of damage".formatted(attack_points);
}
}
public static class Character {
private int health;
private int defense;
private final int strength;
public final String name;
public Character(String name, Random rand) {
health = rand.nextInt(1, 100);
defense = rand.nextInt(1, 100);
strength = rand.nextInt(1, 100);
this.name = name;
}
public boolean isAlive() {
return health > 0;
}
public Hit receiveHit(Character attacker) {
int attack_points = Integer.max(0, attacker.strength - defense);
boolean blocked = attack_points == 0;
if (blocked)
defense -= (defense % attacker.strength) + 1;
health = Integer.max(0, health - attack_points);
return new Hit(blocked, attack_points);
}
private void printStats() {
System.out.printf("%s's Stats:%n", name);
System.out.println("---------------");
final String fmt = "%-10s %-2d%n";
System.out.printf(fmt, "Health:", health);
System.out.printf(fmt, "Defense:", defense);
System.out.printf(fmt, "Strength:", strength);
System.out.println();
}
@Override
public String toString() {
return name;
}
}
private final Random rand = new Random();
private final Scanner scanner = new Scanner(System.in);
private final Character hero;
private final Character enemy = new Character("Spock", rand);
public Game() {
System.out.println("Welcome to the arena!");
System.out.print("Enter your hero's name: ");
hero = new Character(scanner.nextLine(), rand);
System.out.printf("Avast, %s! Go forth!%n", hero);
}
public void clash() {
System.out.printf("%s takes a cheap shot!%n", enemy);
System.out.println("(Crowd gasps)");
System.out.printf("But %s blocks it in the nick of time!%n", hero);
System.out.println("(Crowd cheers)");
}
public Optional<Character> rollInitiative() {
int hero_initiative = rand.nextInt(1, 7),
enemy_initiative = rand.nextInt(1, 7);
if (hero_initiative > enemy_initiative)
return Optional.of(hero);
if (hero_initiative < enemy_initiative)
return Optional.of(enemy);
return Optional.empty(); // tie ("cheap shot")
}
public void attack(Character attacker, Character defender) {
Hit hit = defender.receiveHit(attacker);
if (hit.blocked) {
System.out.printf("%s blocks %s's attack and takes %s!%n", defender, attacker, hit);
System.out.println("(Crowd cheers)");
}
else {
System.out.printf("%s strikes %s for %s!%n", attacker, defender, hit);
System.out.println("(Crowd boos)");
}
}
public void doBattle() {
Optional<Character> goesFirst = rollInitiative();
if (goesFirst.isPresent()) {
Character attacker = goesFirst.get();
System.out.printf("%s takes initiative!%n", attacker);
Character defender = hero == attacker? enemy: hero;
attack(attacker, defender);
} else {
clash(); // tie ("cheap shot")
}
}
public void run() {
do {
System.out.println();
hero.printStats();
enemy.printStats();
if (!hero.isAlive()) {
System.out.printf("%s defeats %s!%n", enemy, hero);
System.out.println("(Crowd boos aggressively)");
System.out.println("Someone from the crowd yells \"YOU SUCK!\"");
break;
}
if (!enemy.isAlive()) {
System.out.printf("%s utterly smites %s!%n", hero, enemy);
System.out.println("(Crowd ROARS)");
break;
}
} while (doRound());
}
private boolean doRound() {
while (true) {
System.out.print("Enter option (1 to battle, 2 to escape)! ");
switch (scanner.nextLine()) {
case "1":
doBattle();
return true;
case "2":
System.out.println("YOU COWARD!");
return false;
default:
System.err.println("Invalid option");
}
}
}
public static void main(String[] args) {
new Game().run();
}
}
Output
Welcome to the arena!
Enter your hero's name: Kirk
Avast, Kirk! Go forth!
Kirk's Stats:
---------------
Health: 68
Defense: 59
Strength: 50
Spock's Stats:
---------------
Health: 56
Defense: 3
Strength: 49
Enter option (1 to battle, 2 to escape)! 1
Spock takes a cheap shot!
(Crowd gasps)
But Kirk blocks it in the nick of time!
(Crowd cheers)
Kirk's Stats:
---------------
Health: 68
Defense: 59
Strength: 50
Spock's Stats:
---------------
Health: 56
Defense: 3
Strength: 49
Enter option (1 to battle, 2 to escape)! 1
Kirk takes initiative!
Kirk strikes Spock for 47 points of damage!
(Crowd boos)
Kirk's Stats:
---------------
Health: 68
Defense: 59
Strength: 50
Spock's Stats:
---------------
Health: 9
Defense: 3
Strength: 49
Enter option (1 to battle, 2 to escape)! 1
Spock takes a cheap shot!
(Crowd gasps)
But Kirk blocks it in the nick of time!
(Crowd cheers)
Kirk's Stats:
---------------
Health: 68
Defense: 59
Strength: 50
Spock's Stats:
---------------
Health: 9
Defense: 3
Strength: 49
Enter option (1 to battle, 2 to escape)! 1
Spock takes a cheap shot!
(Crowd gasps)
But Kirk blocks it in the nick of time!
(Crowd cheers)
Kirk's Stats:
---------------
Health: 68
Defense: 59
Strength: 50
Spock's Stats:
---------------
Health: 9
Defense: 3
Strength: 49
Enter option (1 to battle, 2 to escape)! 1
Spock takes initiative!
Kirk blocks Spock's attack and takes no damage!
(Crowd cheers)
Kirk's Stats:
---------------
Health: 68
Defense: 48
Strength: 50
Spock's Stats:
---------------
Health: 9
Defense: 3
Strength: 49
Enter option (1 to battle, 2 to escape)! 1
Spock takes a cheap shot!
(Crowd gasps)
But Kirk blocks it in the nick of time!
(Crowd cheers)
Kirk's Stats:
---------------
Health: 68
Defense: 48
Strength: 50
Spock's Stats:
---------------
Health: 9
Defense: 3
Strength: 49
Enter option (1 to battle, 2 to escape)! 1
Spock takes initiative!
Spock strikes Kirk for 1 point of damage!
(Crowd boos)
Kirk's Stats:
---------------
Health: 67
Defense: 48
Strength: 50
Spock's Stats:
---------------
Health: 9
Defense: 3
Strength: 49
Enter option (1 to battle, 2 to escape)! 1
Kirk takes initiative!
Kirk strikes Spock for 47 points of damage!
(Crowd boos)
Kirk's Stats:
---------------
Health: 67
Defense: 48
Strength: 50
Spock's Stats:
---------------
Health: 0
Defense: 3
Strength: 49
Kirk utterly smites Spock!
(Crowd ROARS)
Stats tables
A simple way to condense your stats to a table-like format could look like
public static void printStats(Character... chars) {
System.out.print(" ".repeat(9));
for (Character col: chars)
System.out.printf("%8s ", col);
System.out.println();
System.out.print("-".repeat(8));
for (Character col: chars)
System.out.print("-".repeat(9));
System.out.println();
System.out.printf("%8s ", "Health");
for (Character col: chars)
System.out.printf("%8d ", col.health);
System.out.println();
System.out.printf("%8s ", "Defense");
for (Character col: chars)
System.out.printf("%8d ", col.defense);
System.out.println();
System.out.printf("%8s ", "Strength");
for (Character col: chars)
System.out.printf("%8d ", col.strength);
System.out.printf("%n%n");
}
called like
Character.printStats(hero, enemy);
with output
Kirk Spock
--------------------------
Health 78 42
Defense 99 5
Strength 93 97
-
\$\begingroup\$ Thanks, this helps me understand how to "Java better". On the suggestion to remove the recursive doBattle() call from clash(), I did that to simulate either character immediately counter-attacking or continuing the attack based on random initiative instead of it just prompting the user to continue again, because I felt that was a little more interesting. Do you have any suggestions to implement that concept in a different way? \$\endgroup\$spaghetticode– spaghetticode2024年01月26日 14:06:13 +00:00Commented Jan 26, 2024 at 14:06
-
1\$\begingroup\$ If an initiative tie always implies the enemy taking a cheap shot and the hero counter-attacking, then you can modify
doBattle()
such that in theclash
scenario, it callsattack()
with a hard-coded hero as the attacker and enemy as the defender. This would be safe and free from recursion. \$\endgroup\$Reinderien– Reinderien2024年01月26日 14:18:00 +00:00Commented Jan 26, 2024 at 14:18 -
\$\begingroup\$ Well the intent was to let it go either way after clash so it would have to call rollInitiative() again, so maybe I could put a do while loop in the doBattle() function to track whether clashes are occurring to allow it to continue or return to prompt. \$\endgroup\$spaghetticode– spaghetticode2024年01月26日 15:30:28 +00:00Commented Jan 26, 2024 at 15:30
-
1\$\begingroup\$ In a case like that, you can run a tie-breaking call to
rand.nextBoolean()
to decide who gets initiative. \$\endgroup\$Reinderien– Reinderien2024年01月26日 15:55:59 +00:00Commented Jan 26, 2024 at 15:55
I rewrote my game based on Reinderien's suggested code including the combined stat table with some small differences:
The receiveHit function causes defense to reduce by a different modifier based on whether the attack was fully blocked or not. And it reduces the attacker's strength by a modifier to simulate exhaustion.
public Hit receiveHit(Character attacker) {
int attack_points = Integer.max(0, attacker.strength - defense);
boolean blocked = attack_points == 0;
int modifier;
if (blocked) {
modifier = (defense % attacker.strength) + 1;
defense -= modifier;
attacker.strength -= modifier / 2;
} else {
modifier = (defense % attack_points) + 1;
defense -= modifier;
attacker.strength -= modifier / 2;
}
health = Integer.max(0, health - attack_points);
return new Hit(blocked, attack_points);
}
I added a while loop to cause the doBattle function to automatically continue into another iteration if the characters tie the rollInitiative and clash which means either character could follow up with an attack based on the next initiative roll without the user prompt.
- Scenario 1: Spock takes a cheapshot and hero blocks but spock immediately follows up with another successful attack.
- Scenario 2: Spock takes a cheapshot and hero blocks and successfully counterattacks.
public void doBattle() {
boolean go = true;
while (go) {
Optional<Character> goesFirst = rollInitiative();
if (goesFirst.isPresent()) {
Character attacker= goesFirst.get();
System.out.printf("%n%s takes initiative!%n%n", attacker);
Character defender = (hero == attacker) ? enemy : hero;
attack(attacker, defender);
go = false;
} else {
clash();
}
}
}