FYI Lobstermania slot machine simulator

jpm9511

Member
Joined
Jul 15, 2020
Messages
5
Programming Experience
Beginner
If you're interested, I've just set up a github repository for my 1st project. You can find this console app by clicking here: Lobstermania simulator.

Example use cases
  1. As a learning tool to understand video slot machine probabilities, payouts, and algorithms.
  2. As a base engine for developing a graphical version of the game.
  3. As a base for developing other slot machine games by changing symbols, payouts, and game rules.
Enjoy!
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
1,831
Location
Chesapeake, VA
Programming Experience
10+
If part of your goals was to really to learn C#, then you should follow the C# / .NET Framework naming conventions.

 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
1,831
Location
Chesapeake, VA
Programming Experience
10+
Sorry, wrong link above... Here is the more general .NET Framework Guidelines, and the C# convention above is with regards to some C# specific things.

 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
1,831
Location
Chesapeake, VA
Programming Experience
10+
Why are you "stringly-typed"? Shouldn't you be strongly typed and use enums?
C#:
SYMBOLS123 = { "WS", "LM", "BU", "BO", "LH", "TU", "CL", "SG", "SF", "LO", "LT" };

// All 11 game symbols, Corresponds to: 
{ "Wild", "Lobstermania", "Buoy", "Boat", "Lighthouse", "Tuna", "Clam", "Seagull", "Starfish", "Bonus", "Scatter" }
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
1,831
Location
Chesapeake, VA
Programming Experience
10+
Yikes! Those goto's are making my eyes twitch... Must go start refactoring code...
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
1,831
Location
Chesapeake, VA
Programming Experience
10+
Goto[icode]'s are bad. I've refactored the code to not use [icode]goto's. Also using exceptions for flow control is the same as using exceptions as goto's. I've changed the code to use TryParse() instead.

Re-wrote the following bits of code:

from LM962.cs:
switch (sym)
{
:
    case "WS": // Wild
    :

    // Leading 3 wilds
    if ((line[1] == "WS") && (line[2] == "WS") && (line[3] == "LM") && (line[4] == "WS") && (line[4] != "LM"))
    {
        sym = "LM";
        count = 4;
        goto Done;
    }
    if ((line[1] == "WS") && (line[2] == "WS") && (line[3] != "LM") && (line[3] != "WS") && (line[4] != line[3]))
    {
        sym = "WS";
        count = 3;
    }

Done:
    break; // case "WS"
:
}
as
No more goto.:
switch (sym)
{
:
    case "WS": // Wild
    :

    // Leading 3 wilds
    if ((line[1] == "WS") && (line[2] == "WS") && (line[3] == "LM") && (line[4] == "WS") && (line[4] != "LM"))
    {
        sym = "LM";
        count = 4;
        break;
    }
    if ((line[1] == "WS") && (line[2] == "WS") && (line[3] != "LM") && (line[3] != "WS") && (line[4] != line[3]))
    {
        sym = "WS";
        count = 3;
    }

    break; // case "WS"
:
}
and

Program.cs:
public static void Main()
{
    mainMenu:
    Console.Clear();
    Console.WriteLine("PRESS:\n");
    Console.WriteLine("\t1 for Bulk Game Spins\n\t2 for Individual Games\n\t3 to quit\n");
    Console.Write("Your choice: ");
    string res = Console.ReadLine();
    int num;
    try
    {
        num = Convert.ToInt32(res);
    }
    catch (Exception)
    {
        Console.WriteLine("\n***ERROR: Please enter number 1, 2, or 3 !!!");
        Thread.Sleep(2000); // sleep for 2 seconds
        goto mainMenu;
    }
    switch(num)
    {
        case 1:
            long numSpins;
            int paylines; // number of paylines to play

            labelNumSpins:
            Console.Write("\nEnter the number of spins:  ");
            try
            {
                numSpins = Convert.ToInt64(Console.ReadLine()); // convert to a long
                if (numSpins <= 0)
                    throw new ArgumentOutOfRangeException();
            }
            catch (Exception)
            {
                Console.WriteLine("\n***ERROR: Please enter a positive number greater than 0 !!!");
                goto labelNumSpins;
            }

            labelActivePaylines:
            Console.Write("Enter the number of active paylines (1 through 15):  ");

            try
            {
                paylines = Convert.ToInt32(Console.ReadLine()); // convert to an int

                if (paylines < 1 || paylines > 15)
                    throw new ArgumentOutOfRangeException();
            }
            catch (Exception)
            {
                Console.WriteLine("\n***ERROR: Please enter a positive number between 1 and 15 !!!");
                goto labelActivePaylines;
            }

            Console.Clear();
            BulkGameSpins(numSpins,paylines);
            Console.WriteLine("Press any key to continue to Main Menu ...");
            Console.ReadKey();
            goto mainMenu;
        case 2:
            Console.Clear();
            IndividualGames();
            goto mainMenu;
        case 3:
            Environment.Exit(0); // planned exit
            break;
        default:
            goto mainMenu;
    }

} // End method Main

