Mini Project - PokeTutor

The aim of this tutorial is to combine techniques learned so far into a mini, text-based, game. It follows the basic premise of Pokemon Go, however in a much simplified format, with tutors being what you are trying to catch.

In the game of Pokemon go, the player throws Pokeballs (standard, great, or ultra) with the hope of capturing a Pokemon. The better the ball, the higher the capture rate. Balls are a fairly simple type, so we can represent them with an enum in Java.

Getting started - creating the Pokeball

  1. Create a new command line project in IntelliJ
  2. Add a new Java Class, but in the drop-down menu that appears change it from 'class' to 'enum'. Name it PokeBall
  3. Modify the code as follows to represent a PokeBall:
    public enum PokeBall {
        STANDARD, GREAT, ULTRA
    }

Creating the PokeTutor

The next stage is to create a class to represent a tutor. In Pokemon Go, the Pokemon have different attributes, such of which are known to the player (such as Hit Points, Combat Points, Name, Weight, Height), and others that are not (for example how likely the Pokemon is to flee when you are attempting to catch it). In order to keep our game simple, for our PokeTutors, we will only consider name, combat points and flee rate

  1. Add a new Java class (in its own file) called PokeTutor
  2. Add variables to the class as follows:
    class PokeTutor {
        private String name;
        private int cp;
        private double fleeRate;
    }
  3. For the name and cp variables, create accessor (getter) methods. Note that we will not allow the name or combat points to be changed, so we do not require mutators. The flee rate (as in the real game) will not be publically accessible, so it will not have an accessor or mutator
  4. Next add a constructor, which will take parameters for all three variables as described above. The constructor will have some custom logic will which randomly allocate a CP value between 10 and the maximumCP value passed into the constructor. This ensures PokeTutors of the same type can be different. The code for this should be as follows:
    PokeTutor(String name, int maxCP, double fleeRate) {
        this.name = name.trim();
        this.fleeRate = fleeRate;
        //randomly allocate CP value up to the maximum
        Random randomGenerator = new Random(); //ensure you add 'import java.util.Random;' to the top of the file
        final int randomCP = randomGenerator.nextInt(maxCP);
        //use the Math class's max method to ensure the CP is not less than 10
        this.cp = Math.max(10,randomCP);
    }

The next stage requires us to consider how a tutor can be captured. In the real game, when a player tries to catch a Pokemon, they can either catch it, they can fail (but try again), or the pokemon can flee.

  1. Inside the PokeTutor class, create an enum to represent these three states, as follows:
    enum CaptureResult {
        FAIL, FLEE, CAUGHT
    }

Determining the capture result

  1. We also need to consider the chance the PokeTutor has of escaping if a ball is thrown at them. On the basis that the max CP of any 'wild' PokeTutor will be 999 (based on the final three digits of their telephone extension) we will calculate a percentage (value between 0 and 1) chance of them escaping by dividing their cp value by 1000. We will further modify that, based on the ball that has been thrown - an ultra ball will reduce it to 1/3 its original value, and a great ball to 2/3. Create the following method (inside the PokeTutor class) to do that:
    private double getEscapeChance(PokeBall ball){
        double escapeChance = cp / 1000.0; //max CP will be 1000
        switch (ball) {
            case GREAT:
                escapeChance *= 0.66666;
                break;
            case ULTRA:
                escapeChance *= 0.33333;
        }
        return escapeChance;
    }
    In the above example, if CP was 600, then the escape chance would start at 0.6 (60%). Should a great ball be thrown, then this would be reduced to 0.4 (40%). A standard ball would not lower the escape chance, so it has been omitted from the switch's list of cases. Also note that the method is private as we will not call it from outside the class.
  2. We also have to consider the possibility of the PokeTutor fleeing. To do this we will simply generate a number between 0 and 1, and compare it to the PokeTutors flee rate (which will be less than 1, but above 0). Add the following private method to do that:
    private boolean shouldFlee(){
        Random random = new Random();
        boolean flee = random.nextDouble() < fleeRate;
        return flee;
    }
  3. We can now create a method which, when called will use the above to methods to determine the capture result. Create this as follows:
    CaptureResult attemptCapture(PokeBall ball){
        final double escapeChance = getEscapeChance(ball);
        Random random = new Random();
        final double chance = random.nextDouble();
        if (chance > escapeChance){
            return CaptureResult.CAUGHT;
        }
        //we will only get this far if it is not caught
        if (shouldFlee()) {
            return  CaptureResult.FLEE;
        } else {
            return CaptureResult.FAIL;
        }
    }
    Note that this method is not marked as private as we will call it from other places within the package

