Chapter 4. Rule-Based Systems

Rule-based systems attempt to use the best qualities of hard-coded AI without their disadvantages—and without constraining the designer to partition the problem into the independent states of an FSM. Rule-based systems provide a formal way to store expert knowledge and use it appropriately. (As in the case of FSM, game AI programmers may use terminology less exactly than researchers.) Regardless of how they are coded, rules can yield a very entertaining game AI; for example, the chase mode AI for the ghosts in Pac-Man can be written as four simple rules [Pittman09].

This chapter looks at what rule-based systems are and considers their advantages and disadvantages. To illustrate them, this chapter features a project that implements a rule-based system that plays Minesweeper. This project should be the most enjoyable project so far. Not only will it provide you with a playable game including AI assistance, but the presentation is in a “build a little, test a little” style that has more frequent rewards along the way.

What Is a Rule-Based AI?

The basic idea behind a rule-based AI is very similar to a method school teachers use with young children. The teacher presents the students with a question. Some of the children raise their hand. Each student is asked to present his or her idea as to the answer, and the teacher picks the best of them. The teacher can pick one or many of the ideas, or possibly have the children work on all the ideas in parallel—or at least all of them that do not place conflicting demands on classroom resources.

Rule-based systems consist of a collection of rules and the framework to apply them. When the AI is asked to think, the current state of the world is presented to the rules. Each rule determines if it matches the current situation. From among the matching rules, the framework activates one or more of them.

Besides being familiar from classroom experience, you may recognize that the transition checking done by the states in the FSM project of Chapter 3, “Finite State Machines (FSMs),” fits the definition of a rule-based system. The transitions out of a state are checked for a match; the system picks one of the matching transitions and changes the FSM state. Just as in an FSM, where multiple transitions may be valid, there may be multiple rules that match in a rule-based system. Unlike an FSM, however, a rule-based system need not be limited to activating only a single rule. Activating multiple rules forces the programmer to deal with the issues of conflict and efficiency. The activated rules must not conflict with each other; “go left” and “go right” may both be valid responses, but one precludes the other. Evaluating all the rules ensures that the best response can be used, but it has a direct impact on efficiency. Conflict resolution and efficiency concerns suggest that the rules be prioritized in some way, the simplest method being that the first rule to match is the only rule to be activated.

Let us examine what rules are. Done properly, rules are a collection of really good ideas about what to do, combined with the knowledge about when to use them. This means that there are two parts to each rule: the matching part and the execution part. For game AI, intelligence comes from sufficient rules with good execution, and stupidity comes from bad matching or too few rules.

Rules have to determine whether they match the current situation. Recall our simulated opera singer who confused a funeral gathering with a small theater and broke into song. His problem is not in the execution part; we made no comment on his singing skills, and no one at the funeral really cared. His problem is in the matching part. The AI could be missing a rule specifically for funerals; the near match of the funeral situation with a theater situation meant that it was the best-matching rule in the AI, so it was the rule used. Alternatively, if the AI had a rule for funerals, and somehow the rule for theater was selected instead, then the matching part of the funeral rule might be miscoded or poorly prioritized. That is, the framework might not have disambiguated the matched rules properly, or the design of the matching system might not be adequate.

In general, highly specialized rules need to match very strongly or not at all. More general rules need to match often, but not as strongly. Conversationally, this is the difference between asking a person about his or her new baby compared to commenting on the weather. People who actually have a new baby tend to react quite positively when asked about it. People who do not have a new baby tend to react equally poorly when asked about one. Commenting on the weather is a generally safe, if uninspired, conversational gambit.

The execution side of the rules is where the expertise in expert systems really shines. A sports AI that properly recognizes a zone defense needs an effective counter tactic; what’s more, it needs to be able to execute that counter tactic. As another example, there are many people who can correctly distinguish between heartburn and heart attack; among those people, a trained cardiologist is more likely to have superior execution in the case of heart attack.

Any existing algorithms make the game AI programmer’s job easier when developing the execution part of a rule. The AI is free to exhibit machine-like precision using optimal algorithms if they exist. More abstract games tend to have such algorithms, while more real-world simulations force the AI programmer to do the best that he or she can with other methods. When the AI is very effective, the AI programmer is required to mediate the conflict between an AI that is stupid and boring and one that is too smart and frustrating.

The rules execute in a framework. One of the design decisions that AI programmers need to consider is whether the framework will allow more than one rule to execute at a time. For many systems, executing one rule at a time is sufficient (or perhaps required). However, concurrent rule execution is a neat trick that enhances the richness of the AI. Systems that allow concurrent rule execution need to provide a mechanism to ensure that the demands of all the rules selected for execution can be met. There are many ways to do this. One algorithm repeatedly adds the rule with the best match to the rules selected to run, provided that the rule to be added does not conflict with any of the rules already selected. However it operates, the framework in a rule-based system selects a rule or rules, including breaking ties and conflicts among them.

Design and Analysis

Besides being a programming method to implement AI, rule-based systems require the programmer to think about the AI in a particular way. As with FSMs, this forces programmers to crystallize what they expect the AI to actually do.

Rule-based systems lend themselves to a somewhat Darwinian approach to coding the AI. Game AI programmers add their best rules to the system and test it. Rules that never fire are considered suspect. They address inadequacies in the AI by adding new rules or improving existing ones. Then they balance the rules in the framework with careful tuning.

How many rules are required? This will depend on the game and the desired quality. To play Sudoku, two rules will solve any board that has a difficulty level below “evil.” To play Minesweeper, three rules suffice to play every move of a beginner- or intermediate-level board and nearly every move of an expert-level board [Kirby08]. Yet at one point in time, the SOAR Quakebot had more than 300 rules [Laird].

How complicated will the framework need to be? Simpler games do not allow concurrent rule execution; the AI is expected to pick a single move or do one thing during its turn. Concurrent rule execution is too complex for most beginning game AI programmers doing their first rule-based AI. But without concurrent rule execution, the framework still needs a method to select a rule when more than one rule matches. The rules need a method to report how well they match, and the framework needs a method to select among them. This can be as simple as comparing integers using a fixed algorithm, or it can be very complex. A glance back at the section “Multiple-Transition Review” in Chapter 3 might be in order. Do not make your methods complex until tuning demands it.

Advantages

There are many advantages to rule-based game AI. The rule structure helps contain complexity without the straightjacket of a state-based approach. When the rules are based on human play, the AI plays like a human. When the rules loaded into a rule-based system come from a high degree of expertise, the system usually exhibits that same level of expertise. Simple rule-based systems are within the reach of a beginning AI programmer. The execution part of a rule can be as complex as required to do the job. There are no constraints to get in the way.

Disadvantages

Rule-based systems also have disadvantages. It is very hard to have a good rule for every situation. The method places strong demands on some general rules to cover default behaviors. Writing rules takes human expertise at the task at hand. This is true of most beginning AI techniques. There is no inherent structure to the execution part of a rule. This is the flip side of an advantage; great freedom includes the freedom to create a nightmare. The temptation to drown the system with a rich set of rules must be balanced against the additional cost required to evaluate each new rule.

The Minesweeper Project

Our project is based on the ubiquitous Minesweeper game. We will implement both the game and the AI to help play it. The game requires more code than the AI, but the game code is generally less complex than the AI code. Not surprisingly, the basic game code will need additions to accommodate the AI. The AI will make moves through the same code pipeline that implements player moves. The added code allows the AI to sense the world. The AI commonly asks the world what squares neighbor a given square. The AI is also interested in the number of mines that need to be placed around a square and the number of unmarked squares surrounding a given square.

In our project, each rule will report the number of moves it can make. This customizes the general idea of each rule, reporting how well it fits the situation. The emphasis here is on how much gain each rule proposes to deliver. The rule with the highest number of proposed moves will be executed. Our project will also order the rules by cost. The costs are fixed and roughly based on the complexity of the rule’s matching algorithm. The framework breaks ties by using the lowest-cost rule with the highest proposed gain. Since the lowest-cost rules are checked first, the first rule with the best score can be the rule used.

Implementing the Basic Game

The game itself will have three main components: squares, a playing field to hold them, and a control panel. In addition, we will need an AI to help play it. The AI will assist the human player, helping to spot moves and making them when requested. A fully automatic AI is left as an exercise. We will again use a spiral model of development. We start with the basics of the parts and add sophistication each time we go around rather than writing each part completely before going on to the next part.

The Playing Field

The most basic part of the game is the playing field. The minefield of an expert level game spans 30 columns and 16 rows. Our tiles will be 30-pixel squares, so we will need the minefield to be more than 900 pixels wide and 480 pixels tall. We will put the control panel below the mines, so the form will have to be even taller.