private static void BulkGameSpins(long numSpins, int numPayLines)
{
    LM962 game = new LM962()
    {
        activePaylines = numPayLines
        };

    DateTime start_t = DateTime.Now;

    Console.WriteLine("Progress Bar ({0:N0} spins)\n",numSpins);
    Console.WriteLine("0%       100%");
    Console.WriteLine("|--------|");


    if (numSpins <= 10)
    {
        Console.WriteLine("**********"); // 10 markers
        for (long i = 1; i <= numSpins; i++)
            game.Spin();
    }
    else
    {
        int marks = 1; // Number of printed marks
        long markerEvery = (long)Math.Ceiling((double)numSpins / (double)10); // print progression marker at every 1/10th of total spins.

        for (long i = 1; i <= numSpins; i++)
        {
            game.Spin();
            if ((i % markerEvery == 0))
            {
                Console.Write("*");
                marks++;
            }
        }

        for (int i = marks; i <= 10; i++)
            Console.Write("*");
    }

    Console.WriteLine();
    game.stats.DisplaySessionStats(numPayLines);

    DateTime end_t = DateTime.Now;
    TimeSpan runtime = end_t - start_t;
    Console.WriteLine("\nRun completed in {0:t}\n", runtime);

} // End method BulkGameSpins

private static void IndividualGames()
{
    LM962 game = new LM962
    {
        printGameboard = true,
        printPaylines = true
        };

    for (; ;) // ever
    {
        Console.Clear(); // clear the console screen
        Console.WriteLine("\nPlaying {0} active paylines.\n", game.activePaylines);

        game.Spin();
        game.stats.DisplayGameStats ();
        game.stats.ResetGameStats();

        Console.WriteLine("Press the P key to change the number of pay lines");
        Console.WriteLine("Press the Escape key to return to the Main Menu");
        Console.WriteLine("\nPress any other key to continue playing.");
        ConsoleKeyInfo cki = Console.ReadKey(true);

        if (cki.KeyChar == 'p')
        {
            getPayLines:
            Console.Write("\nEnter the new number of active paylines (1 through 15):  ");
            int paylines;
            try
            {
                paylines = Convert.ToInt32(Console.ReadLine()); // convert to an int

                if (paylines < 1 || paylines > 15)
                    throw new ArgumentOutOfRangeException();
            }
            catch (Exception)
            {
                Console.WriteLine("\n***ERROR: Please enter a positive number between 1 and 15 !!!");
                goto getPayLines;
            }

            game.activePaylines = paylines;

        } // end if cki.KeyChar == 'p'

        if (cki.Key == ConsoleKey.Escape) // quit when you hit the escape key
            break;
    }  // end for ever

} // End method IndividualGames
as
No more goto's. No more using exceptions for flow control.:
private static int GetIntValue(string prompt, int min, int max, Action onError = null)
{
    while (true)
    {
        Console.Write(prompt);
        string input = Console.ReadLine();
        if (int.TryParse(input, out int value) && min <= value && value <= max)
            return value;
        onError?.Invoke();
    }
}

private static long GetLongValue(string prompt, long min, long max, Action onError = null)
{
    while (true)
    {
        Console.Write(prompt);
        string input = Console.ReadLine();
        if (long.TryParse(input, out long value) && min <= value && value <= max)
            return value;
        onError?.Invoke();
    }
}

