2
\$\begingroup\$

I've been coding for around a month, learning through CodeHS and coding within their sandbox tool. I am currently working on a text-based explorer/RPG game in order to practice Object-Oriented Programming in Java.

I wanted to know if the current relationship between the Location and the Explore class is optimal, as I am not sure if accessing the instance variables of Location within Explore is considered to be bad practice. Also, my current method of determining whether or not a generated location is for example a stream seems inefficient, as it simply checks if it contains the strings "stream" or "river". Any help with design is greatly appreciated as well.

The Explore Class:

public class Explore{

    public Location location;
    public Player player;
    //distances from the town originated and to next town
    public int distanceFrom;
    public int distanceTo;

    public Explore(Player player){
        this.player = player;
        this.distanceFrom = 0;
        this.distanceTo = RNG.range(3, 6);
        this.location = new Location(player);
    }
    public int getDistanceFrom(){
        return distanceFrom;
    }
    public int getDistanceTo(){
        return distanceTo;
    }

    public boolean isStream(){
        return location.isStream;
    }
    public boolean canDrink(){
        return location.canDrink;
    }
    public void drink(){
        location.canDrink = false;
    }

    public String getDesc(){
        return location.toString() + " " + distanceFrom + " km away from your origin";
    }
    public Location getLocation(){
        return location;
    }

    public void advance(){
        if(distanceFrom <= 5){
            player.incrementDev(RNG.range(1, 4));
        }else{
            player.incrementDev(RNG.range(3, 6));
        }

        this.location = new Location(player);
        this.distanceFrom++;
        this.distanceTo--;
    } 

    public boolean checkNearbyTown(){
        if(distanceTo <= 1 && distanceTo > -1){
            return true;
        }else{
            return false;
        }
    }
}

The Location Class:

public class Location
{
    private Player player;
    private String desc;

    public boolean isStream = false;
    public boolean isCave = false;
    public boolean canDrink = false;
    //add caves later
    //public boolean canCave = false;

    public Location(Player player){
        this.player = player;  
        this.desc = RNG.locationDesc(player);

        if((this.desc.indexOf("stream") != -1) || (this.desc.indexOf("river") != -1)){
            this.isStream = true;
            this.canDrink = true;
        }
    }

    public String toString(){
        return desc;
    }
}

The RNG Class, which handles my games random events:

import java.util.*;

public class RNG{

    //Handles all random events such as pulling a number from a range or generating a name/stats
    public static int roll(int sides){
        int aRoll = (int)(Math.random() * sides + 1);
        return aRoll;
    }
    public static int range(double min, double max){
        return (int)(Math.random() * (max - min + 1) + min);
    }

    public static String locationDesc(Player player){
        int dev = player.getDev();
        //takes in a player input in order to determine their insanity, making a more twister are
        String[] desc;
        if(dev <= 75){
            desc = new String[]{"an unassuming wooded area", "an open flower field", 
                "a steady stream", "the entrance of a small cave", "a small river"};
        }else{
            desc = new String[]{"Spookier areas to add later"};
        }

        int index = (int)(Math.random() * desc.length);
        return desc[index];
    }
}
\$\endgroup\$
  • \$\begingroup\$ My main concern is that I don't understand what 'Explore' is supposed to mean. In OO try to model the objects as close to real-world things as possible. What is an 'Explore'? \$\endgroup\$ – RobAu Nov 30 '17 at 8:01
  • \$\begingroup\$ @RobAu It's supposed to be like an adventure, I agree that Explore wasn't necessarily the best word to describe what the function of it is \$\endgroup\$ – WatCow Nov 30 '17 at 14:19
  • \$\begingroup\$ so what is it's function? \$\endgroup\$ – RobAu Nov 30 '17 at 16:12
  • \$\begingroup\$ @RobAu Its function is to basically create a path of a randomly generated length from the current town to a new town for the player, and the player is able to advance from location to location, and some locations give the player the additional option to drink water or explore a cave for example \$\endgroup\$ – WatCow Dec 1 '17 at 3:53
  • \$\begingroup\$ Then I agree with @iangreen's answer: just get rid of it. Location seems to cover your needs and makes more sense in 'explaining your classes' as well. \$\endgroup\$ – RobAu Dec 1 '17 at 8:00
3
\$\begingroup\$

thanks for sharing your code!

I want to start by saying that I think this looks pretty good for someone only having been coding for around a month, but I think you're getting some concepts mixed up or not quite sure how/where/why to use them.

The other comments and answers have already pointed out flaws with the Explore class, so I won't repeat those

Also, my current method of determining whether or not a generated location is for example a stream seems inefficient, as it simply checks if it contains the strings "stream" or "river"

It's a good sign that this jumped out as not being the best idea! There are a few problems with your design here, let's have a look.

Your Location class contains these instance variables

public boolean isStream = false;
public boolean isCave = false;
public boolean canDrink = false;

There are 2 pretty big problems here

  1. You almost never want to make your instance variables public. You should use a getter if you actually need to see that variable outside the class (which would happen less often than you would think)
  2. Your location class contains information that specifies what type of location it is!

If we wanted to make a new Location, which I think is a very reasonable thing to do. We would need to go and change the code in the Location class itself. Ideally, if we wanted to make a brand new location, say a Town. All we should need to do is make a Town class, and slot it in. Let's have a look at how we could do that.

Firstly, we could make the Location class, an interface instead

public interface Location {
    void explore(Player player); // note explore is a method (verb), not a noun (class)
}

Now, in order to be a Location, we just need to implement an explore method. So instead of maintaining booleans which describe the Location concrete class, we can make many smaller implementations instead. Let's have a look at a potential Cave implementation.