Launch Visual Basic and create a project called Mines.

  1. Change the name of Forml to PlayingField.vb and its Text property to Mines.

  2. Resize the form to around 920 by at least 650 pixels. Final sizing will depend on your Windows settings and can be adjusted later. If you have room to make it taller, it is a good idea to do that now.

  3. Drag a Panel control to the form from the Toolbox. This will be our control panel. Change its Location property to 0,490 so that it will be below the minefield on the form.

  4. Resize the panel so that it takes up all of the bottom of the form.

  5. Change the BackColor property to White.

  6. Open the File menu and choose Save All and choose an appropriate location on your system.

Your screen should resemble Figure 4.1. The actual proportions will vary according to the resolution of your monitor.

Basic layout of the playing field.

Figure 4.1. Basic layout of the playing field.

This gives us rudimentary versions of two of our three main components of the game.

Squares

We will base our squares on the Button control. We will create a class called Square, and it can inherit from the Windows Button control. Playing Minesweeper involves a lot of clicking and right-clicking, Button controls have all the events we would need to handle the user input and display the results. Our Square class will extend the Button class and add the custom data and code we need.

So far, we have dragged all the controls that we have placed on the forms from the Toolbox, but that’s not the only way to get them there. Here we will create Square objects and place them on the form using code. We will write the Square class and add just enough code to test that we can get Square objects onto the form.

Click the File menu and add a class to the project. Name it Square.vb. We need to make the Square class inherit from Button and we need to control what a Square object looks like when it is created. We also need to have them make sure that they are ready to act like buttons. Our Square objects need to know what row and column they are at so that they can identify themselves. Add the following code to the class:

     Inherits System.Windows.Forms.Button
     Dim Row, Col As Integer
     Public Sub New(ByVal R As Integer, ByVal C As Integer)
         MyBase.New()
         'Get a different font and color for the button
         Me.Font = New System.Drawing.Font("Arial", 9, FontStyle.Bold)
         Me.BackColor = Color.White

         Row = R
         Col = C
         Height = 30
         Width = 30
         Text = ""
         FlatStyle = Windows.Forms.FlatStyle.Standard
     End Sub

Our Square class inherits from Button. The MyBase keyword is how our class refers to the class from which it inherits. To make sure that our class acts like a button, we want the initialization code that buttons use to run when our control initializes—hence the call to MyBase.New().

After doing something new, it is a good idea to test it. We will need a few more controls on the form to do that.

  1. Switch to the Design view of PlayingField:

  2. Drag a Button control from the Toolbox onto the top left of the white panel. The panel is a container, and we want the control to go inside it. That way, we can move it by moving the container if we need to resize the underlying form. If you drop the Button control onto the form, the form will be its container.

  3. Change the Button control’s Text property to Expert and the Name property to ExpertButton.

  4. After you change its properties, double-click the Button control to view the Click event handler. Add the following line of code:

         Call NewGame(16, 30, 99)
    

Adding Squares to the Playing Field

The NewGame code does not exist yet, so you will see the name flagged in the code and an error in the error list. If you have played a lot of Minesweeper, you know that an expert level board has 16 rows and 30 columns and conceals 99 mines. We want to be able to walk through the tiles and find their neighbors easily, so we will do more than just put the controls on the form; we will hold them in an array as well. Add the following code to PlayingField.vb:

     Public Field(,) As Square

     Dim NumRows As Integer
     Dim NumCols As Integer
     Dim NumMines As Integer

     Private Sub NewGame(ByVal nRows As Integer, ByVal nCols As Integer, _
             ByVal nMines As Integer)
         Dim Sq As Square
         'Put up an hourglass
         Me.Cursor = Cursors.WaitCursor
    'If we have an existing game, get rid of it
         'Do we have a field?
         If Field IsNot Nothing Then
             For Each Sq In Field
                 'If it exists, take it off the form
                 If Sq IsNot Nothing Then
                     Sq.Parent = Nothing
                 End If
             Next
         End If

         'Copy the passed-in parameters to the globals
         NumRows = nRows
         NumCols = nCols
         'Do some error checking
         Dim sqcnt As Integer = NumRows * NumCols
         If nMines > sqcnt Then
             nMines = sqcnt - 1
         End If
         'Then do the last assignment
         NumMines = nMines

         'Create the tiles for the new game
         'VB uses zero-based arrays
         ReDim Field(NumRows - 1, NumCols - 1)
         Dim row, col As Integer
         For col = 0 To NumCols - 1
             For row = 0 To NumRows - 1
                 'Create an actual object
                 Sq = New Square(row, col)
                 'Set the location
                 Sq.Top = row * Sq.Height
                 Sq.Left = col * Sq.Width
                 'Put it on the form
                 Sq.Parent = Me
                 'Store it in the array as well for easy access later
                 Field(row, col) = Sq
             Next
         Next
         'Back to regular cursor
         Me.Cursor = Cursors.Default
     End Sub

Open the File menu and choose Save All. Then start the project in the debugger and click the Expert button. After a bit of thinking, the form will paint all the Button controls. If you click Expert again, the form will remove them and paint new ones. Remember that you have to stop debugging before you can edit the code. Your application should resemble Figure 4.2.

A field of blank tiles.

Figure 4.2. A field of blank tiles.

Note that locations are in terms of top and left, and the values grow as you progress down the form and to the right. Programmers not used to the way Windows does things will need to remember that the Y axis is inverted.

Turning Plain Squares into Mines

At present, these are just clickable buttons, not a minefield. One of the benefits of object-oriented programming is that objects can conceal their inner data. We will design the Square class so that the only way to find out if a tile is truly a mine is to click it. This will ensure that our AI does not cheat. We will, of course, need a way to tell the tile if it is a mine or a safe square. We also need a way for safe squares to know how many mines are adjacent to them. Our code will not let the safe squares ask their neighbors if they are mines or not, however, so the mine squares will need to tell their neighbors to increment their count of nearby mines anonymously.

Before we load the Square objects with their ominous data, we have to wait for the user to click the first tile. In Minesweeper, the first click is always safe. We placed error checking in the NewGame code to make sure that at least one square was open for this very reason. This means that our Square objects have three possible states: They can be mines, they can be safe, or they can be waiting for the first click. They have to exist on the form in order for the user to make the first click, but they cannot have mines loaded until after that first click.

So what we will do next is modify the squares to have three possible states and to start in the uninitialized state. They will have to detect the first click and ask the playing field to tell them all what they contain. The squares will need to tell their neighbors to increment their mine counts. We will use the concept of neighbors a great deal, so the playing field needs some helper functions to create lists of neighbors. Finally, we should test that all of this code works. To test, we will add code that we will later turn into comments.

We start with the Square objects. Under the Inherits line in the Square class, add the following code:

     Public Enum HiddenValue
         Uninitialized
         Safe
         Mine
     End Enum
     'Hold the definitions of the button text in one place
     Public Const ShowMine As String = "@"
     Public Const ShowFlag As String = "F"
     Public Const ShowBrokenFlag As String = "X"

     'What does this Square object actually hold?
     Private contents As HiddenValue
     'How many mines are near us?
     Private actualNearMines As Integer

The contents variable holds the secret value of the square. An Enum is a way of creating an enumerated list of independent values. We do not really care what the values are, we just need for all of them to be different, and we need to be able to tell them apart. A variable of an enumerated type is restricted to hold only values from the enumeration. We want our Square objects to be created with their contents equal to Uninitialized, so we add the following line to the New() routine.

     contents = HiddenValue.Uninitialized

VB initializes integer values to zero, so we do not have to explicitly set actual-NearMines to zero.

While we are working with the Square object, we should create the routine that lets the playing field initialize it. This routine will store the hidden value and tell the neighbors to increment their count of the mines near them. Add the following code to the Square class:

     'Load the square with its hidden value
     Public Sub Init(ByVal HV As HiddenValue, ByVal Neighbors As Collection)
         contents = HV

         'If that was a mine, the surrounding counts need to go up
         If contents = HiddenValue.Mine Then
             'Let the neighbors know
             Dim Sq As Square
             For Each Sq In Neighbors
                 Call Sq.IncrementMineCount()
             Next
         End If

         'Debugging code to comment out later
         If contents = HiddenValue.Mine Then
             'Use @ to mark a mine
             Me.Text = ShowMine
         End If
         'End debugging code
     End Sub