private static void RunBulkGameSpins()
{
    var numSpins = GetLongValue("Enter the number of spins: ",
                                min: 1, max: long.MaxValue,
                                () => Console.Error.WriteLine("***ERROR: Please enter a positive number greater than 0 !!!"));

    var paylines = GetIntValue("Enter the number of active paylines (1 through 15): ",
                               min: 1, max: 15,
                               () => Console.Error.WriteLine("***ERROR: Please enter a positive number between 1 and 15 !!!"));

    Console.Clear();
    BulkGameSpins(numSpins, paylines);
    Console.WriteLine("Press any key to continue to Main Menu ...");
    Console.ReadKey();
}

private static string BuildMenuString()
{
    var sb = new StringBuilder();
    sb.AppendLine("PRESS:");
    sb.AppendLine();
    sb.AppendLine("\t1 for Bulk Game Spins");
    sb.AppendLine("\t2 for Individual Games");
    sb.AppendLine("\t3 to quit");
    sb.AppendLine();
    sb.Append("Your choice: ");
    return sb.ToString();
}

public static void Main()
{
    var menu = BuildMenuString();
    int num;
    do
    {
        Console.Clear();

        num = GetIntValue(prompt: menu,
                          min: 1, max: 3,
                          onError: () =>
                          {
                              Console.WriteLine("***ERROR: Please enter number 1, 2, or 3 !!!");
                              Thread.Sleep(2000);
                              Console.Clear();
                          });
        switch (num)
        {
            case 1:
                RunBulkGameSpins();
                break;

            case 2:
                IndividualGames();
                break;
        }
    } while (num != 3);
}

private static void BulkGameSpins(long numSpins, int numPayLines)
{
    LM962 game = new LM962()
    {
        activePaylines = numPayLines
        };

    DateTime start_t = DateTime.Now;

    Console.WriteLine("Progress Bar ({0:N0} spins)\n",numSpins);
    Console.WriteLine("0%       100%");
    Console.WriteLine("|--------|");


    if (numSpins <= 10)
    {
        Console.WriteLine("**********"); // 10 markers
        for (long i = 1; i <= numSpins; i++)
            game.Spin();
    }
    else
    {
        int marks = 1; // Number of printed marks
        long markerEvery = (long)Math.Ceiling((double)numSpins / (double)10); // print progression marker at every 1/10th of total spins.

        for (long i = 1; i <= numSpins; i++)
        {
            game.Spin();
            if ((i % markerEvery == 0))
            {
                Console.Write("*");
                marks++;
            }
        }

        for (int i = marks; i <= 10; i++)
            Console.Write("*");
    }

    Console.WriteLine();
    game.stats.DisplaySessionStats(numPayLines);

    DateTime end_t = DateTime.Now;
    TimeSpan runtime = end_t - start_t;
    Console.WriteLine("\nRun completed in {0:t}\n", runtime);

} // End method BulkGameSpins

private static void IndividualGames()
{
    LM962 game = new LM962
    {
        printGameboard = true,
        printPaylines = true
        };

    while (true)
    {
        Console.Clear();
        Console.WriteLine("\nPlaying {0} active paylines.\n", game.activePaylines);

        game.Spin();
        game.stats.DisplayGameStats ();
        game.stats.ResetGameStats();

        Console.WriteLine("Press the P key to change the number of pay lines");
        Console.WriteLine("Press the Escape key to return to the Main Menu");
        Console.WriteLine("\nPress any other key to continue playing.");
        ConsoleKeyInfo cki = Console.ReadKey(true);

        if (cki.KeyChar == 'p' || cki.KeyChar == 'P')
        {
            game.activePaylines = GetIntValue("\nEnter the new number of active paylines (1 through 15): ",
                                              1, 15,
                                              () => Console.WriteLine("\n***ERROR: Please enter a positive number between 1 and 15 !!!"));
        }

        if (cki.Key == ConsoleKey.Escape)
            break;
    }
}
I've submitted a pull request.
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
1,831
Location
Chesapeake, VA
Programming Experience
10+
Why are you "stringly-typed"? Shouldn't you be strongly typed and use enums?
C#:
SYMBOLS123 = { "WS", "LM", "BU", "BO", "LH", "TU", "CL", "SG", "SF", "LO", "LT" };

