Horse Race Winning Odds Simulator

J.C. SolvoTerra 1 Tallied Votes 883 Views Share

=======Download The Full Solution Below========

This will be my last VB.Net soure as I've made the move to C#. For a recent job application I was given the technical test to create a horse race simulator which would calculate the odds of a runner winning a race of up to 16 runners, then ensure the runners were within a 2% winning margin of there expected win percentage after 1,000,000 races. I got the results between 0.10% and 0.02% but never got offered the position as my solution was "Beyond Requirements".

HRS.png

This solution is host to ll sorts of goodies including multithreading, basic user controls etc... further more, if you bet on the horses this may be great for you.

Jay.

=======Download The Full Solution Below========

Imports System
Imports System.Text
Imports System.Text.RegularExpressions

Public Class bet365

#Region "Private Methods"

    ''' <summary>Count the number of syllables in a string</summary>
    Private Shared Function SyllableCount(StringValue As String) As Integer

        If Len(StringValue) = 0 Then Return 0

        Dim Syllables As New List(Of String)(New String() {"A", "E", "I", "O", "U"})
        Dim IntPos As Integer = 0

        SyllableCount = 0

        For StrPos As Integer = 1 To Len(StringValue)

            IntPos = StrPos

            If Not IsNothing(Syllables.Find(Function(Str As String) Str = Mid(StringValue, IntPos, 1))) Then
                SyllableCount += 1
            End If

        Next

    End Function

    ''' <summary>Determine if a string may contain offencieve words</summary>
    Private Shared Function BadWordCount(StringValue As String) As Integer

        'Alternatively there are a wide range of APIs which provide a full list of bad words
        'e.g. http://www.wdyl.com/profanity?q=shit
        'Response is in JSON {"response": "false"}
        'ALternatively these values could be obtained from a database

        If Len(StringValue) = 0 Then Return 0

        Dim BadWords As New List(Of String)(New String() {"S**T", "F**K", "A**E", "C**T", "C**K", "T**T", _
                                                          "T****R", "P**S", "X", "Y", "Z", "B******S", _
                                                          "B****X", "N", "A", " D**K ", "C***E", "DAVID CAMERON"}) 'Etc... Etc 
        BadWordCount = 0

        For Each BadWord As String In BadWords

            If UCase(StringValue).Contains(BadWord) Then BadWordCount += 1

        Next

    End Function

#End Region

#Region "Public Methods"

    ''' <summary>Perform convention naming checks on supplied runners name</summary>
    Public Shared Function ConfirmRunnerName(Name As String) As ExceptionEx

        Dim RunnerException As New ExceptionEx

        'No Name
        If Trim(Name) = "" Then
            RunnerException.ID = "1"
            RunnerException.Message = "A Name Hasn't Been Provided For The Runner."
            RunnerException.Proposal = "Provide A Name For The Runner."
            RunnerException.Parent = System.Reflection.MethodBase.GetCurrentMethod().Name
            Return RunnerException
        End If


        'Existing Registered Names
        'Return New Exception("Runner's name is already registered.")


        'Protected Names
        'Return New Exception("Runner's name is protected")


        'Contains <> A-Z or Space
        If Not Regex.IsMatch(Name, "^[A-Za-z ]*$", RegexOptions.IgnoreCase) Or Name.Contains("  ") Then
            RunnerException.ID = "2"
            RunnerException.Message = "The Runner's Name Contains Illegal Characters."
            RunnerException.Proposal = "Make Sure The Runner's Name Only Contains a-z, A-Z or Spaces"
            RunnerException.Parent = System.Reflection.MethodBase.GetCurrentMethod().Name
            Return RunnerException
        End If


        'More then 7 syllables
        If SyllableCount(Name) > My.Settings.Name_MaxSyllables Then
            RunnerException.ID = "3"
            RunnerException.Message = String.Format("Runner's name cannot exceed {0} syllables", My.Settings.Name_MaxSyllables)
            RunnerException.Proposal = "Remove Unnecassary Syllables"
            RunnerException.Parent = System.Reflection.MethodBase.GetCurrentMethod().Name
            Return RunnerException
        End If


        'Maximum Horse Name 18 Chars
        If Len(Name) > My.Settings.Name_MaxChars Then
            RunnerException.ID = "4"
            RunnerException.Message = String.Format("Runner's name cannot exceed {0} syllables", My.Settings.Name_MaxChars)
            RunnerException.Proposal = String.Format("Make Sure The Runner's Name Doesn't Exceed {0} Chartacters Including Spaces", My.Settings.Name_MaxChars)
            RunnerException.Parent = System.Reflection.MethodBase.GetCurrentMethod().Name
            Return RunnerException
        End If


        'Bad Word Filter + Religious + Political
        If BadWordCount(Name) > 0 Then
            RunnerException.ID = "5"
            RunnerException.Message = "Runner's name is considerd to be potentially offencieve."
            RunnerException.Proposal = "Remove Any Potentially Offencieve Words."
            RunnerException.Parent = System.Reflection.MethodBase.GetCurrentMethod().Name
            Return RunnerException
        End If


        Return Nothing

    End Function

    ''' <summary>Ensure Odds values are within limits</summary>
    Public Shared Function ConfirmOdds(Odds As Point) As ExceptionEx

        Dim RunnerException As New ExceptionEx

        If Odds.X < 1 Or Odds.Y < 1 Then
            RunnerException.ID = "1"
            RunnerException.Message = "The Odds Are Invalid."
            RunnerException.Proposal = "Make Sure The Odds Are Between 1/1 and 100/100"
            RunnerException.Parent = System.Reflection.MethodBase.GetCurrentMethod().Name
            Return RunnerException
        Else
            Return Nothing
        End If

    End Function