We are calling IncrementMineCount, but we have not written it yet, so it will be marked as an error for now. We have included debugging code so that as soon as possible, we can fire up the application and make sure that what we have so far works. As soon as it does, we will comment out the code because it gives away secret data, but we will leave it in case we need it later. We need to add the IncrementMineCount routine to the class:

     'Some unknown neighbor is telling us that they have a mine
     Public Sub IncrementMineCount()
         'Add one to the existing count
         actualNearMines += 1

         'Debugging code to comment out later
         'If I am not a mine, show my count
         If contents <> HiddenValue.Mine Then
             Me.Text = actualNearMines.ToString
         End If
         'End debugging code
     End Sub

There is an easy way to comment out a block of lines: Highlight the lines you want to make into comments and then hover your mouse over the buttons in the toolbar below the Data and Tools main menu. You are looking for the ones with horizontal black lines and blue lines. The tooltip will indicate which button comments out the lines and which button uncomments the lines. Try them out and watch what they do to your code. Commenting a comment line adds another leading’ character to any that are already there. That way, when you uncomment a block that includes comment lines, the comment lines stay comments.

Our Square objects are ready to be initialized by the form, but they do not yet ask the form to do so. Click the left drop-down list at the top of the Editing pane. This one probably has a current value of Square in bold text. Select (Square Events), which is marked with a lightning bolt. From the right drop-down list, select the Click event; VB will give you the skeleton of the event handler. Add code to the event handler so that it looks like the following:

    Private Sub Square_Click(ByVal sender As Object, ByVal e As System.EventArgs)
Handles Me.Click
        'We should be part of a playing field
        Dim theField As PlayingField = Me.Parent

        'If not, we can't ask it anything
        If theField Is Nothing Then Exit Sub

        'If this square is uninitialized, all of them are
        If contents = HiddenValue.Uninitialized Then
            'Have the playing field object init all the squares
            Call theField.InitializeSquares(Row, Col)
        End If

        'Make the button look pressed
        FlatStyle = Windows.Forms.FlatStyle.Flat
        Me.BackColor = Color.LightGray

        'Below here is where the player finds out if it is safe or not

    End Sub

This block of code introduces a few notational shortcuts. The first is that you can assign a value to a variable on the same line that you declare the variable. The next shortcut is for when you have a single line as the object of an If statement; you can just put the line after the Then keyword. When you do this, there is no need for an End If. (If you are new to VB, use this construct sparingly. It is not advisable to use it inside a nested If statement. The compiler could care less, but the programmer might get confused.) One of the very nice features of VB is that it takes care of indenting nested constructs for you. If you think that you have messed up the indentation, highlight the entire routine (or even the entire file) and press the Tab key. VB will line everything up based on where the compiler places the levels.

At this point there is one error. We have called upon the form to initialize the squares, but that code does not yet exist. The code for this has to walk the field and randomly place mines. Squares that get a mine need to know who their neighbors are. Do not let the length of the code fool you into thinking that it is complicated. Add this routine to the PlayingField class:

    'After the first click, place the mines
    Public Sub InitializeSquares(ByVal ClickedRow As Integer, ByVal ClickedCol
As Integer)
        'There is a lot of code that goes here.
        'We will add it in stages.

        'We have to track how many mines are yet to be placed
        Dim minesLeft As Integer = NumMines
        'We track the number of squares to go (one has been clicked)
        Dim squaresleft As Integer = (NumRows * NumCols) - 1
        'Percent and random numbers are floating point
        Dim perCent, roll As Single

        'Reseed the random number generator
        Call Randomize()

        'Our working variables
        Dim Row, Col As Integer
        Dim Neighbors As Collection

        'Walk the grid
        For Row = 0 To NumRows - 1
            For Col = 0 To NumCols - 1
            If Row <> ClickedRow Or Col <> ClickedCol Then
                'What percent of the squares need mines?
                'Has to be converted to a single precision float
                perCent = CSng(minesLeft / squaresleft)
                'Roll the dice, get a number from 0 to almost 1
                roll = Rnd()
                'If we roll less than the percent, we place a mine
                'Also, we ensure that we place them all
                If (roll < perCent) Or (minesLeft >= squaresleft) Then
                   'It has a mine!
                   'Call init on the square - we need the neighbors
                   Neighbors = NearNeighbors(Row, Col)
                   Field(Row, Col).Init(Square.HiddenValue.Mine, _
                       Neighbors)
                   'We placed a mine, so dec the count remaining
                   minesLeft -= 1
                Else
                   'It is safe - don't bother the neighbors
                   Neighbors = New Collection
                   Field(Row, Col).Init(Square.HiddenValue.Safe, _
                       Neighbors)
                End If
                'We either place a mine or not, but we have one less
                'Square in the computations
                squaresleft -= 1
            Else
                'It is the initial tile, therefore safe
                Neighbors = New Collection
                Field(Row, Col).Init(Square.HiddenValue.Safe, Neighbors)
            End If
        Next Col
    Next Row

    'Error checking:   All mines should be placed by now
    If minesLeft > 0 Then
        MsgBox(minesLeft.ToString & " Mines leftover!", _
              MsgBoxStyle.OkOnly)
    End If
End Sub

The new and interesting parts of the code include our first brush with the random number generator and the underscore (_) continuation character. Random number generators are not really random. The call to Randomize uses the system clock to “seed” the random number generator with a reasonably unpredictable value. This means that each game should look different. The Rnd calls return a floating-point number that is greater than or equal to zero and less than one.

The error check at the end should never trip; the code does its best to force all the mines into the field. This type of coding is a good idea, even when it detects errors that are not fatal to the current routine but might be fatal to later routines. The call to MsgBox presents the user with a small dialog box. The underscore is used to break a long line over many lines. If you use the underscore, it should have a space before it and nothing after it. It is most commonly used following a comma, and it cannot be inside an open set of quotes.

Neighbor

We need to code the NearNeighbors function so that the squares can tell their neighbors about mines. The AI will rely heavily upon it as well. The AI will also want a function for neighbors that are two squares away. We will code the NearNeighbors function with this need in mind.

One of the questions the AI will need to ask is if a square is in a collection of squares. To make this question easier to answer, we will combine the row and column numbers for a square into a unique key string to use as an identifier.

Add the following line to the PlayingField class and press Enter:

#Region "Neighbor Code"

VB will add the End Region for you. All the code we are about to add will go in this region to help organize it. We will do the easy things first. Start with the code to create a unique string key from the row and column values:

     'Turn two items into a single unique key name for each square
     Public Function KeyFromRC(ByVal row As Integer, ByVal col As Integer) _
             As String
         Return "R" & row.ToString & "C" & col.ToString
     End Function

The next thing we will do is create a list of offsets to compute the neighbors of a square. We will use one set of offsets to compute near neighbors and a different set to compute neighbors two away. Add the following code to the region:

     'We use this list to compute the row and col of adjacent squares
     'Point objects make it easy to store X,Y pairs
      Private NearNeighborOffsets() As Point = { _
         New Point(-1, -1), New Point(-1, 0), New Point(-1, 1), _
         New Point(0, -1), New Point(0, 1), _
         New Point(1, -1), New Point(1, 0), New Point(1, 1)}

If you add the X,Y values stored in the Point objects to the row and column numbers of a square, you will get the row and column numbers of the eight surrounding squares. Squares on the border will have fewer than eight neighbors, so our code will have to catch proposed neighbors that are off the board.

'We have the idea of near neighbors and far neighbors
    Public Function NearNeighbors(ByVal Row As Integer, ByVal Col As Integer) _
            As Collection
        Return GeneralNeighbors(Row, Col, NearNeighborOffsets)
    End Function

    'Both neighbors' functions use same method on different offsets
    Private Function GeneralNeighbors(ByVal Row As Integer, ByVal Col As Integer, _
            ByVal Offsets() As Point) As Collection
        'Put the neighboring Square objects into a collection
        Dim Neighbors As New Collection

        'No neighbors if no field
        If Field IsNot Nothing Then
            Dim Pt As Point
            Dim NeighborRow, NeighborCol As Integer
            For Each Pt In Offsets
                'Add the values in the point to get neighbor
                NeighborCol = Col + Pt.X
                NeighborRow = Row + Pt.Y
                'It has to be on the board
                If (NeighborRow >= 0) And _
                   (NeighborRow < NumRows) And _
                   (NeighborCol >= 0) And _
                   (NeighborCol < NumCols) Then
                   'It is on the board, add it in with key
                   Neighbors.Add(Field(NeighborRow, NeighborCol), _
                       KeyFromRC(NeighborRow, NeighborCol))
                End If
            Next
        End If
        'We always return a collection, even if it is empty
        Return Neighbors
    End Function

