3
\$\begingroup\$

This is my first java program. I'm coming from a python background. This is a text based combat arena game.

  1. Are there any ways I could better implement the overall code structure?
  2. 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[H\033[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");
       }
    }
}
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

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 
\$\endgroup\$
4
  • \$\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\$ Commented Jan 26 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 the clash scenario, it calls attack() with a hard-coded hero as the attacker and enemy as the defender. This would be safe and free from recursion. \$\endgroup\$
    – Reinderien
    Commented Jan 26 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\$ Commented Jan 26 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
    Commented Jan 26 at 15:55
1
\$\begingroup\$

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();
            }
        } 
    }
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.