#End Region

#Region "Public Objects"

    '''<summary >Extended Exception Class</summary>
    Public Class ExceptionEx

        Public Property Exception As Exception
        Public Property ID As Integer
        Public Property Message As String
        Public Property Proposal As String
        Public Property Parent As String

    End Class

    '''<summary>'Used To Hold Data Regarding Multiple Races</summary> 
    Public Class RaceMonitor

        '''<summary>'The Target Number of Races</summary>
        Public Property Races As Integer

        '''<summary>Represents A Runner Being Monitored</summary>
        Public Class Runner

            '''<summary>The Name of The Runner</summary> 
            Public Property Name As String

            '''<summary>The Number of Wins This Runner has Accumulated</summary>
            Public Property Wins As Integer

        End Class


        '''<summary>The List of Runners Being Monitored</summary>
        Public Property Runners As List(Of Runner)

        '''<summary>The Actual Calculated Number of Races Ran</summary>
        Public ReadOnly Property RacesRan
            Get

                Return Runners.Sum(Function(W As Runner) W.Wins)

            End Get

        End Property

        '''<summary>Create a New Monitor Instance</summary>
        Public Sub New(ConfirmedRunners As List(Of bet365.Runner))

            If ConfirmedRunners.Count < 1 Then
                'TODO Error
                Exit Sub
            End If

            Runners = New List(Of Runner)

            'Add The Runners to The Monitor
            For Winner As Integer = 0 To ConfirmedRunners.Count - 1
                Runners.Add(New Runner With {.Name = ConfirmedRunners(Winner).Name, .Wins = 0})
            Next

        End Sub

    End Class

    ''' <summary>Holds data regarding a race's results</summary>
    Public Class RaceResults

        Private _Runner As Runner
        ''' <summary>The Winning Runner</summary>
        Public ReadOnly Property Runner As Runner
            Get
                Return _Runner
            End Get
        End Property

        ''' <summary>True if the race completed successfully</summary>
        Private _Success As Boolean
        Public ReadOnly Property Success As Boolean
            Get
                Return _Success
            End Get
        End Property

        ''' <summary>A message regarding the status of the race</summary>
        Private _Message As String
        Public ReadOnly Property Message As String
            Get
                Return _Message
            End Get
        End Property

        Public Sub New(Success As Boolean, Optional Winner As Runner = Nothing, Optional Message As String = Nothing)

            _Success = Success
            _Runner = Winner
            _Message = Message

        End Sub

    End Class

    ''' <summary>Holds preliminary information regarding a runner</summary>
    Public Class Runner

        Private _Name As String
        ''' <summary>The Runner's Name</summary>
        Public Property Name As String

            Get

                Return _Name

            End Get

            Set(value As String)

                'Has this name thrown an exception
                Dim NameException As ExceptionEx = ConfirmRunnerName(value)

                If IsNothing(NameException) Then

                    'If not update the name
                    _Name = value

                Else
                    'Display warning to user
                    Dim Msg As New StringBuilder

                    Msg.AppendLine("This Name Is Not Valid.")
                    Msg.AppendLine()
                    Msg.AppendLine("""" & value & """")
                    Msg.AppendLine()
                    Msg.AppendLine(NameException.Message)

                    MsgBox(Msg.ToString, MsgBoxStyle.OkOnly + vbInformation, "Unable To Set Runner's Name")

                End If

            End Set

        End Property

        Private _Odds As Point
        ''' <summary>The Runners Odds</summary>
        Public Property Odds As Point

            Get

                Return _Odds

            End Get

            Set(value As Point)

                'Did the odds return an exception
                Dim OddsException As ExceptionEx = ConfirmOdds(value)

                If IsNothing(OddsException) Then

                    'If not, set this runners odds
                    _Odds = value

                Else

                    'If an exception was returned, alter the user.
                    Dim Msg As New StringBuilder

                    Msg.AppendLine("These odds are not valid.")
                    Msg.AppendLine()
                    Msg.AppendLine("Odds " & value.X & "/" & value.Y)
                    Msg.AppendLine()
                    Msg.AppendLine(OddsException.Message)

                    MsgBox(Msg.ToString, MsgBoxStyle.OkOnly + vbInformation, "Unable To Set Odds.")

                End If

            End Set

        End Property

        ''' <summary>The Runner's % chance based on their odds</summary>
        Public ReadOnly Property Chance As String

            Get

                Return CDbl((CDbl(_Odds.Y) / CDbl(_Odds.X + _Odds.Y)) * 100).ToString("N2")

            End Get

        End Property

        ''' <summary>The Runner's name and odds values have been accepted</summary>
        Public ReadOnly Property Confirmed As Boolean
            Get
                'Return a bloolean representing the valid state of a runners name and odds values
                If IsNothing(ConfirmOdds(Odds)) And IsNothing(ConfirmRunnerName(Name)) Then
                    Return True
                Else
                    Return False
                End If

            End Get
        End Property

    End Class

    ''' <summary>A runner's additional information once entered into a race</summary>
    Public Class ConfirmedRunner

        ''' <summary>Represents an existing runner</summary>
        Public Property Runner As Runner

        ''' <summary>A runner's % chance of winning this race</summary>
        Public Property RaceChance As Double

        ''' <summary>A runners winning percent bracket</summary>
        Public Property WinningBracket As Double

        Public Sub New()
            Runner = New Runner
        End Sub

    End Class

    ''' <summary>The Primary Race Engine</summary>
    Public Class Race

        '''<summary>Initialize a New Instance of Race</summary>
        Public Sub New()

            _Runners = New List(Of Runner)
            CancelRaces = False

        End Sub