If you have not been doing so regularly, now is a very good time to open the File menu and choose Save All. Note that starting the debugger saves the files as well as providing you a chance to see if this works. Start your application in the debugger. Click the Expert button and, once the field paints all of the tiles, click one of them. The results you see should resemble Figure 4.3. The hard work is paying off—this looks like a Minesweeper minefield!

A correctly initialized minefield after the first click.

Figure 4.3. A correctly initialized minefield after the first click.

Take the time to carefully evaluate each square of your own running game. Are all 99 of the mines there? Noting that blanks imply zero, does every square that does not have a mine have the right number? If these numbers are not correct, both human and AI players will have a very frustrating time with your game.

Making It Playable

Our next step is to turn what we have into a playable game. First, we must turn off the debug code that sets the text of the Square objects. That was in two places in the Square class. Comment out the debugging sections in Init and Increment. Run the game and click a tile to make sure.

The player needs more than a field to play; the player also needs to know how many mines remain. Switch to the Design view of PlayingField. We will drag four labels to the control panel to help the user:

  1. Drag a Label control from the Toolbox and drop it to the right of the Expert button.

  2. Change the Text property to 888 and the Name property to MovesLeftLabel.

  3. Change the BorderStyle property to FixedSingle and the TextAlign property to MiddleCenter.

  4. Drag a Label control from the Toolbox and drop it to the right of the MovesLeftLabel.

  5. Change the new label’s Text property to Moves Left.

  6. Drag a Label control from the Toolbox and drop it below the MovesLeftLabel.

  7. Change the Text property to 999 and the Name property to MinesLeftLabel.

  8. Change the BorderStyle property to FixedSingle and the TextAlign property to MiddleCenter.

  9. Drag a Label control from the Toolbox and drop it to the right of the MinesLeftLabel.

  10. Change the Text property to Mines Remaining.

After you open the File menu and choose Save All, your screen should resemble Figure 4.4.

Some vital numbers on the user interface.

Figure 4.4. Some vital numbers on the user interface.

Now we need to provide the code to update those numbers. Add the following code to the end of the NewGame routine, just above the code that changes the cursor back.

     'Init the counters
     MinesLeftLabel.Text = NumMines.ToString
     MovesLeftLabel.Text = sqcnt.ToString

As people manipulate the squares, the squares will need to change the numbers as well. When a player clicks a square to reveal it, the number of moves remaining goes down. When the player flags a square, it reduces both the number of moves and the number of mines. Removing a flag increases both. Add the following code to PlayingField.vb to handle those changes:

     'Code to change the counters - convert the text to int,
     'add or subtract one, change back to text. 2 for moves,
     'and 2 for mines
     Public Sub DecrementMovesLeft()
         MovesLeftLabel.Text = (CInt(MovesLeftLabel.Text) - 1).ToString
     End Sub

     'If you undo a flag, the resulting blank is a valid move
     Public Sub IncrementMovesLeft()
         MovesLeftLabel.Text = (CInt(MovesLeftLabel.Text) + 1).ToString
     End Sub

     'Usually by placing a flag
     Public Sub DecrementMinesLeft()
         MinesLeftLabel.Text = (CInt(MinesLeftLabel.Text) - 1).ToString
     End Sub
     'Removing a flag
     Public Sub IncrementMinesLeft()
         MinesLeftLabel.Text = (CInt(MinesLeftLabel.Text) + 1).ToString
     End Sub

Another demand that the squares will place on PlayingField is to end the game if the player clicks on a mine. The field will do this by telling each square that the game is over and letting the squares act appropriately. Add the following code to the PlayingField class:

     'Something bad happened and a square is calling for the game to end
     Public Sub EndGame()
         Dim Sq As Square
         'Tell them all
         For Each Sq In Field
             Sq.Endgame()
         Next
     End Sub

That dealt with PlayingField. Now onto the squares. A square needs to be able to determine whether its contents have been revealed. It will not want to tell the AI how many mines are near it if it has not been revealed, and it will treat the mouse differently as well. Add the following code to the Square class just below the declarations for contents and actualNearMines:

      'Have I been clicked or not?
      Private Revealed As Boolean

Boolean variables initialize to False. The AI will also want to ask the square if it is revealed, so we should support that capability as well while we are at it. Add the following code to the Square class:

     'The outside world will want to ask us
     Public ReadOnly Property IsRevealed() As Boolean
         Get
             Return Revealed
         End Get
     End Property

We needed to control how the Square object exposes the private variable Revealed to the outside world. Properties allow us to have code between internal data and the outside world. Unlike functions, properties can be either or both directions (read or write) using the same name.

We need to do some work on the Click event handler for the Square class. Refactor the code that makes the button look pressed as follows. This code uses the existing comment and property changes, but integrates them into a more sophisticated block.

     If Not Revealed Then
         'Below here is where the player finds out if it is safe or not
         If contents = HiddenValue.Safe Then
             'This square is done
             Revealed = True
             'Make the button look pressed [reused code from before]
             FlatStyle = Windows.Forms.FlatStyle.Flat
             Me.BackColor = Color.LightGray
             'Tell the user how many are near (if any)
             If actualNearMines > 0 Then
                 Me.Text = actualNearMines.ToString
             Else
                 'Implement free moves here
             End If
             'One fewer move left to make
             theField.DecrementMovesLeft()
         Else
             'Make bad things happen here
             Me.Text = ShowMine
             theField.Endgame()
         End If
     End If

If the user or the AI does click a mine, the Square object asks PlayingField to tell all the squares that the game is over. We will now add code to the Square class so that it can take end-of-game actions. Add the following code to the class:

     'It no longer matters
     Public Sub Endgame()
         'If it is the end of the game,
         'I cannot be clicked (stops cheats)
         Me.Enabled = False
         If Not Revealed Then
             If contents = HiddenValue.Mine Then
                 'If they did not flag me, show the mine
                 If Me.Text <> ShowFlag Then
                     Me.Text = ShowMine
                 End If

             Else
                 'I am a safe square
                 If Me.Text <> "" Then
                     'If they marked it, they were wrong
                     Me.Text = ShowBrokenFlag
                 End If
             End If
         End If
     End Sub

At this point, the code should be playable—aside from the fact that it does not allow the user to mark mines. Run the game in the debugger and see if it plays. Intense concentration and some luck may be required to play for very long. Check that the number of moves decrements and that making mistakes in play not only is fatal, but stops the game. Your game might resemble Figure 4.5 after you make a mistake in play.

After 343 moves and only 38 safe moves to go, one mistake ends the game.

Figure 4.5. After 343 moves and only 38 safe moves to go, one mistake ends the game.

There were still deterministic moves available in the game shown in Figure 4.5 when the mistake was made. An AI player would not have missed the moves or made the mistake. After we add the ability to mark mines with flags, the game will be complete and ready for the AI.

Making It Fun: Adding Flags and a Safety Feature

To flag a blank square, the player right-clicks it. The player right-clicks a second time to remove the flag. It makes no sense to do this to a revealed square.

Examine the events available to the Square class. Note that the Click event is in bold to indicate an event handler is present for it. Scan the list carefully. There is no right-click event. How will we detect a right-click? The list does have the MouseUp event and many other mouse-related events. A control will get a MouseUp event each time the user releases a mouse button, provided the user pressed the button while the mouse was over that control. The MouseUp event always fires for the control that got the MouseDown event. The behavior here is different from a Click event. The Click event will not fire if you move the mouse off the control between button press and button release.

Select the MouseUp event to get a code skeleton. Then add code to complete that skeleton as shown here:

    Private Sub Square_MouseUp(ByVal sender As Object, ByVal e As System.Windows.
Forms.MouseEventArgs) Handles Me.MouseUp
        'This is where we catch right-click - but we have to
        'check which button came up
        If e.Button = Windows.Forms.MouseButtons.Right Then
            'We should be part of a playing field
            Dim theField As PlayingField = Me.Parent

            'If not, we can't tell it anything
            If theField Is Nothing Then Exit Sub

            If Not Revealed Then
                'We change the marking on the tile
                'What is on the tile now?
                Select Case Me.Text
                   Case ""
                       'Blanks get a flag
                       Me.Text = ShowFlag
                       theField.DecrementMinesLeft()
                       theField.DecrementMovesLeft()
                     Case ShowFlag
                        'Flags get a blank
                        Me.Text = ""
                        theField.IncrementMinesLeft()
                        theField.IncrementMovesLeft()
                 End Select
             Else
                 'Placeholder for AI
             End If
         End If
     End Sub

Run the game and right-click some revealed and concealed squares. Mark a square that you know is safe with a flag. Watch the counters to see the number of moves and mines remaining decrease. Next, mark a square that you know holds a mine with a flag. Then click the flagged square that holds a mine. Two interesting things happen, one good, one bad. The good thing is the flag on the safe square turns into an X to show that it was incorrectly flagged. We have now tested a bit of code we wrote earlier. The bad thing is that even though we marked the square with a flag, the square let us click it in the first place, ending the game.

