Small Basic: The Battleship Game in detail
For the Small Basic Challenge of the month – March 2015 , I do the challenge to create a Battleship game against the computer.
You import the game in Small Basic : MRV593-2. Or you can download source code and exe from the Technet gallery : https://gallery.technet.microsoft.com/Small-Basic-Game-Battleship-4760e6f3.
As I do these programs primarily to help beginners in programming in their learning, I tried to do a clear code with lot of comments. However this program is a little complex, also this article gives more details on the code and how it run.
The Program Code
Splitting code in subroutine
The subroutines are generally used to execute code that we reuse throughout our code, this to avoid copy/paste code. In this program, code is divided into multiple subroutines that are not reused, the purpose being to create small "parts" of code more easy to read and to be find quickly in the code (with a search we reach the specific location of the program).
This is a nice practice to do because it allow you to organize your code and therefore your ideas.
General Concepts of the Game
The game begins with initialize game (call subroutine InitializeGame
), then a loop executes et call the GameLoop
subroutine running the specific code of state of the game (see below), until a request to stop the game is made (empty gameState
).
Game states
The game can be in differents states, the gameState
contains the current state:
- intro : Show the introduction screen
- createboard : Screen allowing the player to create his board with these ships
- play : Handle the the playing game
- end : Screen displaying the winner
- Empty : if
gameState
is empty, the the game loop is stopped and we quit the program
Each state has is own loop "Game
[State]
Loop
" called by the GameLoop
subroutine according gameState
. This loop manages the operations of the screen corresponding of the state.
For each state we have a "Goto
[State]
Screen
" subroutine managing the screen initialisation, more variables for the game loop, and changes gameState
for the game loop runs the good state loop.
Keyboard Management
The keyboard management is simply, an HandleKeyPress
event is raised when a key is pressed, this key is saved in the variable lastKey
. This variable is used by the loop to manage the keyboard. Generally this variable is cleared so that the next loop does not handle once again this key.
Mouse Management
The mouse is supported in this game. The mouse movements are not used, only the position of the mouse at a specific time is important for us. On the other hand we manage the mouse buttons clicks on the same way as the keyboard.
A HandleMouseDownUp
event is raised whenever one of the mouse button is pressed or released. The lastMouseButtons
variable contains the mouse buttons that are pressed :
- Left : Left button is pressed
- Right : Right button is pressed
- Left+Right: Both buttons are pressed
- Empty : no buttons are pressed
Like in the keyboard management, this variable is cleared so that the buttons are not treated several times.
The Game Board and the Ships
The ships are placed on a board usually called a battlefield. This board is a grid of 10 x 10 cells. It is displayed by drawing a grid with cells of drawBoardCellSize
pixels size (height and width because cells are squares).
Each ship is a Small Basic array with the following indexes :
["name"]
: Name of the ship["x"]
: Ship X position (starting to 1)["y"]
: Ship Y position (starting to 1)["size"]
: Ship size["dir"]
: Ship direction- "r" (as Right) : Horizontal ship
- "d" (as Down) : Vertical ship
The ships list is saved in the ships
variable for the player and computerShips
for the computer.
The ships can't be out the board, can't intersect another ship and can't touch another ship.
The ships list used in the game (name and size) is build in the BuildShips
subroutine located in the end of the source code. If you want change the ships played in the game, update this subroutine.
Game Initialization
A starting, we call InitializeGame
to prepare the game. This subroutine does nothing special, it initialize variables, prepare the GraphicsWindows, define keyboard and mouse events.
We can find some valuables as game parameters like drawBoardCellSize
which contains the width and the height of a cell board.
When finished, it called GotoIntroScreen
to go to the intro state.
Main Game Loop
The game spend lot of time to wait for a player's action. This action is detected by the lastKey
and lastMouseButtons
variables. To handle this, we used a "Game Loop" which is a While
when each iteration check these variables to execute the player actions.
As our program can be in different states, the player actions changes according the state, this is why we are a specific loop for each state (see above).
Our main loop (at beginning of the code) call GameLoop
which process the specific loop in function of gameState
and do a small pause. This loop runs while gameState
is not empty (that is not a state). To stop the game, assign the variable to an empty string and the loop stop.
Why doing a pause ?
If you remove this pause, et run the program; when looking your computer performances, you could see one of your CPU to work at 100% (or near). This is not a good situation because this can cause problems to another application running on your computer (mail management, instant messaging, etc.).
When we doing this small pause (50ms in our case) your computer will stop running a lot. This pause don't change our program et release some resources for the other applications. This is a good practice to try to apply this pause behavior.
Introduction Screen
This screen starting the game and ask if you want to start a new game or quit the game.
It is initialized by GotoIntroScreen
that cleaning the GraphicsWindow and drawing the messages, and then changes the game state to intro.
The loop is handle by GameIntroLoop
. This subroutine does nothing special, if 'Escape' key pressed the game state is setted to Empty to stop the game. Else if we pressed the bar space or 'Return', 'Enter' ou do a mouse click, we start a new game by changing the state to createboard by calling GotoCreateboardScreen
.
The Create Player Board Screen
This screen is used by the player to placing his ships in his board (ou Battlefield) before playing against the computer.
This screen is initialized by GotoCreateboardScreen
that cleaning the GraphicsWindow, display the title, display the board grid with the DrawBoard
subroutine (see below, the helpers subroutines), build the ships list with BuildShips
and create a shape for each ship to place on the battlefield. It draw the explain messages for the player. Then change the state to createboard. We use a shape for each ship because we can move the Shape in Small Basic.
The state loop is in the GameCreateboardLoop
subroutine. This loop handle some features :
- The current ship placement with the
currentShip
variable that contains the number of the ship in the player listships
. - The "cursor" which is the position where the player want to place the ship. This cursor is handle by the
currentCursor
variable : an array with the["x"]
index for the X position and the["y"]
for the Y position. We display the ship at the location where is the cursor to display the placement.
The Mouse
The mouse can be used for placing the ship, for this we compute the mouse position relative to position where is the board is draw, and to find the "cell" under the mouse. When we move the cursor.
To calculate the position in the board we need to convert the "pixel position" of the mouse to a "cell position" in the battlefield. See below in the "Technicals Used" for the calculation detail.
The Keyboard
The keyboard arrows keys are used to move the cursor. Depending of the button we will determine if we can move the cursor to the asked direction. According the direction we will prevent the ship go out the board.
Ship Rotation
Beyond moving, can also change the direction of the ship (with bar space or right click), in this case we remove the current form to replace it with a new form with new dimensions (cannot change the dimensions of a shape in Small Basic). Finally we check that the ship keep in the board, and move it if necessary.
Place a Ship
When the player is agree with is ship position, he place it by validate the position ('Enter' key ou left click). For this we call the ValidateShip
subroutine. This is explain in detail below in the helpers subroutines. If the ship position is valid, we save it in the ships
list, and we go to the next ship to place. When we save the position, we draw a rectangle to display to the player where he can't place another ship. This code calculate the bouding of ship in a rectangle and resize it to be drawn out of the grid.
If all the ships are placed, we goto the the computer battlefield generation with GotoBuildComputerShipsScreen
.
Computer Battlefield Generation Screen
Despite its name GotoBuildComputerShipScreen
does not handle a state. The creation of the computer battlefield does not require a game loop because there is no player action.
This screen display a waiting message, while the computer generate his board.
The method used to create the board is fairly simplistic but work pretty well. For each ship to place, we compute a random position. Then we call ValidateShip
as to validate the player ship placement. (see below for more detail on this subroutine).
If the ship has a valid position, we save it in the computer board computerShips
, else we restart the at the random position.
Potentially (if you haven't chance with random numbers) this technique can take some time to complete the table. In fact I am unable to a time more than a second. So I kept this simplistic algorithm.
When the computer board is generated, we run the game with GotoPlayScreen
.
Playing the Game
Initialization
GotoPlayScreen
initialize the game by creating the scores
variable which is a Small Basic array that will contain the score of each participant.
It draw the screen with titles, scores display and a board for each battlefield : one for the player where his ships are displayed with the computer moves, et another board where the player design his next move to find the computer ships. The it build the player ships (shapes placed in the board).
It create also a "cursor" to define where the player want to place his next move.
Finishing by define randomly the starting player.
Then go to state play.
Game loop
In the loop GamePlayLoop
, we check if it's the computer to play or the player. We used currentPlayer
variable containing "player" or "computer". If it's the computer turn we call ComputeNextMove
subroutine (see below for details). Else we manage the player action in the loop.
The management of the player action in this loop, works like the player battlefield creation : a cursor (green square) is movable by mouse or keyboard, we use the same method explains above in GameCreateboardLoop
. But the move validation is not the same.
When the player valids the move, we check if this move was never played. In this case, the move is rejected, so we waiting another move.
If the move is valid, we searching if it match a ship of the computer, we draw an information (a dot if it's in the water, a square if it's ship). We save the move (to check if we make the same move twice) in the playerGameState["moves"]
variables. So then :
- We calculate the player score
- We calculate the umber of the next turn
- If the player wins (find all ships), we call
GotoEndGame
to finish the game - If the move is on the water, it's at the computer to play (changes
currentPlayer
variable) - If the move find a ship, the player can make another move
Calculate the Next Computer Move
The next move calculation is done by the ComputeNextMove
subroutine.
In this version, the computer is not really intelligent ;) It make random move. To find the next move, we search a available move in a moves list. See "Technicals Used" for detail.
On the other hand it has a little bit of savvy, if he finds a boat he will 'exclude' the next impossible moves (the cells at the Northwest, Northeast, Southeast, Southwest) saving it as if he had played and drawing them on the grid of the player.
The rest works like for the player, scores calculation, check is wins, or who is the next player.
End of the Game
The end of the game is managed by GotoEndGame
which display titles et who wins by the scores
variable.
The loop GameEndLoop
just waiting the player press a key to go back to the introduction screen with the GotoIntroScreen
subroutine.
The Helpers Subroutines
More of subroutines are used to run recurring code loke DrawTitle
to draw title on each screen, SetShipShapeProperties
to define the graphics properties to draw the ships, or UpdateScores
to display the scores texts.
But we are more complex subroutines.
DrawBoard
This subroutine draw the grid for a battlefield (board). Because we can draw a grid at to position in the screen (the play screen contains one grid for the player and ne for the computer), before calling the subroutine we need to assign the draw position in the drawBoardX
and drawBoardY
variables to say to DrawBoard
where to draw the grid.
The rest of code is a loop to draw the lines of the grid and the headers.
ValidateShip
This subroutine is quite complicated. It used for check if a ship can be placed on a board based on a ships list.
It check if the ship is not out of board, don't intersect another ship nor be aside another ship.
To works this subroutine required "parameters", it's some variables that we need to assign before calling the subroutine:
validShipList
: Small Basic array containing the ships list to testvalidShip
: The ship number in the precedent listvalidPosX
: X position where we want to place the shipvalidPosY
: Y position where we want to place the shipvalidDir
: Direction where we want to place the ship
This subroutine checks if the position is valid et set the shipIsValid
variable with the result :
- True : The position is valid
- OutOfBoard : The position represents a ship out of the board
- OnAnotherShip : The position represents a ship that touch or intersect another ship
The first step of this subroutine est to compute the start and end position of the ship (in the sx1
, sy1
, sx2
, sy2
variables) to get a rectangle based on the direction.
Once the rectangle defined, we check if the ship is out of board.
Else we loop on each ship in the validShipList
list. If we have a ship that can be compared with the validShip
, we calculate a rectangle for it (variables ix1
, iy1
, ix2
, iy2
) too.
Then after we need to calculate if the two ships are intersected (rectangles intersection). These tests are generally done with 6 or 8 conditions (if I have a good memory) to check all cases. But in fact we can test the contrary (the two rectangles ARE NOT INTERSECTED) we can do it with only 4 conditions. So this is what we do. Particularity of these tests, we have some offsets ("-1"/"+1") in th tests, it's take care about the rule that two ships can't be side-by-size, with this offsets we simulate the bound around the ship more thin of one cell. I could calculate the rectangle directly, but I confess that I did not thought at the time, and now that the code is published I leave as well ;)
Caution: most of time I use the i
and j
variables in For
loop (it is a tradition of programmers), exceptionally I used the vi
variable in the For
loop of tis subroutine because it's called by GotoBuildComputerShipsScreen
from another For
loop using i
variable, therefor reused i
would cause a bug. We using a variable that is not used elsewhere.
Technicals Used
Convert Mouse Pixels Position to Cell Battlefield
Our battlefield is represented by a grid drawn in the GraphicsWindow. The purpose of this algorithm is to find the cell of the board that is located under the mouse pointer.
The first step to do is to calculate where is the mouse in the grid in pixels. If my grid is drawn to the position [100; 80], and my mouse is at [120; 97], then my mouse is located at position [20; 17] relative to the upper left corner of the grid. We get this position by subtracting the X position of the grid to the X position of the mouse in the the GraphicsWindow (GraphicsWindow.MouseX
), and on performs the same operation for the position Y (GraphicsWindow.MouseY
).
Once we have the position of the mouse "relative" to the grid, for get the cell under the mouse, simply divide the X position by the width of the cell (it rounded to the smallest integer) to obtain the column, and the Y position by the height of the cell (also rounded to the smallest integer) to obtain the line.
In fact if I have a cell width of 20 pixels:
- Mouse at 17 => (17/20) = 0.85 => Math.Floor (0.85) = column 0
- Mouse at 40 => (40/20) = 2.00 => Math.Floor (2.00) = column 2
In our case the width and height of a cell are the same and defined in the drawBoardCellSize
variable. Our calculation gets us a value starting with 0. But our grid have headers containing the numbers of the line and and the letters of the columns, therefore the position of the ships begin at 1 what corresponds to what we have in our lists, we do not need to make additional calculations.
For summary if battlefield is drawn from the position [100; 80] and that drawBoardCellSize
is equal to 10 pixels, if my mouse is located at [120; 97]:
- For X :
(120 – 100) => 20
20 / drawBoardCellSize => 2
Math.Foor(2) => 2
- For Y :
(97 – 80) => 17
17 / drawBoardCellSize => 1,7
Math.Foor(1,7) => 1
The cell under the mouse is [2;1].
Random search for the Computer Move
When the computer thinking his next move in ComputeNextMove
, it is done randomly, however we need to not find a played move.
For this the method used is simple, it determines the number of moves which are possible, IE the number of cells that have not yet been discovered. This number is the number of cells of the battlefield (10 x 10) minus the number of moves already played. We get a random number between 1 and the number of moves possible. In our case this is given by :
pos = Math.GetRandomNumber(100 - Array.GetItemCount(computerGameState["moves"])) - 1
Then we loop on all the cells of the battlefield and we decrement (substract 1) the random number (pos
in our case) each time we find a cell that is not discovered. When pos
reach 0 then we find our cell.
The particular point in the algorithm are these two lines:
move["x"] = 1 + Math.Remainder(i, 10)
move["y"] = 1 + Math.Floor(i / 10)
They allow to convert a linear position to a [X;Y] position (or [column;line] if you want) in a grid. With a 10x10 grid :
- Position 0 => [0;0]
- Position 5 => [5;0]
- Position 10 => [0;1]
- Position 37 => [7;3]
a linear position is neither more nor less than a vision of the flattened grid, putting each line one after another. To obtain the corresponding line divide the position by the width of the grid, all rounded to the integer. To get the corresponding column just get the remainder of the division of the position by the width of the grid. It is what these two lines do. We add 1 to these elements because our battlefield starts at 1 (not 0).
Join
I tried to be accurate in my explanations, however if this is not the case, do not hesitate to leave a comments or even to correct this article, the Wiki is made for this.
If you want to discuss about this program I recommend you to post messages in Small Basic forum rather than the comments the Wiki that serve more to ask or add details on the article itself. The discussion on the forum regarding this program: https://social.msdn.Microsoft.com/forums/en-us/1ab990d7-c6c3-49d6-Abba-693571262afa/my-new-game-Battleship-La-Bataille-Navale-frfr?Forum=SmallBASIC .