#Region "RACE Public Properties"

        Private _ConfirmedRunners As List(Of ConfirmedRunner)

        ''' <summary>The list of runners and their race specifc data</summary>
        Public ReadOnly Property ConfirmedRunners As List(Of ConfirmedRunner)
            Get
                Return _ConfirmedRunners
            End Get
        End Property

        Private _Runners As List(Of Runner)
        ''' <summary>The list of runners entered for a race</summary>
        Public ReadOnly Property Runners As List(Of Runner)
            Get
                Return _Runners
            End Get
        End Property

        ''' <summary>The race margin % based on the list of runner's odds</summary>
        Public ReadOnly Property RaceMargin As String
            Get

                'SUM the values of all the runner's chances 
                Dim Margin As Double = Runners.Sum(Function(Runner As Runner) Runner.Chance)

                Return Margin.ToString("N2")

            End Get
        End Property

        '''<summary>Set To True to Cancel Current Race Scenario</summary>
        Public Property CancelRaces As Boolean

#End Region

#Region "RACE Public Methods"

        ''' <summary>Use this method to add runners to ensure constraints are adhered too</summary>
        Public Function AddRunner(Runner As Runner) As ExceptionEx

            Dim RunnerException As New ExceptionEx

            'Is the runners odds and name values valid
            If Not Runner.Confirmed Then
                RunnerException.ID = "1"
                RunnerException.Message = "The Runners Odds or Name Values are Invalid."
                RunnerException.Proposal = "Check The Runners Odds and Name Values."
                RunnerException.Parent = System.Reflection.MethodBase.GetCurrentMethod().Name
                Return RunnerException
            End If

            'Does the runner's name already exist
            If Not (IsNothing(Runners.Find(Function(R As Runner) UCase(R.Name) = UCase(Runner.Name)))) Then
                RunnerException.ID = "2"
                RunnerException.Message = "The Runners Odds or Name Values are Invalid."
                RunnerException.Proposal = "Check The Runners Odds and Name Values."
                RunnerException.Parent = System.Reflection.MethodBase.GetCurrentMethod().Name
                Return RunnerException
            End If


            'Are their already 8 runners
            If Runners.Count >= My.Settings.Race_MaxRunners Then
                RunnerException.ID = "3"
                RunnerException.Message = "The Runners Odds or Name Values are Invalid."
                RunnerException.Proposal = "Check The Runners Odds and Name Values."
                RunnerException.Parent = System.Reflection.MethodBase.GetCurrentMethod().Name
                Return RunnerException
            End If


            'Add the runner to the race
            Try
                Runners.Add(Runner)
            Catch ex As Exception
                RunnerException.ID = "X"
                RunnerException.Message = "Unexpected Error."
                RunnerException.Proposal = "Check Exception."
                RunnerException.Parent = System.Reflection.MethodBase.GetCurrentMethod().Name
                RunnerException.Exception = ex
                Return RunnerException
            End Try

            Return Nothing

        End Function

        ''' <summary>Start a race</summary>
        Public Function BeginRace() As RaceResults

            '####################
            'Perform Final Checks
            '####################

            '4 Horse Minimum
            If Runners.Count < My.Settings.Race_MinRunners Then _
                Return New RaceResults(False, Nothing, String.Format("A minimum of {0} runners are required to begin a race.", _
                        My.Settings.Race_MinRunners))


            '8 Horse Maximum
            If Runners.Count > My.Settings.Race_MaxRunners Then _
                Return New RaceResults(False, Nothing, String.Format("A maximum of {0} runners are allowed per race.", _
                        My.Settings.Race_MaxRunners))


            'Check The Race Margin
            If RaceMargin < My.Settings.Race_MinMargin Or RaceMargin > My.Settings.Race_MaxMargin Then _
                Return New RaceResults(False, Nothing, String.Format("The race margin must be between {0}% and {1}%", _
                        My.Settings.Race_MinMargin, My.Settings.Race_MaxMargin))


            'Final Runner Checks
            If Not IsNothing(Runners.Find(Function(R As Runner) R.Confirmed = False)) Then _
                Return New RaceResults(False, Nothing, "Please Check Your Runners Details.")

            '##############
            'Begin The Race
            '##############

            'Begin The Race Returning Success and The Winning Runner
            Return New RaceResults(True, ExecuteRace, "Race Completed Succesfully.")



        End Function