Creating the game logic

In order for the game to work, we need a few more things to be able to happen - we need to be able to

Create the data source

  1. Add a new file to the root of the project, named Data.dat. It should be a text file and contain the following text (and nothing else), which you can copy and paste
    Andrew,98,0.4,
    Linda,106,0.4,
    Graham,101,0.8,
    Phil,304,0.4,
    Nigel,415,0.3,
    Serban,414,0.5,
    Andy,708,0.1,
    Dominic,803,0.5
  2. Next create a class called PokeTutorGame
  3. Add the following method, which will read the file created above, and using the data within, create a series of PokeTutors, which it will return in an ArrayList:
    private ArrayList<PokeTutor> getAllPokeTutorsFromFile(){
        ArrayList<PokeTutor> tutors = new ArrayList<PokeTutor>();
        File dataFile = new File("Data.dat");
        try{
            Scanner fileScanner = new Scanner(dataFile);
            fileScanner.useDelimiter(",");
            fileScanner.useLocale(Locale.UK); //data supplied uses UK style decimal point, this ensures that no matter what locale the system has, the data is read correctly
            while (fileScanner.hasNextLine()) {
                final String name = fileScanner.next();
                final int cp = fileScanner.nextInt();
                final double fleeRate = fileScanner.nextDouble();
                PokeTutor tutor = new PokeTutor(name,cp,fleeRate);
                tutors.add(tutor);
            }
        }
        catch (FileNotFoundException ex){
            System.err.println("Error loading PokeTutors file: " + ex.getMessage());
        }
        return tutors;
    }
    Ensure you add the appropriate import statements to the top of the file

Keeping track and setting up game variables

  1. Next (still within the PokeTutorGame class) add variables to keep track of the number of each type of ball the player has, the PokeTutors that are in the wild, and a single PokeTutor that has been found (if applicable). The code for this should appear as follows:
    private int ballCount;
    private int greatBallCount;
    private int ultraBallCount;
    private ArrayList<PokeTutor> pokeTutorsInWild;
    private ArrayList<PokeTutor> caughtPokeTutors;
    private PokeTutor foundTutor;
  2. Add accessor methods for foundTutor and caughtPokeTutors
  3. You may have also noticed that in the PokeTutor class we instantiated an instance of the Random class a number of times. To avoid that again, we will create a variable to hold this, so add code as follows:
    private Random randomNumberGenerator;
  4. Now add a constructor to set some default ball values and instantiate the various arrays (including the wild PokeTutors)
    PokeTutorGame() {
        randomNumberGenerator = new Random();
        caughtPokeTutors = new ArrayList<PokeTutor>();
        
        ballCount = 5;
        greatBallCount = 2;
        ultraBallCount = 1;
        
        //load tutors from file
        pokeTutorsInWild = getAllPokeTutorsFromFile();
    }

