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.
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.
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.
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.
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.
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.
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 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.
Change the name of Forml to PlayingField.vb and its Text property to Mines.
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.
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.
Resize the panel so that it takes up all of the bottom of the form.
Change the BackColor property to White.
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.
This gives us rudimentary versions of two of our three main components of the game.
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.
Switch to the Design view of PlayingField
:
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.
Change the Button control’s Text property to Expert and the Name property to ExpertButton.
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)
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.
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.
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.
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!
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.
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:
Drag a Label control from the Toolbox and drop it to the right of the Expert button.
Change the Text property to 888 and the Name property to MovesLeftLabel.
Change the BorderStyle property to FixedSingle and the TextAlign property to MiddleCenter.
Drag a Label control from the Toolbox and drop it to the right of the MovesLeftLabel.
Change the new label’s Text property to Moves Left.
Drag a Label control from the Toolbox and drop it below the MovesLeftLabel.
Change the Text property to 999 and the Name property to MinesLeftLabel.
Change the BorderStyle property to FixedSingle and the TextAlign property to MiddleCenter.
Drag a Label control from the Toolbox and drop it to the right of the MinesLeftLabel.
Change the Text property to Mines Remaining.
After you open the File menu and choose Save All, your screen should resemble Figure 4.4.
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.
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.
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.
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.
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:
Drag a TextBox control from the Toolbox and drop it to the right of the controls already there.
Change its Name property to ThoughtsTextBox.
Set the Multiline property to True and resize the control to fill the available space.
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()
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.
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 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
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.
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).
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.
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.
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?
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.
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.
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.
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.
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.
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.
Answers are in the appendix.
The code for some of these exercises is on the CD.
18.225.72.245