Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I add Levels to my game with less duplication of code?

I am designing a game with multiple levels. I have a setup class that sets up the board based on the argument it receives, which indicates which level it should set up. Here is the class:

public class BoardState {
    public BoardState(InitialState state) {
        switch (state) {
            case EMPTY:
                setupEmptyState();
                break;
            case INTEGRATIONTEST:
                setupIntegrationTestState();
                break;
            case LEVEL_1:
                setupLevelOne();
                break;
            case LEVEL_2:
                setupLevelTwo();
                break;
            default:
                throw new Error("Invalid level selection");
        }
    }

    private void setupEmptyState() { }

    private void setupIntegrationTestState() { }

    private void setupLevelOne() { }

    private void setupLevelTwo() { }
}

This works fine, but every time I add a new level I have to add code in three places: The InitialState enum which defines the list of accepted states, the switch statement in the constructor, and the body of the class, where I have to add a method to set up the level in question.

One nice thing that I want to keep is the fact that my GUI automatically populates with a new button for each level I add based on the enum defining the list of levels.

How can I refactor this code so that there is less overhead associated with adding a new level?

like image 518
nhouser9 Avatar asked Dec 22 '16 01:12

nhouser9


3 Answers

You could use inheritance, polymorphism is the keyword here.

Set up your InitialState class as abstract base class (or interface if you have no common fields) and define a method public abstract void setup();.

abstract class InitialState {
    public abstract void setup();
}

Then, for each of your original switch cases, derive a class from your base class, for example LevelOne, and implement its specific by overriding setup().

class LevelOne extends InitialState {
    @Override
    public void setup() {
        // The code from "setupLevelOne()" goes here
    }
}

Your BoardState class reduces to this:

public class BoardState {
    public BoardState(InitialState state) {
        // At runtime, the right method of the actual
        // state type will be called dynamically
        state.setup();
    }
}

However, if you need to set interal state of your BoardState class, consider defining the setup method as public abstract void setup(BoardState boardState), so you can access its getter and setter methods.

This appraoch could also foster reuse of code, as you could add several abstract layers for different types of levels.

like image 38
thatguy Avatar answered Oct 13 '22 19:10

thatguy


Often when you need to reduce code duplication, an interface arise. This time (based on your comment in OP) it seems you need to add different objects to the board depending on which level you are:

import java.util.List;

public interface LevelSettings {
    List<GameObject> startingObjects();
}

Now, BoardState looks like that (no more setupX() methods)

import java.util.List;

public class BoardState {
    private final List<GameObject> gameObjects;

    public BoardState(LevelSettings settings) {
        this.gameObjects = settings.startingObjects();
    }
}

Since you also specified it is nice for you to have an enum to dynamically creates buttons on the GUI, one can combine the best of both world (interface and enum) by implementing the interface in an enum...

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public enum InitialState implements LevelSettings {
    EMPTY {
        @Override
        public List<GameObject> startingObjects() {
            return Collections.emptyList();
        }
    },
    INTEGRATIONTEST {
        @Override
        public List<GameObject> startingObjects() {
            GameObject g1 = new GameObject("dummy 1");
            GameObject g2 = new GameObject("dummy 2");
            return Arrays.asList(g1, g2);
        }
    },
    LEVEL_1 {
        @Override
        public List<GameObject> startingObjects() {
            //read a config file to get the starting objects informations
            //or also hardcoded (not preferred)
        }
    },
    LEVEL_2 {
        @Override
        public List<GameObject> startingObjects() {
            //read a config file to get the starting objects
            //or also hardcoded (not preferred)
        }
    };
}

And that's it basically. If you need to add LEVEL_3 do it in InitialState and everything will follow.

Going one step further

From here it goes beyond what you requested, feel free to ignore this part if you are not convinced.

As a good practice I would store these configurations only in config files to reduce even more the code duplication and gain in flexibility:

import java.util.List;

public enum InitialState implements LevelSettings {
    EMPTY {
        @Override
        public List<GameObject> startingObjects() {
            return readFromFile("empty.level");
        }
    },
    INTEGRATIONTEST {
        @Override
        public List<GameObject> startingObjects() {
            return readFromFile("integration_test.level");
        }
    },
    LEVEL_1 {
        @Override
        public List<GameObject> startingObjects() {
            return readFromFile("1.level");
        }
    },
    LEVEL_2 {
        @Override
        public List<GameObject> startingObjects() {
            return readFromFile("2.level");
        }
    };

    private static List<GameObject> readFromFile(String filename) {
        //Open file
        //Serialize its content in GameObjects
        //return them as a list
    }
}

So that when you decide to add a new level you actually only need to know the filename in which the level's configuration is stored.

Going another step further

What you will see there is really tricky and I don't advice you to use it in production code (but it reduces code duplication) !

import java.util.List;

public enum InitialState implements LevelSettings {
    EMPTY, INTEGRATIONTEST, LEVEL_1, LEVEL_2;

    @Override
    public List<GameObject> startingObjects() {
        return readFromFile(this.name() + ".level");
    }

    private static List<GameObject> readFromFile(String filename) {
        //Open file
        //Serialize its content in GameObjects
        //return them as a list
    }
}

Here we rely on enum names themselves to find the corresponding correct file. This code works because it is based on the convention that the files are named accordingly to the enum names with the ".level" extension. When you need to add a new level, just add it to the enum and that's it...

like image 167
Spotted Avatar answered Oct 13 '22 19:10

Spotted


well you can refactor all of the work into a one method. say it takes an int as the ID of the level, while it loads a JSON file containing structured information of each level, and creates the given level. for example :

"levels" : [
  "level" : {
      "id" : "001",
      "size" : "200",
      "difficulty" : "2"
  },
  "level" : {
      "id" : "002",
      "size" : "300",
      "difficulty" : "3"
  }
]

then, in your code:

public void setupLevel(int id) throws levelNotFoundException{
    //somehow like this
    Document doc = parse("levels.json");
    for(element elm: doc.get("levels")){
        if(Integer.parseInt(elm.get("id")).equals(id)){
            //setup your level
        }
    }
}

and then somewhere you call your method:

int levelId = getNextLevel();
try{
setupLevel(levelId);
} catch (LevelNotFoundException e){e.printStackTrace();}

or you can use XML, or simply hard code it, and store all levels in an array

like image 23
AminePaleo Avatar answered Oct 13 '22 20:10

AminePaleo