Visiting the Pokestop and collecting / using balls

  1. In order for players to gain more Pokeballs, they have to visit a 'Pokestop'. This will randomly issue some more balls (with a preference to the less powerful balls). It will also return a string which can be used to inform the player of their gains. Add the following method to do that:
    String visitPokestop(){
        //todo: limit result of this method by time
        //todo: use singular of balls when finding only one ball
        int noOfStandardBalls = randomNumberGenerator.nextInt(4); //between 0 - 3
        int noOfGreatBalls = randomNumberGenerator.nextInt(3);
        int noOfUltraBalls = randomNumberGenerator.nextInt(2);
        ballCount += noOfStandardBalls;
        greatBallCount += noOfGreatBalls;
        ultraBallCount += noOfUltraBalls;
        return "Gained "+ noOfStandardBalls+ " balls, " + noOfGreatBalls + " great balls and " + noOfUltraBalls + " ultra balls";
    }
    Ideally players should be limited to the frequency of their visits, which could be done by tracking the time of the last visit, and the wording currently does not consider the singular for when one ball is found, e.g. it would state "Gained 1 balls...". Once you have completed the tutorial, come back and amend these issues.
  2. Players may also want to know how many of each type of ball they have remaining. Add the following method to make that possible
    int getBallCount(PokeBall ballKind){
        switch (ballKind){
            case GREAT:
                return greatBallCount;
            case ULTRA:
                return ultraBallCount;
            case STANDARD:
            default:
                return ballCount;
        }
    }
  3. The game also needs to be able to determine whether a player can use a ball. The following method will check if the type of ball they want to use is available, if so it will decrement the count of that ball and return true, otherwise it will return false
    private boolean useBall(PokeBall ball){
            switch (ball){
                case ULTRA:
                    if (ultraBallCount > 0) {
                        ultraBallCount--;
                        return true;
                    }
                case GREAT:
                    if (greatBallCount > 0) {
                        greatBallCount--;
                        return true;
                    }
                case STANDARD:
                default:
                    if (ballCount > 0){
                        ballCount--;
                        return true;
                    }
            }
            return false;
        }
    We don't want the player using this method directly (it will be called from within the method that attempts a capture, so ensure it is marked as private)

Looking for a PokeTutor