We can guard against a click on a flagged square. In the Click event, find the following line of code:

     If Not Revealed Then

Then add the following code:

     If Not Revealed Then
         'Safety code: if marked, ignore the click!
         If Me.Text = ShowFlag Then Exit Sub

Test this code by marking a square with a flag and then clicking it. The square does not reveal itself. If you right-click the square to remove the flag, anything could happen the next time you click it.

Implementing the AI

We now have a complete Minesweeper game! When the thrill of playing it wears off, you may wish to review the discussion of a rule-based AI earlier in this chapter. We need to design the classes that we will use for the rules and for the framework and extend the game so that the AI can “see” what the player sees. We also need to extend the game so that we can listen to the AI think. The AI will not do anything if we fail to add code that runs the AI.

The AI Needs to Think Out Loud

We start by giving the AI a place to tell us what it is thinking. The extra space on the right side of the control panel makes a perfect place for messages. Bring up the Design view of PlayingField; then follow these steps:

  1. Drag a TextBox control from the Toolbox and drop it to the right of the controls already there.

  2. Change its Name property to ThoughtsTextBox.

  3. Set the Multiline property to True and resize the control to fill the available space.

  4. Set the ReadOnly property to True.

Later on, you may wish to add a vertical scrollbar to the control by setting the ScrollBars property to Vertical. We will create two routines that manipulate this control. Both will add text, but one of them will clear out the old text first. Switch to Code view and create a new region for the AI. Do not put this region inside any other regions. Add code to it to get the following:

#Region "AI Related"
   'Let it speak - clear old stuff
    Public Sub FirstThought(ByVal someThought As String)
        ThoughtsTextBox.Clear()
        MoreThoughts(someThought)
    End Sub

   'Say what it is thinking
    Public Sub MoreThoughts(ByVal someThought As String)
        ThoughtsTextBox.AppendText(someThought & vbCrLf)
    End Sub
#End Region

Our AI now has a place to say things. We should clear what it is thinking when we start a new game. Add the following line of code to the NewGame routine just above where the cursor is returned to normal:

     'Remove thoughts from last game
     ThoughtsTextBox.Clear()

Rules

It is time to design the rules and the framework to make use of them. We start with the rules. All the different rules have common elements. For example, every rule proposes a set of moves as part of the matching phase. In addition, every rule needs to execute its proposal when selected for execution by the framework. Add a class to the project and name it BasicRule.vb. Mark it MustInherit and add code to get the following:

 Public MustInherit Class BasicRule

      'Child classes and outside helpers need this
      Public Enum PossibleActions
           BlanksToFlags
           ClickBlanks
       End Enum

       'We need to remember what move we propose
       Protected SimonSays As PossibleActions
       'And the targets of that action
       Protected SquaresList As New Collection

       'All rules must have tell how well they match
       Public MustOverride Function Matches(ByVal RevealedSquare As Square) As Integer

       'The match routine stores our proposal for possible execution
        Public Sub Execute()
             Dim Sq As Square
             For Each Sq In SquaresList
                 'We only ever do unknown blanks
                 If (Not Sq.IsRevealed) And (Sq.Text = "") Then
                     'What did we propose to do?
                     Select Case SimonSays
                        Case PossibleActions.ClickBlanks
                            Call Sq.LeftClick()
                        Case PossibleActions.BlanksToFlags
                            Call Sq.RightClick()
                     End Select

                 End If
             Next
         End Sub
     End Class

You can try to see if you can get the Sq variable to divulge the hidden contents variable, but VB respects the private marking in the Square.vb file. Hidden in this design is the idea that a rule will do only one kind of move. It turns out not to be a limitation; all the rules we will write will boil down to either “Flag a bunch of squares” or “Click a bunch of squares” but never both. This code asks the squares to click and right-click themselves; that code does not exist. We will add that capability to the Square class. Switch to the Square class and add the following code:

    'Let code work our UI:
    'A regular click of the square
    Public Sub LeftClick()
        Call Square_Click(Nothing, Nothing)
    End Sub

    'Let them mark a square with right-click
    Public Sub RightClick()
        'Create the arguments for a right-click
        'All we care about is the button
        Dim e As New System.Windows.Forms.MouseEventArgs(Windows.Forms.
MouseButtons.Right, 0, 0, 0, 0)
        Call Square_MouseUp(Nothing, e)
    End Sub

The Click event handler ignores its arguments, so we can safely pass it Nothing for both of them. The MouseUp handler looks to see what button was pressed, so we created a new mouse event arguments object with the correct button and zeroes for all the numbers. We do not care about those numbers, and zero is a safe value for them.

There are two types of cheating for game AI: The AI can know things that it should not, or the AI can do things the player cannot. For this reason, there is a very important design decision made here: The AI uses the same user interface as the player, and is restricted to the same actions as the player. The AI has shims between it and the player code, but those shims are very thin and know nothing about the intent of what they transmit. In most commercial games, the shim is usually between the player and the game because the AI can natively speak the command language of the game. Besides preventing cheating, using common code simplifies the evolution of the game by having only one command path instead of two that must be kept synchronized.

A Rule for Single-Square Evaluation

Let us create our first rule. The first rule will ask the question, “Can I tell what goes in the blanks surrounding a revealed square using the revealed count and the number of flags and blanks surrounding the revealed square?” This boils down to two statements: If the number of flags around the revealed number equals the revealed numbers, then any surrounding blanks must be safe. And if the number revealed minus the number of flags is equal to the number of blanks, then any surrounding blanks are all mines. More simply, “These are safe because I know all of the mines already,” or “Mines are all that are left.”

This rule will require that our AI find out a number of basic statistics. How many flags surround the revealed square? How many blanks? The revealed square itself gives the number of nearby mines. In addition to statistics, the rule will want to know what squares around the revealed square are blanks because the action of the rule, if it executes, will be to click them all or flag them all. It turns out that three of our rules will need this data. It will be a lot easier to get this data if we can get the Square objects to tell us their row and column data so that we can get their neighbors and their key value. Add the following to the Square class:

     'Let the outside world ask but not set our row
     Public ReadOnly Property R() As Integer
         Get
             Return Row
         End Get
     End Property

     'Let the world ask our column, too
     Public ReadOnly Property C() As Integer
         Get
             Return Col
         End Get
     End Property

Since many rules will need the basic data, we should place the code for it in a separate file where all rules can get to it. Add a module to the project (similar to adding a class) and name it AI.vb. Then add the following code:

     'Note the three ByRef parameters - we write to them
     Public Function BasicStatsAndBlanks(ByVal RevealedSquare As Square, _
             ByVal Neighbors As Collection, _
         ByRef sees As Integer, ByRef flags As Integer, _

             ByRef blanks As Integer) As Collection
         'Look at line above and see the integers are all ByRef!

         Dim BlankSquares As New Collection
         'Text of revealed squares are blank = 0 or a number
         If RevealedSquare.Text <> "" Then
             sees = CInt(RevealedSquare.Text)
         End If

         'Get the counts of what they show
         Dim Sq As Square
         For Each Sq In Neighbors
             'We want hidden squares only
             If Not Sq.IsRevealed Then
                 'Count the flags and blanks
                 Select Case Sq.Text
                     Case ""
                        blanks += 1
                        BlankSquares.Add(Sq,PlayingField.KeyFromRC(Sq.R,Sq.C))
                     Case Square.ShowFlag
                        flags += 1
                 End Select
             End If
         Next
         'The caller often needs the blank squares as a group
         Return BlankSquares
     End Function

This routine collects the stats and writes them back onto the passed-in parameters. VB defaults to call by value, so we have to make sure that we use the ByRef keyword. This function returns a collection holding any blank squares. Armed with this helper routine, our first rule is easy to write. Create a class and name it RuleOne. Mark it to inherit from BasicRule. The only code we have to add is the Matches routine.

Public Class RuleOne
   Inherits BasicRule

      Public Overrides Function Matches(ByVal RevealedSquare As Square) As Integer
           'Clear out anything from before
            Me.SquaresList.Clear()
            'Do not run on a hidden square!
        If RevealedSquare.IsRevealed Then
            'We should be part of a playing field
            Dim theField As PlayingField = RevealedSquare.Parent

            'Who is around me?
            Dim Neighbors As Collection = theField.NearNeighbors