#End Region

#Region "RACE Private Methods"

        ''' <summary>Determine a race's winner</summary>
        Private Function ExecuteRace() As Runner

            PrepareRunners()

            'Random Pointer
            Dim WinSeed As Double = 0

            'Random Pointer Location Percentage
            Dim SeedPercentage As Double = 0

            'Generate Random Point
            WinSeed = (Rnd() * 1000)

            'Get Percentage of Pointers Location
            SeedPercentage = (100 / 1000) * WinSeed

            'Use LINQ to determine which runners have a higher win bracket than the SeedPercentage
            Dim RunnerResults = From Runner In _ConfirmedRunners Where SeedPercentage <= Runner.WinningBracket

            'Return the runner whos winning percentage bracket encapsulated the random seed percentage
            'it will always be the runner returned first in the data list
            Return RunnerResults.First.Runner

        End Function

        ''' <summary>Provide additional information for each runner based on their position within a race line-up</summary>
        Private Sub PrepareRunners()

            'The Top Margin or Bracket To Be Considered For A Win
            Dim Bracket As Double = 0
            Dim ConfirmedRunner As ConfirmedRunner = Nothing

            'Create a new ConfirmedRunner List
            _ConfirmedRunners = New List(Of ConfirmedRunner)

            'Itterate through all the runners updating their race chance % based on 100% and
            'prepare their top winning brackets.
            For Each Runner As Runner In Runners

                ConfirmedRunner = New ConfirmedRunner

                'Copy The runner object into the ConfirmedRunner object
                ConfirmedRunner.Runner = Runner

                'Populate the Runners Race Win Chance %
                ConfirmedRunner.RaceChance = (Runner.Chance / CDbl(RaceMargin)) * 100

                'Generate the runners win bracket
                Bracket += ConfirmedRunner.RaceChance

                'Populate the runners win bracket for this race
                ConfirmedRunner.WinningBracket = Bracket

                'Add the confirmed runner to the confirmed runners list
                ConfirmedRunners.Add(ConfirmedRunner)

            Next

            'Ensure the final Entry is at peak bracket
            ConfirmedRunners(ConfirmedRunners.Count - 1).WinningBracket = 100

        End Sub

#End Region

    End Class

#End Region

End Class