// All 11 game symbols, Corresponds to:
{ "Wild", "Lobstermania", "Buoy", "Boat", "Lighthouse", "Tuna", "Clam", "Seagull", "Starfish", "Bonus", "Scatter" }
As a quick aside, switching from strings to enums gave me a 100% increase in speed. Likely because ints can be compared directly versus having to dereference strings to compare them.
 

jpm9511

Member
Joined
Jul 15, 2020
Messages
5
Programming Experience
Beginner
If part of your goals was to really to learn C#, then you should follow the C# / .NET Framework naming conventions.

Thank you for taking the time to look at my code and provide feedback. I appreciate it. I'll learn the naming conventions and apply them in this project as well as all future ones. Thanks for the link!
 

jpm9511

Member
Joined
Jul 15, 2020
Messages
5
Programming Experience
Beginner
As a quick aside, switching from strings to enums gave me a 100% increase in speed. Likely because ints can be compared directly versus having to dereference strings to compare them.
Great point! The 1st commit I have on the master branch is based on char data types. I switched to strings to make things easier for folks to understand. That 1st version also ran markedly faster (about 100% faster than the string version). I know the current version using strings spends the vast majority of its time doing string == comparisons from profiling it. I never even thought to use enums rather than char. That will satisfy my desire for easy to understand code as well as speed.
 

jpm9511

Member
Joined
Jul 15, 2020
Messages
5
Programming Experience
Beginner
Sorry, wrong link above... Here is the more general .NET Framework Guidelines, and the C# convention above is with regards to some C# specific things.

Git it. Thanks! I'll read both.
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
1,831
Location
Chesapeake, VA
Programming Experience
10+
I'm not seeing the claimed 96.2% payback that the classname LM962 seems to be representing itself as. Granted that I've only been running 10,000,000 spins and 1,000,000 spins instead of the all out 259,440,000 spins, but I was expecting returns to be at least be consistently in the high 90% instead of the mid to low 80% ranges.

I've not looked closely at the reel compositions and distributions. I'd only done spot checks on the reel lengths which looked consistent with the PAR sheets.
 

jpm9511

Member
Joined
Jul 15, 2020
Messages
5
Programming Experience
Beginner
I'm not seeing the claimed 96.2% payback that the classname LM962 seems to be representing itself as. Granted that I've only been running 10,000,000 spins and 1,000,000 spins instead of the all out 259,440,000 spins, but I was expecting returns to be at least be consistently in the high 90% instead of the mid to low 80% ranges.

I've not looked closely at the reel compositions and distributions. I'd only done spot checks on the reel lengths which looked consistent with the PAR sheets.
It sounds like maybe you are not setting the number of paylines to 1. You have to use1 payline, else the payback % drops successively with each additional payline used. Also, the par sheets show the payback % (and other stats like hit rate, etc.) for 1 line . You can absolutely verify the theoretical payback % by changing the program so that you are driving the function:
GetLinePayout(payLines); // will include any bonus win <<- (LM962:115)
to cycle through every possible combination of symbols (11x11x11x10x10) on your payline -- 11 symbols per reel in the 1st 3 reels, only 10 symbols in each of the remaining 2 reels . You would need to call GetScatterWin() to get any scatter wins (these are wins at the gameboard level -- not the line level. BTW - I've done this deterministic verification for hit rate and payback %,, etc. at or before my 1st commit .

Since this simulator is non-deterministic (randomly selects each slot from a reel on a payline), the more runs you do on 1 payline will yield closer approximations to the par sheets theoretical 96.2% payback.

Program.cs : 9
// IMPORTANT NUMBERS
// -----------------
// Theoretical spins per JACKPOT 8,107,500
// Number of all slot combinations (47x46x48x50x50 -- 1 slot per reel) = 259,440,000
// Number of all possible symbol combinations on a line (11x11x11x10x10) = 133,100



I've included my screenshots of 10M spins, paylines = 1, 2 different times, and paylines = 15, 2 different times.

1595043205787.png



1595043736307.png



1595043933090.png


1595044621472.png
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
1,831
Location
Chesapeake, VA
Programming Experience
10+
That's what it was, the number of paylines.
 
Top Bottom