(RevealedSquare.R, RevealedSquare.C)

            'We keep a bunch of numbers:
            'How many mines do we see?
            Dim sees As Integer = 0
            'And how many flags are around us?
            Dim flags As Integer = 0
            'And how many blanks are around us?
            Dim blanks As Integer = 0
            Dim BlankSquares As Collection
            'Now fill in all of those.   Note that the variables
            'for the three numbers are passed by reference.
            BlankSquares = BasicStatsAndBlanks(RevealedSquare, Neighbors, sees, _
                flags, blanks)

            'No blanks, no work possible
            If blanks > 0 Then
                'Decision time! No worries, it can't be both

                If sees = flags Then
                   theField.MoreThoughts(Me.GetType.Name & " sees " & _
                       blanks.ToString & " safe squares to click.")
                   'Store the result for later execution
                   SimonSays = PossibleActions.ClickBlanks
                   SquaresList = BlankSquares
                End If

                If blanks + flags = sees Then
                   theField.MoreThoughts(Me.GetType.Name & " sees " & _
                       blanks.ToString & " mine squares to flag.")
                   'Store the results for later execution
                   SimonSays = PossibleActions.BlanksToFlags
                   SquaresList = BlankSquares
                End If
            End If
        End If

             'This is how many moves we can make
             Return Me.SquaresList.Count
         End Function
     End Class

The routine declares the numbers it needs and sets them to zero. It gets the neighboring squares from the playing field. Armed with all that, it gets the basic statistics and the collection of nearby blank squares. The decision will be to flag all the blanks as mines, click all of them because they are safe, or do nothing. Since this is the only rule, we can test it without writing the framework.

Switch to Square.vb. Find the MouseUp event handler. Look for the comment about a placeholder for AI. When the user right-clicks a revealed square, that user is asking the AI to run. Replace the placeholder comment with the following code:

     'Placeholder for AI
     theField.FirstThought("Thinking about Square at Row=" & _
     Row.ToString & ", Col=" & Col.ToString)
     Dim R1 As New RuleOne
     If R1.Matches(Me) > 0 Then
         R1.Execute()
     End If
     'End placeholder

This is sufficient to test the rule. Run the game and right-click every revealed square. If the AI makes a move, you may want to click again on revealed squares previously clicked to see if the AI now has enough information to make another move. Armed with this single simple rule, after you get a game started, the AI can make around 90 percent of the moves needed to solve the game. You will have to help it now and then by using the information of more than one square. This rule proves that Minesweeper is less about thinking hard than it is about never making a mistake.

This rule executes perfectly, giving it an advantage over human players. Does it make the game more or less fun? If the fun part of the game is making the hard moves and the thrill of making a non-fatal guess, then the rule takes away the boring, repetitive part of play. If the fun part of the game is the challenge of holding to discipline and demonstrating the perfection of your play, then this rule trashes the fun right out of the game. Recall in the earlier discussion that the programmer must mediate between an AI that is stupid and thus boring versus one that is too smart and thus frustrating. With only one rule in place, we can clearly see this need.

The Framework

The first rule was a great start. It is time to add the framework so that we can add another rule. Add a new class to the project and name it FrameWork.vb. The framework is very easy to code; it depends on the rules being loaded in order of increasing complexity and needs a place to store the rules. It also needs a routine to match and then execute the best rule, as well as a routine for loading rules. Add the following code to the class:

     Private Rules As New Collection

     Public Sub AddRule(ByVal goodIdea As BasicRule)
         'Add it if it is not there
         If Not Rules.Contains(goodIdea.GetType.Name) Then
             'Use its type name as string
             Rules.Add(goodIdea, goodIdea.GetType.Name)
         End If
     End Sub

Now we need the match and execute routine. It is far less complex than the rules it invokes. Add the following routine to the FrameWork class:

     Public Sub RunAI(ByVal RevealedSquare As Square)
         'Keep the best rule and its score
         Dim bestRule As BasicRule = Nothing
         Dim bestScore As Integer = 0

         'We want the playfield so that we can talk
         Dim theField As PlayingField = RevealedSquare.Parent

         Dim someRule As BasicRule
         Dim currentScore As Integer
         'Go through the rules we have loaded in order
         For Each someRule In Rules
             currentScore = someRule.Matches(RevealedSquare)
             If currentScore > bestScore Then
                 'Best idea so far, at lowest cost
                 bestRule = someRule
                 bestScore = currentScore
             End If
         Next

         'Did we get a good idea? If so, use it
         If bestRule IsNot Nothing Then
             theField.MoreThoughts("       Executing " & bestRule.GetType.Name)
             bestRule.Execute()
         Else
             theField.MoreThoughts("       No good ideas found.")
         End If
     End Sub

Adding the Framework to the Game

The right place to create and hold a FrameWork object is in PlayingField. We only need one copy of it, and we only need to initialize it once. We will need to make it available to the squares so they can ask to run the AI when they get user input. Switch to the Code view of PlayingField and add the following code to the class just below the declarations for Field and the three Num variables. (We are keeping this kind of data together to make it easier to find.)

     'This is the AI.
     Public Brains As New FrameWork

Have VB create the skeleton of the Load event handler for PlayingField. Add code to it so that it resembles the following:

         Private Sub PlayingField_Load(ByVal sender As Object, ByVal e As
     System.EventArgs) Handles Me.Load
             'All we have to do is load the rules IN ORDER
             Brains.AddRule(New RuleOne)
         End Sub

The framework is available and loaded; next, we need to call it. Return to the MouseUp event handler in Square.vb, locate the placeholder AI code, and replace the placeholder code, including the begin and end comments, with the following:

     'Run the real AI
     theField.FirstThought("Thinking about Square at Row=" & _
         Row.ToString & ", Col=" & Col.ToString)
     theField.Brains.RunAI(Me)

Now run the game and right-click on the revealed squares. The game plays as expected; we are ready for another rule.

Rules for Two-Square Evaluation

The next two rules are very similar: They use the information from two revealed squares to look for moves. These rules could be combined into a single rule, but we will leave them separate so that we can control how smart our AI appears. We also leave them separate because one version is easier for humans to see than the other. Both of these points are game-design issues. We want our AI to play like a human and we want to easily control how well it plays.

So how will the rule work? It takes a revealed square and attempts to use a nearby square as a helper. In the simpler version of the rule, the nearby helper is adjacent to the original revealed square. The two squares need to share some blank squares, and the original square also needs to be adjacent to some blank squares that are not shared. The helper square computes the minimum number of mines and the minimum number of clear squares that are in the shared squares. These numbers can be zero but not negative. If either number is greater than zero, the helper has provided the original square with potentially new information. The original square already has information about all of its squares, but the helper gives information about a subset of them. If the minimum number of mines in the shared squares is equal to the number of mines the original square has not yet placed, the original square can safely click all the squares that are not shared because it knows that all of its mines are somewhere in the shared squares. If the minimum number of clear squares in the shared squares is equal to the number of unknown clear squares around the original square, then all the non-shared blank squares around the original square must be mines. This rule does not act on the shared squares; it acts on the original square’s private squares. If your brain is in danger of exploding, perhaps a picture will make the situation clear (see Figure 4.6).

Can you spot the three safe moves?

Figure 4.6. Can you spot the three safe moves?

The upper 1, with the dark border, is the helper square. The lower 1 is the original square. The rule will not work the other way around because the upper 1 has no private blanks. The lower 1, the original square, has three private blanks, in addition to four shared blanks. The helper can compute that at least one mine must be in the shared squares; it sees one mine, and the only thing around it is shared squares. The original square needs to place one mine, and the helper just told it that at least one mine is in the shared squares. That was all the mines the original square had left to place, so the private squares must all be clear. If there are any flags nearby, they adjust the various numbers, but the method is the same. Note that the move is in the original square’s private blank squares. The two squares do not yield enough information to safely determine anything about the shared squares. Note that the move consumes all the private blank squares.

The same method also places mines. If the original square had been a 4 and the helper square remained a 1, the helper would report that there are at least three safe shared blank squares. The original square has seven blanks and four mines to place, leaving three safe blank squares to account for. The original square hears from the helper that all three of the original square’s safe squares are among the four shared squares. This means that there are no safe private blank squares around the original square, so those private blank squares are all mines (see Figure 4.7). This is a powerful rule, and together with the single square rule it plays very effectively. Turning the rule into code will be somewhat complex.

The AI places three mines using the second rule.

Figure 4.7. The AI places three mines using the second rule.

A Two-Square Evaluation Rule Using Near Neighbors

Add a class to the project, name it RuleTwoNear.vb, and make it inherit from BasicRule.

     Inherits BasicRule

