18
\$\begingroup\$

What I'm trying to do is very straightforward and easy, but I am wondering whether there is a more elegant solution.

Basically I have a game and I want the difficulty to increase the moment the score gets between 100 - 500, then increase once again when the score is between 500 - 1500, then again when the score is between 1500 - 3000. In total, the difficulty should increase 3 times.

I need to check:

  1. Whether I am in one of those ranges.
  2. Whether I already increased the score when I got in that range.

Here's what I've got.

bool Increased100500 = false;
bool Increased5001500 = false;
bool Increased15003000 = false;

void IncreaseScore()
{
    if (Score >= 100 && Score <= 500 && !Increased100500)
    {
        Increase();
        Increased100500 = true;
    } 
    else if (Score >= 500 && Score < 1500 && !Increased5001500)
    {
        Increase();
        Increased5001500 = true;
    } 
    else if (Score >= 1500 && !Increased15003000)
    {
        Increase();
        Increased15003000 = true;
    } 
}

LE: I should have mentioned that the method above is called multiple times per frame.

\$\endgroup\$
2
  • 6
    \$\begingroup\$ Since it ill be called multiple time/ frames. I think it's best to create events. You can create an level Up event and call it inside the Increase score event. \$\endgroup\$
    – jean
    Commented Sep 3, 2014 at 21:07
  • 1
    \$\begingroup\$ @jean please avoid answering questions in comments. \$\endgroup\$
    – ANeves
    Commented Sep 4, 2014 at 10:03

10 Answers 10

19
\$\begingroup\$

Be careful with your inequalities. You've listed 500 twice: once as if (Score <= 500) and again as else if (Score >= 500). That's confusing, as the first condition takes precedence.

I think that you would be better off with one state variable representing the current difficulty level. The code would also be simpler if you determine NewDifficulty using a level-triggered rather than edge-triggered mechanism.

int Difficulty;

void IncreaseScore()
{
    int NewDifficulty = (Score <   100) ? 0
                      : (Score <=  500) ? 1
                      : (Score <= 1500) ? 2
                                        : 3;
    // One-way ratchet: difficulty can only increase, even if the
    // score subsequently drops.
    if (NewDifficulty < Difficulty)
    {
        Difficulty = NewDifficulty;
    }
}

If the score can never decrease in this game, then you can simplify it further:

void IncreaseScore()
{
    Difficulty = (Score <   100) ? 0
               : (Score <=  500) ? 1
               : (Score <= 1500) ? 2
                                 : 3;
}
\$\endgroup\$
10
  • 4
    \$\begingroup\$ If there are a fixed set of valid Difficultly values, using an enum would be better than int. This will ensure the difficulty doesn't mistakenly get set to -100. Also, this function may change the the difficulty, but it never increases the score. UpdateDifficultly() would be a more descriptive name. \$\endgroup\$ Commented Sep 3, 2014 at 21:31
  • \$\begingroup\$ Hm. That's much cleaner, but the last snippet doesn't work in my case. I still need to keep a 'NewDifficulty' or 'OldDifficulty' variable somewhere and increase the score only when NewDifficulty != OldDifficulty (i.e. only when my difficulty changes and only then). By the way, <= should have been < in the first if, correct. My typo. \$\endgroup\$
    – async
    Commented Sep 3, 2014 at 21:40
  • 1
    \$\begingroup\$ @unholysampler: An enum does not actually prevent any value from being entered. It makes it less likely, because there is a name, though. \$\endgroup\$
    – Magus
    Commented Sep 3, 2014 at 21:41
  • \$\begingroup\$ @Magus I'm too used to Java enums. You are right, in C#, it just makes it harder to do it wrong. \$\endgroup\$ Commented Sep 3, 2014 at 21:53
  • 1
    \$\begingroup\$ @Almo The ternary operator in PHP has a broken design. In all other sane languages, including C#, it chains reasonably well. \$\endgroup\$ Commented Sep 4, 2014 at 18:12
15
\$\begingroup\$

Just a quick note about naming:

bool Increased100500 = false;
bool Increased5001500 = false;
bool Increased15003000 = false;

Please, don't do that. I mean, yeah, private fields should be camelCase in the first place, but that chunk of numbers is fairly annoying, and it encodes your game logic into an identifier.

If you wanted to change your levels, let alone making them configurable, then what's 100500 going to tell the maintainer?

Consider using names like this instead:

var increasedLevel1 = false;
var increasedLevel2 = false;
var increasedLevel3 = false;

That's still encoding 3 difficulty levels into the identifiers, but at least the thresholds aren't part of the variables' names, so it's a start.