There's no guarantee if you are looking for a tutor that you will find them (especially if an assignment is due...), and in this game, we also don't get to choose which tutor we find - they should appear at random.

  1. Add the following code, which will, half the time, locate a tutor. If a tutor is found, they will be placed in the foundTutor variable, and the code will return true, but if not, it will return false (and the foundTutor variable will be null)
    boolean lookForPokeTutor() {
        double chance = randomNumberGenerator.nextDouble();
        if (chance > 0.5){
            //randomly get a PokeTutor
            int tutorPosition = randomNumberGenerator.nextInt(pokeTutorsInWild.size());
            foundTutor = pokeTutorsInWild.get(tutorPosition);
            return true;
        }
        foundTutor = null;
        return false;
    }
  2. Before we can get on with the business of catching a PokeTutor, we need to ensure that a PokeTutor can be replaced in the wild, if one is caught (so they don't run out). This will be at random, so the mix of wild tutors can change. Add another method as follows to do that:
    private void replacePokeTutor(){
        ArrayList<PokeTutor> allAvailablePokeTutors = getAllPokeTutorsFromFile();
        int randomIndex = randomNumberGenerator.nextInt(allAvailablePokeTutors.size());
        PokeTutor randomTutor = allAvailablePokeTutors.get(randomIndex);
        pokeTutorsInWild.add(randomTutor);
    }

Catching the PokeTutor

The final method of the game class will enable the player to attempt to catch a PokeTutor. Provided the ball required is available, it will attempt to capture the PokeTutor in the foundTutor variable (using the foundTutor's attemptCapture method). It will return the CaptureResult returned by the foundTutor instance, unless there are no balls available, in which case it will return null (as a capture attempt cannot be made).

  1. Add the code as follows (the method definition looks slightly odd because the return type is PokeTutor.CaptureResult ):
    PokeTutor.CaptureResult attemptToCatchTutor(PokeBall ball){
        if (useBall(ball)) {
            final PokeTutor.CaptureResult captureResult = foundTutor.attemptCapture(ball);
            switch (captureResult) {
                case CAUGHT:
                    caughtPokeTutors.add(foundTutor);
                    pokeTutorsInWild.remove(foundTutor);
                    replacePokeTutor();
                    foundTutor = null;
                    break;
                case FAIL:
                    break;
                case FLEE:
                    foundTutor = null;
            }
            return captureResult;
        } else {
            //no pokeballs
            return null;
        }
    }

Tying it all together - implementing the Main class

The listing below shows all the code for the Main class (excluding imports for ArrayList and Scanner)

  1. Create the class as shown below. You are advised to populate the main method last, as otherwise it will display errors until the remaining methods are implemented. Try and ensure you understand the code as you type it.
    public class Main {
        public static void main(String[] args) {
            PokeTutorGame game = new PokeTutorGame();
            Scanner inputScanner = new Scanner(System.in);
    
            while (true) {
                System.out.println("What do you want to do? [L]ook for PokeTutor, [F]ound PokeTutors, [I]tems, [V]isit Pokestop, [E]xit");
                String input = inputScanner.nextLine();
                switch (input.toUpperCase()){
                    case "L":
                        findTutor(game);
                        break;
                    case "F":
                        displayInventory(game);
                        break;
                    case "V":
                        System.out.println(game.visitPokestop());
                        break;
                    case "I":
                        displayBallCounts(game);
                        break;
                    case "E":
                        return; //exits method and therefore programme
                    default:
                        break; //invalid input does nothing, loop begins again
                }
            }
        }
        private static void findTutor(PokeTutorGame game) {
            if (game.lookForPokeTutor()) {
                PokeTutor tutor = game.getFoundTutor();
                System.out.println("Tutor found: " + tutor.getName() + ", CP: " + tutor.getCp());
                while (true) {
                    displayBallCounts(game);
                    System.out.println("Choose: [B]all, [G]reat ball or [U]ltra ball ([E]xit)");
                    Scanner inputScanner = new Scanner(System.in);
                    String ballType = inputScanner.nextLine();
                    if (ballType.equalsIgnoreCase("E")) {
                        break;
                    }
                    PokeBall ball = PokeBall.STANDARD; //default unless U or G entered
                    switch (ballType.toUpperCase()) {
                        case "U":
                            ball = PokeBall.ULTRA;
                            break;
                        case "G":
                            ball = PokeBall.GREAT;
                            break;
                    }
                    PokeTutor.CaptureResult result = game.attemptToCatchTutor(ball);
                    if (result == null) {
                        System.out.println("Attempt failed, no poke balls");
                        return;
                    }
                    switch (result) {
                        case CAUGHT:
                            System.out.println("You caught " + tutor.getName() + " CP:" + tutor.getCp());
                            return; //catch loop exited - tutor caght
                        case FAIL:
                            System.out.println("Failed to catch PokeTutor");
                            break; //catch loop begins again
                        case FLEE:
                            System.out.println("The tutor has fled their office");
                            return; //catch loop exited - tutor fled
                    }
                }
            }
            else{
                System.out.println("Did not find PokeTutor");
            }
        }
        private static void displayBallCounts(PokeTutorGame game){
            int ballCount = game.getBallCount(PokeBall.STANDARD);
            int greatBallCount = game.getBallCount(PokeBall.GREAT);
            int ultraBallCount = game.getBallCount(PokeBall.ULTRA);
            System.out.println("You have " + ballCount + " balls, " + greatBallCount + " great balls, and " + ultraBallCount + " ultra balls");
        }
        private static void displayInventory(PokeTutorGame game) {
            ArrayList<PokeTutor> tutors = game.getCaughtPokeTutors();
            System.out.println("PokeTutors you have caught");
            if (tutors.size() > 0) {
                for (PokeTutor tutor : tutors) {
                    System.out.println(tutor.getName() + ": " + tutor.getCp() + "CP");
                }
                System.out.println();
            }
            else{
                System.out.println("You have not caught any PokeTutors\n");
            }
        }
    }
  2. Run the game. Try and play it. You will find that the Random values could be tweaked for better game play

Further work

  1. Currently, if a player has no ultra balls and tries to use one, the game will use the next strongest type of ball (if available). Make changes so that the user is notified if they try and use a ball that they don't have
  2. Implement the code in the //todo comments
  3. Further enhance the game using some of the concepts from the real Pokemon Go game. This could take the form of anything from ordering the inventory of PokeTutors by name or CP or even date captured (if you add the appropriate field to the tutor class), to implementing power ups.