VB will provide the skeleton for the Matches function. Add code to the Matches function so that it resembles the following:

     Public Overrides Function Matches(ByVal RevealedSquare As Square) As Integer
         'We use a helper function with near neighbors

          'We should be part of a playing field
          Dim theField As PlayingField = RevealedSquare.Parent

          'Who is around me that might help?
          Dim CloseNeighbors As Collection = theField.NearNeighbors( _
              RevealedSquare.R, RevealedSquare.C)

          'Do the work
          Call AI.TwoSquareMatcher(RevealedSquare, CloseNeighbors, _
              SimonSays, SquaresList)

          'How many moves were found?
          If SquaresList.Count > 0 Then
              'Tell the world what we think
              If SimonSays = PossibleActions.BlanksToFlags Then
                  theField.MoreThoughts(Me.GetType.Name & " sees " & _
                      SquaresList.Count.ToString & " mines to flag.")

        Else
                 theField.MoreThoughts(Me.GetType.Name & " sees " & _
                     SquaresList.Count.ToString & " safe squares to click.")
             End If
         End If

         'Tell the framework how many moves
         Return SquaresList.Count
     End Function

This code will depend on a routine in AI.vb that we have not written yet. The important point here is that the rule asks the squares directly adjacent to the revealed square for help. For most squares, that will be eight surrounding squares, and the NearNeighbor function finds them. We store them in the CloseNeighbors collection. Everything else in this routine is generic. Now we turn to AI.vb to implement the matcher. Add the following code to AI.vb:

     'This function does the work for two rules
     'This code tries to use a helper to help find moves
     'Two byRef parameters
     Public Sub TwoSquareMatcher(ByVal RevealedSquare As Square, _
             ByVal Helpers As Collection, _
             ByRef SimonSays As BasicRule.PossibleActions, _
             ByRef SquaresList As Collection)

         'Clear the list of proposed moves in case we do not find any
         SquaresList.Clear()

         'We should be part of a playing field
         Dim theField As PlayingField = RevealedSquare.Parent

         'Get the basic data of the revealed square
         'Who is around me?
         Dim Neighbors As Collection = theField.NearNeighbors(RevealedSquare.R, _
             RevealedSquare.C)

         'We keep a bunch of numbers:
         'How many mines do we see?
         Dim sees As Integer = 0
         'And how many flags are around us?
         Dim flags As Integer = 0
         'And how many blanks are around us?

          Dim blanks As Integer = 0
          Dim BlankSquares As Collection
          'Now fill in all of those - note that the
          'three numbers are call by reference
          BlankSquares = BasicStatsAndBlanks(RevealedSquare, Neighbors, sees, _
              flags, blanks)

          'If no blanks, we have nothing to do.
          If blanks = 0 Then Return

          'Can one of the helpers aid us?
          Dim Helper As Square
          For Each Helper In Helpers
              'If at any point in the loop we know no help is
              'possible, we continue For to get the next helper

              'To help me, they must be revealed
              If Not Helper.IsRevealed Then Continue For

              'We need the helper's basic data

              Dim TheirNeighbors As Collection = _
                  theField.NearNeighbors(Helper.R, Helper.C)
              'How many mines do they see?
              Dim theySee As Integer = 0
              'And how many flags are around them?
              Dim theirFlags As Integer = 0
              'And how many blanks are around them?
              Dim theirBlanks As Integer = 0
              Dim TheirBlankSquares As Collection
              'Now fill in all of those - note that the variables
              'for the three numbers are passed by reference
              TheirBlankSquares = BasicStatsAndBlanks(Helper, TheirNeighbors, _
                  theySee, theirFlags, theirBlanks)

              'If they lack blanks, they can't help us
              If theirBlanks = 0 Then Continue For

              'My blanks that they can't see are where my moves will go
              Dim PrivateBlanks As New Collection
              'Shared blanks are how they will help us
              Dim commonBlankCount As Integer = 0
           'Compute and collect those blanks
          Dim Sq As Square
          'Go through my blanks looking in theirs
          For Each Sq In BlankSquares
              'Need the key to search
              Dim sqKey As String = theField.KeyFromRC(Sq.R, Sq.C)
              If TheirBlankSquares.Contains(sqKey) Then
                  'It's mine and it's theirs
                  commonBlankCount += 1
              Else
                  'It's mine alone and a possible move
                  PrivateBlanks.Add(Sq, sqKey)
              End If
          Next
          'Do we have anything to say?
          If commonBlankCount = 0 Then Continue For
          'Do I have possible moves?
          If PrivateBlanks.Count = 0 Then Continue For

          'So what do those common blanks tell us?

          'We can compute how many private blanks they have
          Dim theirPrivateBlankCount As Integer = theirBlanks - _
              commonBlankCount
          'From that we can take a crack at their view of the smallest possible
          'number of mines in the common blanks
          Dim minCommonMines As Integer = theySee - theirPrivateBlankCount - _
              theirFlags
          'But it can't be negative
          If minCommonMines < 0 Then minCommonMines = 0

          'We can run similar numbers for clear squares
          Dim minCommonClear As Integer = theirBlanks - _
              (theySee - theirFlags) - theirPrivateBlankCount
          'That can't be negative either
          If minCommonClear < 0 Then minCommonClear = 0

          'If those are both zero, they are no help to us
          If minCommonClear = 0 And minCommonMines = 0 Then Continue For

          'This is a good point for error checks

          'We have useful information - is it useful enough?
          'Do the mines help us?
        If minCommonMines > 0 Then
                 If minCommonMines = sees - flags Then
                     'The common mines are all of my mines!
                     'Since both variables were ByRef, we can change them
                     SimonSays = BasicRule.PossibleActions.ClickBlanks
                     SquaresList = PrivateBlanks
                     'Finding one set of moves is good enough
                     Return
                 End If
             End If

             'Do the clear squares help us?
             If minCommonClear > 0 Then
                 If blanks - minCommonClear = sees - flags Then
                     'The common squares include all of my clear
                     'Therefore, my private blanks must all be mines
                     'Since both variables were ByRef, we can change them
                     SimonSays = BasicRule.PossibleActions.BlanksToFlags
                     SquaresList = PrivateBlanks
                     'Finding one set of moves is good enough
                     Return
                 End If
             End If
         Next Helper
     End Sub

The first part of the routine reads just like single-square matching. We get the basic statistics for the original square and check for blanks. There is nothing to do if there are no blank squares to act on. At that point, the original square looks for help from the helpers that were passed in.

If the helper is not revealed, the helper square lacks the required numerical information. The Continue For directive tells VB that we are done with this iteration of the loop and to go on with the next iteration. We will make numerous qualifying tests on the helpers as we go. This could be coded with nested If statements, but the nesting level would be extreme.

At this point, we know that the helper has basic data, so we get it the same way we get it for any other square. If the helper has no blanks, it cannot help. If it has blanks, we need to know if any of them are common blanks. We need the count of the common squares but not the squares themselves. We do need the original square’s private blanks, however, because those squares are where we will make our moves if we find any.

We loop through the original square’s blanks, checking them against the helper’s blanks. We count the common blanks and store the private blanks. When we are done, we look at the numbers. Without common blanks, the helper cannot feed the original square any new information. Without private blanks, the original square has no moves to make. If there are common blanks but no private blanks, the original square might be a good candidate to help the helper square, but we do not pursue that. The user told the AI to look for moves for the original square.

We are finally ready to compute the numbers. We compute the minimum common mines and clear (safe) squares among the common blank squares as seen by the helper. We start with the number of mines they see and decrement that count by any flags they see since they may know where some of their mines are. We then decrement by the number of private blanks that could hide mines to determine their view of the minimum number of mines in the shared blank squares. Then we make similar computations for the minimum number of clear squares, starting with the blanks they see and decrementing that by the number of mines they do not know about, which is the number they see less any flags they have placed. Then we decrement again by their private blanks that could be safe, and we are left with their view of the minimum number of safe squares among the common blank squares. It takes a ton of code to get to this point. Adding some Debug. Writeline statements might be a good idea here. The output will show in the Immediate window when you run the game in the debugger. Some error checks might be good here as well. The minimum common mines should not be greater than the number of mines the original square needs to place. The minimum common clear squares should not be greater than the number of safe squares that have to be around the original square. If you don’t think your code is working correctly, add those error checks. If you are unsure about your code, use Debug.Writeline to display all the computed numbers so that you can compare them to the board.

All that remains is to evaluate the quality of the numbers. The numbers could indicate that the private squares are mines or that the private squares are safe. The code sets the two variables passed in by reference to the correct move and to a collection holding the proper squares.

Note

VB does automatic garbage collection, so we do not worry about what happens to an object when no variables point to it.