\$\endgroup\$
2
  • 7
    \$\begingroup\$ "If you wanted to change your levels, let alone making them configurable, then what's 100500 going to tell the maintainer?" Great point. +1 \$\endgroup\$
    – async
    Commented Sep 3, 2014 at 21:34
  • \$\begingroup\$ Or you could just keep a single variable denoting the last score at which you increased the difficulty. \$\endgroup\$
    – CompuChip
    Commented Sep 4, 2014 at 12:36
12
\$\begingroup\$

You don't talk about if the score can decrease or if it is possible for the score to jump over one of the difficulty ranges. If neither of these things are possible, the logic to set the difficulty can become very simple by using an enum.

enum Difficultly { Normal, Hard, Harder, Hardest }

class State
{
  private Difficultly _currentDifficultly = Difficultly.Normal;
  private int _score = 0;

  public void UpdateDifficulty()
  {
    switch(_currentDifficulty)
    {
      case Normal:
        if (_score  >= 100)
        {
          _currentDifficulty = Difficulty.Hard;
        }
        break;
      case Hard:
        if (_score  >= 500)
        {
          _currentDifficulty = Difficulty.Harder;
        }
        break;
      //...
    }
  }
}

However, as you can see, this gets very repetitive very fast. An alternative would be to construct a table that defines the data points.

Dictionary<Difficulty, Tuple<int, Difficulty>> _difficultyTransitions = new Dictionary<Difficulty, Tuple<int, Difficulty>>()
{
  { Difficulty.Normal, new Tuple<int, Difficulty>(100, Difficulty.Hard)},
  { Difficulty.Hard, new Tuple<int, Difficulty>(500, Difficulty.Harder)},
  { Difficulty.Harder, new Tuple<int, Difficulty>(1500, Difficulty.Hardest)},
  { Difficulty.Hardest, new Tuple<int, Difficulty>(int.MaxValue, Difficulty.Hardest)},
};

public void UpdateDifficulty()
{
  var transition = difficultyTransitions[_currentDifficulty];
  if (_score >= transition.Item1)
  {
    _currentDifficulty = transition.Item2;
  }
}
\$\endgroup\$
0
8
\$\begingroup\$

If Score cannot jump suddenly in the levels, for example after Score=50 it cannot jump to Score=550, then you could simplify by using half-ranges in the if statements, so instead of Score >= X && Score <= X + C you could use just Score >= X, like this:

void IncreaseScore()
{
    if (Score >= 100 && !Increased100500)
    {
        Increase();
        Increased100500 = true;
    } 
    else if (Score >= 500 && !Increased5001500)
    {
        Increase();
        Increased5001500 = true;
    } 
    else if (Score >= 1500 && !Increased15003000)
    {
        Increase();
        Increased15003000 = true;
    } 
}

But this is just treating the symptoms, not the real problem. It's not good that you need 3 state variables Increased100500, Increased5001500, and Increased15003000, and the hardcoded constants 100, 500, 1500 to keep track of this.

It would be better to refactor somehow without using those boolean variables at all. For example:

private const int[] thresholds = {100, 500, 1500};
private const HashSet<int> passedThresholds = new HashSet<int>();

void IncreaseScore()
{
    for (int threshold : thresholds)
    {
        if (Score >= threshold && !passedThresholds.Contains(threshold))
        {
            passedThresholds.add(threshold);
            Increase();
            break;
        }
    }
}

The behavior of this code is equivalent, and you don't need so many boolean variables to track the state. The increaseScore method doesn't need to know the exact values of the thresholds either, which is a good thing. And you can easily change the thresholds, simply in the declaration of thresholds, and without editing variable names.

\$\endgroup\$
5
  • \$\begingroup\$ I think I misread your answer. The purpose of those flags is to only increase the score once when in the specified ranges. If you don't have any Increased100500 (etc) flag, it will keep calling Increase() whenever you get in that particular branch. \$\endgroup\$
    – async
    Commented Sep 3, 2014 at 20:49
  • \$\begingroup\$ I want the score increased once when it's in [100, 500], then one more time in [500, 1500], then one last time in [1500, 3000]. IncreaseScore() is called multiple times per frame, by the way. I should have mentioned that. \$\endgroup\$
    – async
    Commented Sep 3, 2014 at 20:50
  • 2
    \$\begingroup\$ @user16547 I think maybe you should add some of the surrounding code for extra context. \$\endgroup\$
    – RubberDuck
    Commented Sep 3, 2014 at 20:53
  • \$\begingroup\$ @user16547 I updated my answer, though I'm still not 100% sure that's what you really wanted \$\endgroup\$
    – janos
    Commented Sep 3, 2014 at 20:59
  • \$\begingroup\$ To avoid recalling the increase dificult method just because you are still in a range you must call it inside your increase score method and know before and after score. You are responding to an event, not a state \$\endgroup\$
    – jean
    Commented Sep 3, 2014 at 21:10
6
\$\begingroup\$