public class Cave implements Location {

    @Override
    public void explore(Player player) {
        System.out.println(player.getName() + " is at the entrance of a small cave.");
        // do scary cave things to the player here.
    }
}

Now, the Location interface itself doesn't know anything about whether or not it's a Cave, or Stream or Town etc. Because it doesn't need to know!

What about a Town?

public class Town implements Location {

    @Override
    public void explore(Player player) {
        System.out.println(player.getName() + " arrived at a small town.");
        // do whatever you want to do with the player in the town here.
    }
}

again, very similar to Cave, you could even consider making an Abstract Base Class instead of an interface, but let's keep going with this for now.

And one more example

public class River implements Location{

    @Override
    public void explore(Player player) {
        System.out.println(player.getName() + " is at a river, it's nice and relaxing.");
    }
}

Okay, so now we have more smaller classes that each are specialized, but how is this good? My main goal here, is to not need to alter code when I create a new implementation of Location

You don't have an example of the Player class or the main Runner class, so for now a Player just has a getName method. You can of course do whatever you want in your Player class.

Instead of an Explore class, let's make a Game class. That's what this is after all, an RPG.

public class Game {

    private List<Location> locations;
    private Player player;

    public Game(List<Location> locations, Player player) {
        this.locations = new LinkedList<>(locations);
        this.player = player;
    }

    public void play() {
        System.out.println(player.getName() + " began their adventure.");
        for (Location location : locations) {
            location.explore(player);
            System.out.println(player.getName() + " is moving on to the next location.");
        }
    }
}

So there's not a whole lot of code here, our Game just makes runes a player through all the levels.

It we then just do a short Runner class to tie it all together.

public class Runner {
    public static void main(String[] args) {

        Player player = new Player("Bob The Explorer");

        List<Location> locations = new LinkedList<>();
        locations.add(new Cave());
        locations.add(new Town());

        Game rpg = new Game(locations, player);
        rpg.play();
    }
}

This short example gives the following.

Bob The Explorer began their adventure.
Bob The Explorer is at the entrance of a small cave.
Bob The Explorer is moving on to the next location.
Bob The Explorer arrived at a small town.
Bob The Explorer is moving on to the next location.

I've used a larger number of smaller classes here. We now have the benefit of easily being able to add a new Location to our game, we just make a new class, implement Location, and say what should happen when the explore method is called!

Of course if you needed to do more things than just you explore, you could add new methods. But I'm a big fan of having as few public methods as possible in any given class.

Your logic for checking number of tiles to towns etc. could all be done in the Game class.

In your example, you have some logic around maintaining knowledge about whether or not you can drink from a location, I'll just add one more quick example about how you can do this, let's rework the River class just a little bit.

public class River implements Location{

    private boolean canDrink;

    public River(){
        canDrink = true;
    }
    @Override
    public void explore(Player player) {
        System.out.println(player.getName() + " is at a river, it's nice and relaxing.");
        if(canDrink){
            canDrink = false;
            player.restoreStamina(); // or whatever "drinking" from the river should do!
            System.out.println(player.getName() + " feels refreshed after drinking from the river.");
        } else {
            System.out.println(player.getName() + " has already drank from the river, it's all gone!");
        }
    }
}

It's still a valid implementation of Location, but we can add more stateful behaviour to it. (And the Game class will never know!)

Let's have a look at what main looks like now

public class Runner {
    public static void main(String[] args) {

        Player player = new Player("Bob The Explorer");

        List<Location> locations = new LinkedList<>();
        Location river = new River();
        locations.add(new Cave());
        locations.add(river);
        locations.add(new Town());
        locations.add(river);

        Game rpg = new Game(locations, player);
        rpg.play();
    }
}

With an output of

Bob The Explorer began their adventure.
Bob The Explorer is at the entrance of a small cave.
Bob The Explorer is moving on to the next location.
Bob The Explorer is at a river, it's nice and relaxing.
Bob The Explorer feels refreshed after drinking from the river.
Bob The Explorer is moving on to the next location.
Bob The Explorer arrived at a small town.
Bob The Explorer is moving on to the next location.
Bob The Explorer is at a river, it's nice and relaxing.
Bob The Explorer has already drank from the river, it's all gone!
Bob The Explorer is moving on to the next location.

So we've added new functionality to an existing location (we could have just as easily added a new one!)

And the Game class remains unchanged.

I've done location.explore(player), this might make more sense as being player.explore(location) or location.beExploredBy(player)

Hopefully this review was helpful for you!

\$\endgroup\$
  • \$\begingroup\$ Thank you! So it is okay to print out directly to the console from within a class, I wasn't sure about if that was proper or not. Also sub classes sound to be a lot more efficient to implement rather than my current way of handling different locations, thank you! \$\endgroup\$ – WatCow Dec 2 '17 at 20:25
3
\$\begingroup\$

Explore is a verb, so your Explore class should be called Exploration, or probably something more descriptive like PlayerStatus. And you are defining state in Location and then just re-exposing it in Explore. Merge the two classes if you notice they are bleeding. You are right to be questioning that and noticing something isn't right. What is the point of Explore? Merge it into Location.

\$\endgroup\$
1
\$\begingroup\$

Also the RNG is bound to become a god-class, if you allow methods like locationDesc there.

RNG's job should be to provide random numbers, or return a random element from a list, array, etc. The location description should come from Location.

Something like this:

class Location 
...
   public static final String [] descriptions= { .... } 
   public static final String [] spookyDescriptions= { .... } 
...
   public String getDescription()
   {
        //get appropriate descriptions for current player;
        String[] desc = getDescriptionsForPlayer(this.player)
        return RNG.pick( desc );
   }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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