24
\$\begingroup\$

I wanted to create a save game system for my city building game that did not require the player to input a name for the city. It was also important to allow infinite save games, so I could not just use save slots to solve the problem. With a bit of help, I devised a system that would allow the player to optionally enter a name, and if they didn't the game would determine a default name based on the existing save names.

At first I was just counting all of the existing worlds and then adding that number to "world" to get the name. However, I was concerned about the edge case where someone would, for example, make 5 worlds, then delete "world4", and then the game would suggest the name "world5" again, and if the player wasn't paying attention, their save would be overwritten.

When the user chooses to start a new game, a TextField appears that is populated by the correct string, which would be "world" followed by the next unused number.

String startName = this.getFirstUnusuedWorldName(this.getNumWorlds());
this.worldNameField = new TextField(startName, this.skin);

Here is the getNumWorlds() method:

private int getNumWorlds() {
    FileHandle[] files = Gdx.files.local("worlds/").list();
    int numDirectories = 0;
    for (FileHandle handle : files) {
        if (handle.isDirectory()) {
            numDirectories++;
        }
    }
    return numDirectories;
}

And here is the getFirstUnusuedWorldName method:

private String getFirstUnusuedWorldName(int numWorlds) {
    FileHandle[] files = Gdx.files.local("worlds/").list();
    if (files.length == 0) {
        return "world1";
    }

    ArrayList<String> possibleNames = new ArrayList<String>();
    for (int i = 1; i <= numWorlds + 1; i++) { //world names start with 1, need to search 1 past length
        possibleNames.add("world" + String.valueOf(i));
    }
    for (String possibleName : possibleNames) {
        boolean containsName = false;
        for (FileHandle file : files) {
            if (file.name().equals(possibleName)) {
                containsName = true;
            }
        }
        if (!containsName) {
            return possibleName;
        }
    }

    return "world1";
}

I'm hoping there's a better way to do this.

\$\endgroup\$
27
\$\begingroup\$

You manage to do the right thing, but the way that you do it is a bit... what shall we say...? twisted? backwards? non-optimal!

Your approach:

  • Get list of all files in directory
  • Loop over a range and add the possible names to a list
  • Loop through the possible names and check if it matches an existing file

My comments to that:

  • Using the correct data structures would help greatly here. Adding the existing file names to a Set<String> will make the lookup time \$O(1)\$.
  • Adding the names to a list is just completely unnecessary as you loop through that list directly afterwards.
  • Looping through a specific range is also unnecessary. Start loop from 1 and just don't stop until you have found a free spot.
  • This will also make the special-case files.length == 0 unnecessary.

Here's what we can end up with:

private String getFirstUnusuedWorldName() {
    FileHandle[] files = Gdx.files.local("worlds/").list();
    Set<String> fileNames = new HashSet<String>();
    for (FileHandle handle : files) {
        fileNames.add(handle.getName());
    }

    for (int i = 1; ; i++) {
        String name = "world" + i;
        if (!fileNames.contains(name)) {
            return name;
        }
    }
}

Note the lack of a stopping condition in the for-loop (You don't need one as sooner or later, there will be an empty spot).

Also note that you no longer need the int numWorlds parameter.

\$\endgroup\$
4
\$\begingroup\$

Edited thanks to @Quill.

Assuming that the player doesn't care about the name allocated to the file, browsing the filetree and making exceptions for possible user interactions seems like a lot of work.

You appear to be searching for a unique string to add to the base world name. One is readily available in the form of a timestamp, for example world20150709132530 based on the base world name, year, month, hour, minute and second. I'm sorry not to be able to provide a code example as I'm not familiar with the language, but this should be readily available in the reference documents.

Possible dangers associated with this alternate method include moving to a different time zone, summer time or clock resyncs. In all cases, a problem occurs if the user saves in the 'same second' in the new timeframe as they did in the previous timeframe. Using UTC eliminates the first two errors, but adds a layer of confusion for the user if they look at the filenames from a country not currently in the UTC timezone.

\$\endgroup\$
  • \$\begingroup\$ The name is used to populate a TextField that is visible to the player, so it is better in my case not to use a strange number. Interesting approach though! \$\endgroup\$ – bazola Jul 9 '15 at 15:14
  • \$\begingroup\$ is world-2015-Jul-09-T13:25:30 better? using e.g. String filename = new SimpleDateFormat("'world'-yyyy-MMM-dd-'T'HH:mm:ss").format(new Date()); \$\endgroup\$ – pbeentje Jul 10 '15 at 8:04
  • 1
    \$\begingroup\$ @pbeentje I don't think there's a OS in the world that allows : in a filename. \$\endgroup\$ – Simon Forsberg Jul 13 '15 at 16:45
  • 1
    \$\begingroup\$ @SimonAndréForsberg Any OS following the filename rules of The Single Unix Specification allows any character except NUL and forward slash. This includes both OS X and Linux, which are the basis for iOS and Android respectively. See the Open Group Base Specifications. \$\endgroup\$ – jacwah Sep 2 '15 at 15:03

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.