I am not fond of the hardcoding since that will make it difficult to change your score thresholds later.This rules out Enum's as well. I liked Janos's second answer best, but that means storing a copy of the thresholds in a hash. In addition it will continue to loop through every threshold after the max difficulty is reached just to do nothing.

The following uses one list, handles level jumping, and once max difficulty is reached it short circuits as quickly as possible.

Initial variable declarations are only to illustrate type/expectation. Score, Difficulty, and the threshold list should be wrapped up nicely somewhere else in a static Singleton managing game state. And the thresholds need to come out of a configuration store somewhere so you don't need to recompile your game to change your mind later.

The key to this approach is that the difficulty is the index into the threshold list to determine the next increase. I did assume that you wouldn't be going backwards like the other solutions. Once difficulty level is out of bounds you know you have nothing left to check and are at max level.

int currentScore = 0; 
int currentDifficulty = 0;
List<int> difficultyThresholds = new List<int>() { 100, 500, 1500 };

void DifficultySlider()
{
   if (currentDifficulty >= difficultyThresholds.Count)
   {
      return;
   }

   while (currentScore >= difficultyThresholds[currentDifficulty])
   {
      currentDifficulty += 1;
      if (currentDifficulty >= difficultyThresholds.Count)
      {
         return;
      }
   }
}
\$\endgroup\$
6
\$\begingroup\$

A riff on @200_success answer

// value is the base-score for that level
public enum Difficulty {
    Beginner = 0,
    Easy     = 100,
    Medium   = 500,
    Hard     = 1500
}

void IncreaseScore() // yeah, needs a different name
                     // increasing difficulty, not the score
{
    Difficulty NewDifficulty = 
         (Score <  Difficulty.Easy)    ? Difficulty.Beginner
       : (Score <= Difficulty.Medium)  ? Difficulty.Easy
       : (Score <= Difficulty.Hard)    ? Difficulty.Medium
       :                                 Difficulty.Hard;

    // One-way ratchet: difficulty can only increase, even if the
    // score subsequently drops.
    if (NewDifficulty < CurrentDifficulty)
    {
        CurrentDifficulty = NewDifficulty;
    }
}
  • Defines levels for use everywhere.
  • Encapsulates the actual values associated w/ the levels
  • Useful throughout the application where ever the score-level needs to be used
  • Explicitly defines an initial state
  • Eliminates all magic numbers as seen in several other answers
  • No need for intermediate ints - which were essentially redefining the difficulty values.
  • A single point of change ( the enum ) when level values change
\$\endgroup\$
3
\$\begingroup\$

Checking in reverse order will catch any possible diffuculty jumps (ie from easy straight to insane) as well as any possible negative changes in score.

enum Difficulty { Easy = 0, Medium, Hard, Insane, Deathwish }
Difficulty currentDifficulty;

void UpdateDifficulty(int _score)
{
    if(_score >= 3000)
    {
        currentDifficulty = Difficulty.Deathwish;
    }
    else if(_score >= 1500)
    {
        currentDifficulty = Difficulty.Insane;
    }
    else if(_score >= 1000)
    {
        currentDifficulty = Difficulty.Hard;
    }
    else if(_score >= 500)
    {
        currentDifficulty = Difficulty.Medium;
    }
    else
    {
        currentDifficulty = Difficulty.Easy;
    }
}
\$\endgroup\$
3
\$\begingroup\$

Review

You have a handful of difficulty levels, so you can manage this with a couple of booleans:

bool Increased100500 = false;
bool Increased5001500 = false;
bool Increased15003000 = false;

Your Increase method is a black-box to us. Maybe it's linearly changing some difficulty parameters, or maybe it sets custom values to each of these parameters.

As the game gets bigger, gets more parameters that are sensitive to the difficulty level, gets more difficulty levels and gets such features as skipping levels, decreasing score and levels, it will be hard to maintain the code.

Proposed Changes

To allow the game to get more complex, I would opt to use a state machine pattern to handle difficulty levels. All actions concerning changing game parameters that influence the difficulty are handled by each specific state. This allows for an object-oriented design, extensibility, and drastically decreasing the amount of boolean flags in the game.

State

Each state has actions on Enter and Exit. A Difficulty level state also handles OnScoreUpdated events.

abstract class DifficultyLevelState
{
    public virtual void Enter(Game game) { }
    public virtual void Exit(Game game) { }

    protected virtual DifficultyLevelState EasyLevel() => new EasyState();
    protected virtual DifficultyLevelState NormalLevel() => new NormalState();
    protected virtual DifficultyLevelState HardLevel() => new HardState();
    protected virtual DifficultyLevelState ExpertLevel() => new ExpertState();

