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 beside 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 backward).

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 - as 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

1
  • 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, 2018 at 1:31

3 Answers 3

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.

2
  • 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, 2018 at 17:53
  • 1
    @user45623: Yes, that's true. A generic class with multiple instances configured differently is a great way to start. Oct 8, 2018 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=[])
1
  • 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, 2018 at 18:13
0

Do it the other way around.

Instead of spinning the reels and checking for a win. Pick a random number and select the win from a table. Then "spin" the wheels to that result.

This allows you to control the exact probability of a win and payout according to your desired result/local gambling laws.

This also solves your "crazy variations" problem. Just add the various possible results to the table. Draw lots of animated gold coins/stars etc

7
  • I thought about this, but I don't think it would actually make things easier. Instead of writing logic to calculate our winnings based on random reel positions, we'd need equally complicated logic to arrange the wheel positions to match our random winnings. For example, let's say we decide to have the player spin 2 cherries. We would then need logic to make sure the next 3 reels didn't display cherries or wildcards or anything else that would be a better win than 2 cherries.
    – user45623
    Jul 6, 2023 at 20:15
  • Anyway, I'm no longer working on the project. As I had expected and repeatedly warned the client, the endless scope creep led to the client running out of funds and having to abandon the game before it was finished.
    – user45623
    Jul 6, 2023 at 20:17
  • your table defines the exact result of all wheels, when you "spin" you just show blurred spinning reels and transition to the looped result set which moves a bit before settling. Sure you still need to code up any display, like animated lines, or flashing cherries, but you can hardcode that as a result of the table rather than having to calculate on the fly
    – Ewan
    Jul 7, 2023 at 14:13
  • I think you've misunderstood the problem. The machines had gotten so complicated that it was getting incredibly difficult to generate a pay table. If I had a complete pay table, the problem would be solved. And picking a win from the pay table isn't as simple as you're assuming, if not all of the reels have winning icons on them.
    – user45623
    Jul 8, 2023 at 17:27
  • you dont generate it. manually fill in the desired possible spins and the payout and animation you want to go with them
    – Ewan
    Jul 9, 2023 at 12:21

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.