There is a design decision in the code that the comments point out. We take the first helper who can help and go with it. It is possible that a different helper could come up with more moves for the original square. Rather than evaluate them all, we go with the first one that qualifies. The match portion of a rule needs to be computationally efficient because the framework will run it often.

That completes the rule. The rule will never run if we fail to put it into the framework. Find the PlayingField Load event handler and add the following line after the first one:

     Brains.AddRule(New RuleTwoNear)

Run the code. After you get a game started, you can chase the perimeter by madly right-clicking revealed squares. Slow down and watch carefully, and you will see the two-square rule fire and leave a single-square move that another right-click will pick up. If the game ever steps on a mine, you have a bug or the player has manually flagged a safe square. Look at the thoughts output to make sure that the first rule with the most squares is the one that executes. That way, we know that the framework is working properly.

Play a number of games using the AI as much as possible. How much fun is Minesweeper now? Is the AI too smart or not smart enough?

Two-Square Evaluation Using Non-Adjacent Squares

The AI can still use more help. If you think about it, you may realize that the helper square does not have to come from the directly adjacent squares (usually eight of them). The helper could be from one of the squares surrounding the directly adjacent squares. There are usually 16 such surrounding squares. The original square and the helper will have common squares directly between them. If one or more of these are blank, and the original square has other private blanks, the same method works from farther away.

All we need is access to the next outer ring of neighbors. Switch to the Code view of PlayingField.vb and find the NearNeighbors code. We need a different set of offsets for the new neighbors. Add the following to the class file:

     Private FarNeighborOffsets() As Point = { _
         New Point(-2, -2), New Point(-2, -1), New Point(-2, 0), _
         New Point(-2, 1), New Point(-2, 2), _
         New Point(-1, -2), New Point(-1, 2), _
         New Point(0, -2), New Point(0, 2), _
          New Point(1, -2), New Point(1, 2), _
          New Point(2, -2), New Point(2, -1), New Point(2, 0), _
          New Point(2, 1), New Point(2, 2)}

We also need a public routine to return a collection of Square objects. GeneralNeighbors will do it for us if we pass in the new offsets. Add the following code to the class:

         Public Function FarNeighbors(ByVal Row As Integer, ByVal Col As Integer) As
     Collection
             Return GeneralNeighbors(Row, Col, FarNeighborOffsets)
         End Function

This capability makes writing the rule refreshingly easy. Add another class to the project and name it RuleTwoFar.vb. Copy everything inside the RuleTwoNear class and paste it inside the RuleTwoFar class. Start with Inherits and be sure to get the End Function line. We need to change the code that deals in getting the list of potential helpers.

Change this line:

        Dim CloseNeighbors As Collection = theField.NearNeighbors
(RevealedSquare.R, RevealedSquare.C)

Into this:

        Dim OuterNeighbors As Collection = theField.FarNeighbors
(RevealedSquare.R, RevealedSquare.C)

Since we changed the variable name for clarity, we have to change it everywhere. Just below the declaration is the following line:

        Call AI.TwoSquareMatcher(RevealedSquare, CloseNeighbors, SimonSays,
SquaresList)

That line should be changed to read as follows:

        Call AI.TwoSquareMatcher(RevealedSquare, OuterNeighbors, SimonSays,
SquaresList)

That completes the rule. Remember to put a copy of the rule into the framework. Find the PlayingField Load event handler and add the following line after the first two:

     Brains.AddRule(New RuleTwoFar)

Now run the game. It may be hard to find places where the new rule has moves. Figure 4.8 shows a game example where it can fire.

Our new rule has three safe moves.

Figure 4.8. Our new rule has three safe moves.

The lone revealed 1 square can get help from the revealed 1 square two columns to the left. The common blank squares hold all the mines the lone square needs to place, making the three private squares safe moves. The thinking output is from a prior move and can be ignored. After right-clicking the lone square in Figure 4.8, we get Figure 4.9.

Our new rule takes the three safe moves.

Figure 4.9. Our new rule takes the three safe moves.

In Figure 4.9, the thinking output is current, and we see that our new rule fired. The first two rules we implemented demolish most of a Minesweeper game. This third rule keeps the AI from getting stuck. I risked clicking the tile with the lone 1 in Figure 4.8 precisely to take advantage of the power of the new rule. This rule gives the ability to make guesses productive. The rule did not change the risk of clicking a random blank tile, but it clearly improves the reward of clicking tiles just past the perimeter.

Do We Need More Rules?

As shown in Figure 4.10, the AI still gets stuck sometimes when there are deterministic moves. Find the pair of1 squares at the bottom of the group of four revealed squares at the left edge of all the cleared squares. One of them has a darker outline. There is a 2 and a 3 above those 1 squares. Above that are two unknown squares. By looking at the four revealed squares above those unknown squares, we can determine that there is one mine in the two unknown squares. The 2 and the 3 squares then tell us that there is one unknown mine in the pair of squares to the left of the 2 and one unknown mine to the right of the 3, in addition to the flag already there. The 1 under the 2 sees the same mine that the 2 sees, making all squares below it safe. The outlined 1 under the 3 sees the same additional mine the 3 sees, making all squares below the 1 safe. This gives us four safe moves that the AI does not see.

There are four safe moves that the AI does not see.

Figure 4.10. There are four safe moves that the AI does not see.

Experienced players sometime use three or more tiles to find a move. We could implement rules that use three or even more tiles, but it begs a question: What’s the point? The AI now can play most games either to completion or to the point where all that remains is a handful of purely random guesses. A lucky few games require the player to use some serious brain power or to make the risky guess that will end the game or unleash the AI anew.

If we added the more sophisticated rules, we would want to create a setting for the AI so that we could control how deep into the rule base it could go. This implementation of a rule-based AI is inherently adjustable. One of the advantages to a rule-based system is that it gives an intuitive way for the AI programmer to adjust the degree of difficulty.

There are a few simple rules that could be added to finish the game. If the number of mines left hits zero, then the AI should click all remaining squares. If the number of moves equals the number of mines, the AI should flag every remaining square. The need for these rules did not make an appearance until the AI was well on its way to finishing off most games. We watched it play and noticed an area for improvement.

This illustrates one of the advantages of the method: Working with the rules makes it easier to add new rules. We can evolve the AI by seeing a need and then adding a new rule to cover it. We do not have to implement every good idea we come up with at the very start because we can test as soon as we have one rule. If the AI proves sufficient with a few simple rules, the programmer does not need to risk investing time in more complex ones.

Chapter Summary

This chapter shows that a few rules and a framework to run them go a long way. Once the framework is created and understood, new rules can be added quickly and easily without upsetting existing work. There is an up-front cost to the system, but it pays off quickly with every new capability added. Rule-based systems are inherently tunable and allow for almost Darwinian evolution to cover any deficits. As shown by the project, when the rules fit the game well, they are powerfully effective.

Chapter Review

Answers are in the appendix.

1.

What are the two parts of a rule in a rule-based system?

2.

What does the framework do in a rule-based system?

3.

Why is it that a rule-based system can play like both a human and a machine at the same time?

4.

What makes a rule-based AI appear intelligent? What makes it appear stupid?

Exercises

The code for some of these exercises is on the CD.

1.

Add buttons below the Expert button for Intermediate (16 row, 16 columns, and 40 mines) and Beginner (9 rows, 9 columns, and 10 mines) games.

2.

Add code to track the number of moves made by the player and by each rule. For a more in-depth analysis, keep statistics over many games that include per-move data for all 480 possible moves. When is the game the most dangerous?

3.

Modify the framework so that RunAI runs the match-execute cycle repeatedly until it finds no moves around the revealed square. You will need to add a scrollbar to the ThoughtsTextBox control. You might want to make it taller as well. This code is on the CD.

4.

Add code to take free moves when a zero is revealed. Recall that the playing field can tell a square who its neighbors are. The following fragment of code may come in handy:

     Sq.Square_Click(Nothing, Nothing)

Our Click event handler ignores the parameters that Windows passes in, so we pass in Nothing when we call the event handler.

5.

Add the two end-of-game rules mentioned earlier. Like our other rules, they need some support from the game. In terms of cost, where do they go in our ordered list of rules?

6.

Add code that has the AI search for moves and make all the moves that it can. It may be helpful to keep a work list that holds revealed tiles that have one or more unknown adjacent tiles. It will be far faster to search the work list than to search the entire playing field. This addition will really show how powerful the AI can be, although keeping the work list correct may be a challenge. This code is on the CD.

7.

Write a Sudoku game and a rule-based AI for it. Think of the rules you use to find moves when you play Sudoku. Put those rules into a rule-based AI and see how well it plays.

References

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.72.245