    public void OnScoreUpdated(Game game)
    {
        var score = game.Score;
        if (score < 100)
        {
            game.UpdateDifficultyLevel(EasyLevel());
        }
        else if (score < 500)
        {
            game.UpdateDifficultyLevel(NormalLevel());
        }
        else if (score < 1500)
        {
            game.UpdateDifficultyLevel(HardLevel());
        }
        else //if (score < 3000)
        {
            game.UpdateDifficultyLevel(ExpertLevel());
        }
    }
}

Each specific state implements a self-transition to avoid unnecessary state changes and actions on Enter to change the game's difficulty level. I haven't found a use for Exit, but I'll keep the method because the pattern provides it, and you might find a use case for it. Implement EasyState, NormalState, HardState and ExpertState.

class EasyState : DifficultyLevelState
{
    protected override DifficultyLevelState EasyLevel() => this;

    public override void Enter(Game game)
    {
        // Change game difficulty parameters:
        // game.SpeedFactor = 1;
        // game.CollisionFactor = 0.75;
    }
}

State Machine

Your Game probably provides a score somehow and has an entrypoint to Start the game:

class Game
{
    private int score;
    bool Increased100500 = false;
    bool Increased5001500 = false;
    bool Increased15003000 = false;

    public void Start()
    {
        Score = 0;
    }

    public int Score
    {
        get => score;
        protected set
        {
            if (score != value)
            {
                score = value;
                IncreaseScore(); // OP method
            }
        }
    }
}

Let's remove the IncreaseScore, the boolean flags and implement the state machine.

class Game
{
    private int score;
    private DifficultyLevelState difficultyLevel;

    public void Start()
    {
        Score = 0;
        UpdateDifficultyLevel(new EasyState());
    }

    public int Score
    {
        get => score;
        protected set
        {
            if (score != value)
            {
                score = value;
                OnScoreUpdated();
            }
        }
    }

    private void OnScoreUpdated()
    {
        difficultyLevel.OnScoreUpdated(this);
    }

    internal void UpdateDifficultyLevel(DifficultyLevelState state)
    {
        if (difficultyLevel != state)
        {
            difficultyLevel?.Exit(this);
            difficultyLevel = state;
            difficultyLevel?.Enter(this);
        }
    }
}

Now, any time the score gets updated, the game asks the current difficulty state to handle the score. The current state might decide to keep the state as is, or to change the state to another difficulty level based on the current score. Whenever the difficulty state changes, its Enter actions change the difficulty parameters of the game. This pattern also allows to skip states (big score leaps) and decrease difficulty levels.

\$\endgroup\$
2
  • \$\begingroup\$ OP writes that their code is called multiple times per frame. Under this circumestances I think it might not be such a good idea to spawn new objects here EasyLevel() => new EasyState(). Enter method seems to be pure so I guess it would be sufficient to create them levels only once. On the other hand difficulty levels probably don't change that often. Nevertheless, they also wouldn't have to be virtual since they map to themselves... or am I missing something? \$\endgroup\$
    – t3chb0t
    Commented Aug 4, 2019 at 11:34
  • \$\begingroup\$ Each state overrides the method that maps to itself, the other ones create new state instances. These would only be created on exceeded some score threshold, so not many instances would be made. But having a cache of predefined states is aso a good option. \$\endgroup\$
    – dfhwze
    Commented Aug 4, 2019 at 11:46
0
\$\begingroup\$

I don't use C# but took this as a chance to learn some of it... Tested it first on Progress 4GL and then made the C# version (quite ugly one).

My idea was to use modulo to find out if it's needed to increase the difficulty:

score % thresholds[x] < score

That way there's no need to hardcode the IF-clauses (only need to hardcode the thresholds).

Ideone link: http://ideone.com/JM9Nj1

using System;

public class Test
{
    public static void Main()
    {
        // your code goes here
        int[] thresholds = new int[4] {100, 500, 1000, 1500};
        int difficulty = 0;
        int score = 0;

        for (score = 0; score <= 3000; score++) {
            for (int x = 0; x < thresholds.Length; x++)
            {
                if (score % thresholds[x] < score && difficulty < x + 1) { 
                    difficulty = x + 1;
                    Console.WriteLine("Score: " + score + " Threshold: " + thresholds[x] + " Difficulty: " + difficulty + " Increased difficulty!");
                    break; 
                }
            }
        }
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ I don't think using modulo is helping here... I believe it can screw up the results. \$\endgroup\$ Commented Sep 4, 2014 at 10:08
0
\$\begingroup\$

Without even looking at the code, I think the biggest issue is that

the method above is called multiple times per frame.

That sounds crazy. Given that this is a game for (presumably) a human to play, is it actually perceptible for the difficulty level to change in such a timespan?

It seems much more sensible to move this logic to a less-frequently called context - something that runs once per second should be perfectly adequate.

\$\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.