1

I am building a slot machine game for a client (virtual money, not real-money gambling). They started out with a template game and hired me to reskin it and make "minor" changes for a "quick" release. Unfortunately, there are no brakes on the scope train.

The original template was very basic. Count the number of consecutive icons starting from the left, and check the value in the pay table. For example, if the player spun "7 7 Chery Cherry 7", that would count as two 7s, so we'd check the pay table for the value (if any) of getting two 7s. There were also some special cases in the evaluation code to handle special icons like Wild, Bonus, and Scatter.

Icon originally had three properties:

string name
Image image
IconType type (Normal, Wild, Bonus, or Scatter)

The pay table is a simple table with three columns

string name, int matches, int value

A single ProcessResult class handles the results for all machines; it determines what combination came up, and (based on the icon type) either looks up the value in the pay table, or applies a special effect if the combination consists of bonus or scatter icons.

However, each time they send me specifications for a new machine, the client adds new features that aren't compatible with the existing code. Now we have icons that don't need to be consecutive, icons that don't need to start on the left, icons that can combine with other types of icons, icons that modify the behavior of other icons, icons that modify the general behavior of the machine, etc etc.

For a while I was adding new properties to the Icon class (e.g. compatibleWithWilds, startsFromLeft, consecutiveOnly), but now it has a bunch of properties that don't apply to most icons, and the ProcessResult class is full of messy edge cases to handle all these properties. It's getting to be unmaintainable, and I think it's time for a refactor, but I'm not sure how to approach it.

I've identified several special cases that are particularly problematic:

  • Icons that don't need to be consecutive or start from the left greatly increase the complexity of determining which icon is considered the "result" for the spin
  • Icons that can match with other icons besides themselves are difficult to represent in the current pay table. For example, imagine a 3-reel slot where three cherries pays 5x, three bananas pays 3x, but a combination of cherries and bananas pays out 4x. My current solution is to give them a secondary name with a separate entry in the pay table (so e.g. the "cherry" icon counts as "cherry" or "fruit", and the banana icon counts as "banana" or "fruit", with "cherry" "banana" and "fruit" all listed separately in the pay table).
  • Some special icons usually have no inherent pay value (e.g. "Bonus", which triggers some type of minigame or alternate machine state) and thus we can't look up how many matches are required in the pay table. Right now the machine has generic properties such as "minBonusMatches", but this means that some machines have properties for icon types they don't actually use, and wouldn't work if the client introduced a second type of a special icon to some machine (e.g. "match 3 of the Bonus icons for a regular bonus or 5 of the Super Bonus icons for a super bonus")

I am not sure how to design the architecture in a way that will easily accommodate any crazy new rule that the client thinks up. I definitely don't want to keep building on the current tangle of nested if-elses that handles all the edge cases. I'm pretty sure I don't want to write a separate "ProcessResult" class for each machine, because I think that will be confusing and is likely to encourage code duplication. I've considered extending the Icon class with child classes that define their own logic, but I think that will get messy, and it feels like loading responsibility into the wrong place (the icons would then need to know all about the game state, which seems backwards).

Is there a particular pattern or architecture that is well-suited here? The best I have right now is a vague notion of using something like the component pattern to assign properties to icons, and feeding these through a chain-of-responsibility, but I'm having trouble thinking of a concrete implementation - like the old saying goes, can't see the forest for the edge cases.

The client ignores all advice, so any answer that involves giving advice to the client about scope creep or project management is pointless

  • If I've left out important details or assumed too much knowledge of slot machines, please point it out and I'll expand the question or elaborate in comments. – user45623 Oct 6 '18 at 1:31
5

You need to extract the logic of determining a win out into its own cluster of classes. It's no longer a dumb table.

First, think about the abstraction.

  • A number of icons are tested
  • A winning amount is assigned
  • When multiple winning combinations are found, 1 must take precedence (this highest winning amount)

So extract that into an interface:

public interface IWinningCombination
{
    decimal WinningAmount { get; }
    bool IsWin(Card a, Card b, Card c, Card d, Card e);
}

Then define your implementing classes:

public AllSevensWinningCombination : IWinningCombination
{
    public decimal WinningAmount => 100;

    public bool IsWin(Card a, Card b, Card c, Card d, Card e)
    {
        return a.Value == "7"
            && b.Value == "7"
            && c.Value == "7"
            && d.Value == "7"
            && e.Value == "7"
    }
}

And your slot machine just keeps a reference to all the winning combinations, and loops over them in order of winning amounts:

public class SlotMachine
{
    private IWinningCombination[] winningCombinations = new IWinningCombination[]
    {
        new AllSevensWinningCombination(),
        new CrazyEightsWinningCombination(),
        new AtLeastThreeSevensWinningCombination()
    }

    private IEnumerable<IWinningCombination> WinningCombinations => winningCombinations.OrderBy(w => w.WinningAmount);

    public SpinResult Spin()
    {
        SpinResult result = // ... random spin

        foreach (var winningCombination in WinningCombinations)
        {
            if (winningCombination.IsWin(result.A, result.B, result.C, result.D, result.E))
            {
                result.Win(winningCombination.WinningAmount);

                break;
            }
        }

        return result;
    }
}

By hiding the checking of the cards and calculation of the winning amounts behind an interface, you can basically go crazy with the rules. In fact, this might even be a simple Rules Engine, if you want another searchable buzz word.

  • Thanks, this has some good ideas. Writing a class for every possible winning combination for every machine like your AllSevensWinningCombination would be an enormous amount of work. However, for simple cases like that example I could use a reusable class, e.g. StandardWinningCombination allSevens = new StandardWinnningCombination(icon = "7", matches = 5, winningAmount = 100) and only write out unique classes for the weirder edge cases – user45623 Oct 8 '18 at 17:53
  • 1
    @user45623: Yes, that's true. A generic class with multiple instances configured differently is a great way to start. – Greg Burghardt Oct 8 '18 at 17:54
0

You'll want to structure the classes in this way:

  1. Detector classes: write these classes that can detect various potential winning combinations and add them to a WinEvents list.

  2. Transformer classes: these classes are likely going to be game specific, although many bonuses may be common among different games. The purpose of this class is to transform the detected WinEvents to detect higher level patterns, discard redundant WinEvents, and perform other transformations that can't be done in the initial detection

  3. CalculateWinnings classes: these classes looks at the transformed WinEvents and calculate winnings and other prizes that may be awarded to the player.

You put these classes together in this manner:

# these are parts of the game definition
detectors : List[Detector] = [...]
transformations : List[Transformer] = [...]
paytable : Dict[WinEvent.name, CalculateWinnings] = {...}

# these are the reels state
gamestate = {...}

events : List[WinEvent] = []
prizes : List[Prize] = []

for detector in detectors:
    events.extend(detector.detect(gamestate))

for transformer in transformations:
    gamestate, events = transformer.apply(gamestate, events)

# in more complex games you may need a second line of detectors and transformers, but this would be fairly rare

for event in events:
    prizes.append(paytable[event.name].calculate(gamestate, event))

Don't put too much behaviours in the Icon classes, they should be considered value classes, and it is the job of the detector and transformer to figure out if they need to be treated specially (e.g. as wildcards, scatter icons).

Some examples of detectors might be:

  • WinLineDetector(wildcard_symbols=[])
  • ScatterDetector(scatter_symbols=[])
  • Thanks for contributing an answer. I'm having a little more trouble following your answer because it is very abstract. Is this similar to a well-known pattern I could read more about? If not, could you expand with a concrete example? – user45623 Oct 8 '18 at 18:13

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.