Battleship R package

Introduction

For my project, I wanted to re-create the idea behind the board game Battleship.

Battleship is a guessing game where two players have a fleet of ships on two boards. The players’ boards are hidden from each other so that one cannot see where the other’s fleet are positioned.

The board is usually a 10x10 grid like so:

Here the gray rectangles represent the ships on the board.

Taking alternate turns, each player then guesses a square. The other player then tells them whether the guess was correct or incorrect. The game ends when the entire fleet has been destroyed.

I wanted to recreate this idea by having a randomly generated ship be hidden onto a board of nearly any size. Then the person playing the game can guess where the ship is.

Downloading the package

I decided to call my package battleship. It is on Github and can be downloaded with the following command:

devtools::install_github("pshuwei/battleship")

Creating the function

I made the whole game a function called play.

play <- function(rows, columns) {

  stopifnot(is.numeric(rows), is.numeric(columns), rows >1, columns >1, columns <= 26)

This part of the function allowed the user to create a board almost of their choosing. However, the function would not proceed under if:

  1. the rows and columns were not numeric

  2. if the rows and columns were a negative number

  3. if the number of columns were greater than 26

    my.rows <- rows

    my.cols <- columns

    cat(“Welcome to Battleship!”, “”)

    cat(“You have decided to have a board size of”, my.rows, “rows and”, my.cols, “columns.”, “”)

From here, the function would then just assign the number of rows as my.rows and columns as my.cols. It would then welcome the user to the game and reiterate the size of the board from the number of rows and columns that they chose.

Creating the board

To create the board with the number of rows and columns, I wrote the following lines:

   bracket <- "[ ]"

   board <- matrix(NA, nrow = my.rows, ncol = my.cols)
   colnames(board) <- LETTERS[1:my.cols]
   rownames(board) <- 1:my.rows
   
   #making the board
   for (i in 1:my.rows) {
     for (j in 1:my.cols) {
   board[i,j] <- bracket
      }
   }

   print(board, quote = FALSE)

The board would start out as an empty m x n matrix, which then would be replaced by [ ] for the number of rows and columns. It would name the columns by letter and number the rows.

So for example, making a 5x5 board:

my.rows <- 5
  
my.cols <- 5
  
bracket <- "[ ]"
  
board <- matrix(NA, nrow = my.rows, ncol = my.cols)
colnames(board) <- LETTERS[1:my.cols]
rownames(board) <- 1:my.rows
  
#making the board
for (i in 1:my.rows) {
   for (j in 1:my.cols) {
     board[i,j] <- bracket
   }
}

print(board, quote = FALSE)
##   A   B   C   D   E  
## 1 [ ] [ ] [ ] [ ] [ ]
## 2 [ ] [ ] [ ] [ ] [ ]
## 3 [ ] [ ] [ ] [ ] [ ]
## 4 [ ] [ ] [ ] [ ] [ ]
## 5 [ ] [ ] [ ] [ ] [ ]

So here we created the board that we requested.

Creating the ship size

The next part was creating the ship. In the game, the smallest ship is usually 2 units long. For this game, I only wanted the ship to be 2 or 3 units long.

However, I also had to keep in the mind the size of the board. If the user requested only a 2x2 board, then it is impossible to have a ship size of 3. So the following lines account for this.

Note: I am setting the seed for this report. In the actual function, everything is randomized.

set.seed(123)

if (nrow(board) < 3 | ncol(board) < 3 ) { 
  shipsize <- 2
} else{
  shipsize <- sample(2:3, 1)
}

If the number of rows or columns were less than 3, then it only created a ship that is 2 units long. On the other than, it would randomly decide to create a ship the size of 2 of 3 units.

Using our example of our 5x5 board:

shipsize
## [1] 2

So we created a ship size of 2.

Creating the ship location on the board

Now I had to create the coordinates of the ship.

To do this I randomly generated the coordinates of the first part of the ship.

set.seed(123)

shiprow <- sample(1:my.rows, 1)
shipcol <- sample(LETTERS[1:my.cols], 1)

These lines of code would generate a row number and column letter for the first part of the ship.

Continuing from our example:

print(c(shiprow, shipcol))
## [1] "3" "C"

So our fist ship coordinates are C3.

Creating the ship direction

The next lines of code would decide in what direction the next parts of the ship would be generated:

set.seed(123)

  direction <- c("left", "right", "up", "down")
  
  #randomly deciding how to extend the ship in what direction
  if (shipcol == "A" & shiprow == 1) { #topleft corner
    direction2 <- sample(direction[-c(1,3)], 1)
  } else if (shipcol == LETTERS[my.cols] & shiprow == my.rows) { #bottomright
    direction2 <- sample(direction[-c(2,4)], 1)
  } else if (shipcol == "A" & shiprow == my.rows) { #bottomleft
    direction2 <- sample(direction[-c(1,4)], 1)
  } else if (shipcol == LETTERS[my.cols] & shiprow == 1) { #topright
    direction2 <- sample(direction[-c(2,3)], 1)
  } else if (shipcol == "A") {
    direction2 <- sample(direction[-1], 1)
  } else if (shipcol == LETTERS[my.cols]) {
    direction2 <- sample(direction[-2], 1)
  } else if (shiprow == 1) {
    direction2 <- sample(direction[-3], 1)
  } else if (shiprow == my.rows) {
    direction2 <- sample(direction[-4], 1)
  } else {
    direction2 <- sample(direction, 1)
  }

I labeled direction as a list of left, right, up, and down. Then I randomly generated from those elements and made the next part of ship with the coordinates based on the direction.

So for example, if the first part of the ship was located on coordinates B2, and the direction sampled left, then the next part of the ship would be located on coordinates A2.

However, there were some conditions that needed to be met:

  • if the first coordinates were located in the top left hand corner (A1), then the direction for the next part of the ship could not go left or up.

  • if the first coordinates were located in the top right hand corner ([last letter]1), then the direction for the next part of the ship could not go right or up.

  • if the first coordinates were located in the bottom left hand corner (A[last number]), then the direction for the next part of the ship could not go left or down.

  • if the first coordinates were located in the bottom left hand corner ([last letter][last number]), then the direction for the next part of the ship could not go right or down.

  • if the first coordinates were located in the first column, then the direction could not be left.

  • if the first coordinates were located in the last column, then then direction could not be right.

  • if the first coordinates were on the first row, then the direction could not be up.

  • if the first coordinates were on the last row, then the direction could not be down.]

Let’s use our example again:

direction2
## [1] "up"

Here our example has decided that the direction for the second coordinate of the ship would be above C3, so it should be C2.

Creating the second and/or third coordinates of the ship

To create the second and/or third part of the ship, I made the following code:

set.seed(123)

  #creating next part(s) of ship based on shipsize
  if (direction2 == 'left') {
    shipcol2 = LETTERS[which(LETTERS == shipcol) - 1]
    shiprow2 = shiprow
    if (shipsize == 3 & shipcol2 == 'A') {
      shipcol3 = LETTERS[which(LETTERS == shipcol) + 1]
      shiprow3 = shiprow
    } else if (shipsize == 3) {
      shipcol3 = LETTERS[which(LETTERS == shipcol2) - 1]
      shiprow3 = shiprow
    }
  } else if (direction2 == 'right') {
    shipcol2 = LETTERS[which(LETTERS == shipcol) + 1]
    shiprow2 = shiprow
    if (shipsize == 3 & shipcol2 == LETTERS[my.cols]) {
      shipcol3 = LETTERS[which(LETTERS == shipcol) - 1]
      shiprow3 = shiprow
    } else if (shipsize == 3) {
      shipcol3 = LETTERS[which(LETTERS == shipcol2) + 1]
      shiprow3 = shiprow
    }
  } else if (direction2 == 'up') {
    shipcol2 = shipcol
    shiprow2 = shiprow - 1
    if (shipsize == 3 & shiprow2 == 1) {
      shipcol3 = shipcol
      shiprow3 = shiprow - 1
    } else if (shipsize == 3) {
      shipcol3 = shipcol
      shiprow3 = shiprow + 1
    }
  } else if (direction2 == 'down') {
    shipcol2 = shipcol
    shiprow2 = shiprow + 1
    if (shipsize == 3 & shiprow2 == 1) {
      shipcol3 = shipcol
      shiprow3 = shiprow + 1
    } else if (shipsize == 3) {
      shipcol3 = shipcol
      shiprow3 = shiprow - 1
    }
  }

So then depending the direction, it would make the second and third coordinates, based on the size of the ship.

If the ship size was 3 units, then there are some conditions that need to be met:

  • If the direction was left and the first ship coordinate was on the second column, then the third coordinate of the ship will be on the third column, since it is impossible to be on a column left of the first.

  • If the direction was right and the first ship coordinate was the second-to-last column, then the third coordinate would be located on the third-the-last column.

  • If the direction was up and the first ship coordinate was on the second row, then the third coordinate would be on the third row.

  • If the direction was down and the first ship coordinate was on the second-t0-last row, then the third coordinate would be on the third-to-last row.

So for example, if the first ship coordinate was on B3 and the direction was left, then the second coordinate would be A3. But since it is impossible to be left of A, then the third coordinate would be C3.

Back to our example:

print(c(shiprow2, shipcol2))
## [1] "2" "C"

So here we can see that our second coordinate of the ship is C2, as mentioned earlier. Since our ship size is only 2, there is no third coordinate.

During the game, the user cannot see where the ship coordinates are. The user will have to guess it.

Creating the game

At this point in the function is the start of the game. I first create a value called parts_left to contain the value of the ship parts that have not been destroyed. The whole game is in a while loop that does not break until all parts have been destroyed.

parts_left = shipsize

parts_left
## [1] 2

This part of the report will discuss the parts of the while loop.

Creating the guesses

The following lines prompted the user to enter their guesses:

my.colguess <- "whatever"
outside <- TRUE
options(warn=-1)
while ((my.colguess %in% LETTERS) == F | outside == TRUE) {
  my.colguess <- toupper(readline(prompt="Enter column letter: "))
  if ((my.colguess %in% LETTERS) == F) {
    cat("That's not a letter!", "\n")
  } else if (which(LETTERS == my.colguess) > my.cols) {
    cat("Your letter is outside the board!")
  } else {
    outside <- FALSE
  }

}
options(warn=0)

my.rowguess <- "whatever"
options(warn=-1)
while(is.na(my.rowguess)|!is.numeric(my.rowguess) | my.rowguess > my.rows) {
  my.rowguess <- as.numeric(readline(prompt = "Enter row number:"))
  if (is.na(my.rowguess)|!is.numeric(my.rowguess)) {
    cat("That's not a number!")
  } else if (my.rowguess > my.rows) {
    cat("Your guess is outside the board!")
  }
}
options(warn=0)

The purpose of this code was to account for possible errors in the input. In my.colguess, they could only enter a letter. If anything else, such nothing or a number, it would notify the user that column guess was a not a letter and then it would repeat the input question.This is similar for my.rowguess, where the user can only enter a number.

For the next section, let us put in a wrong guess:

my.colguess <- "A"
my.rowguess <- 4

Since the ship is located on C2 and C3, A4 will be a miss on the board.

Creating the display for guesses

The display is shown in a if...else statement. If the guess is correct, then a [O] appeared on the board, notifying the user that they have made a correct guess. The user should then make more guesses near the area. Then parts_left would decrease by 1.

If the guess is incorrect, then a [X] appeared, notifying the user that the guess was incorrect and they should look elsewhere.

If the parts_left was not 0, then the while loop would repeat and more guesses would be made until all parts of the ship were destroyed.

So using our incorrect guess:

    if (shipsize == 2) {
      if ((my.colguess == shipcol & my.rowguess == shiprow) |
          (my.colguess == shipcol2 & my.rowguess == shiprow2)) {
        cat("Nice! You got part of the ship!", "\n")
        board[my.rowguess,my.colguess] <- "[O]"
        print(board, quote = FALSE)
        parts_left = parts_left - 1
        cat("There are", parts_left, "parts left!", "\n")

      } else {
        cat("Aw you missed!", "\n")
        board[my.rowguess,my.colguess] <- "[X]"
        print(board, quote = FALSE)
        cat("There are still", parts_left, "parts left!", "\n")
      }

    } else if (shipsize == 3) {
      if ((my.colguess == shipcol & my.rowguess == shiprow) |
          (my.colguess == shipcol2 & my.rowguess == shiprow2) |
          (my.colguess == shipcol3 & my.rowguess == shiprow3)) {
        cat("Nice! You got part of the ship!", "\n")
        board[my.rowguess,my.colguess] <- "[O]"
        print(board, quote = FALSE)
        parts_left = parts_left - 1
        cat("There are", parts_left, "parts left!", "\n")

      } else {
        cat("Aw you missed!", "\n")
        board[my.rowguess,my.colguess] <- "[X]"
        print(board, quote = FALSE)
        cat("There are still", parts_left, "parts left!", "\n")
      }

    }
## Aw you missed! 
##   A   B   C   D   E  
## 1 [ ] [ ] [ ] [ ] [ ]
## 2 [ ] [ ] [ ] [ ] [ ]
## 3 [ ] [ ] [ ] [ ] [ ]
## 4 [X] [ ] [ ] [ ] [ ]
## 5 [ ] [ ] [ ] [ ] [ ]
## There are still 2 parts left!

We can see that we missed and a [X] has appeared on the board.

Now if we make a correct guess such as C3:

my.colguess <- "C"
my.rowguess <- 3
    if (shipsize == 2) {
      if ((my.colguess == shipcol & my.rowguess == shiprow) |
          (my.colguess == shipcol2 & my.rowguess == shiprow2)) {
        cat("Nice! You got part of the ship!", "\n")
        board[my.rowguess,my.colguess] <- "[O]"
        print(board, quote = FALSE)
        parts_left = parts_left - 1
        cat("There are", parts_left, "parts left!", "\n")

      } else {
        cat("Aw you missed!", "\n")
        board[my.rowguess,my.colguess] <- "[X]"
        print(board, quote = FALSE)
        cat("There are still", parts_left, "parts left!", "\n")
      }

    } else if (shipsize == 3) {
      if ((my.colguess == shipcol & my.rowguess == shiprow) |
          (my.colguess == shipcol2 & my.rowguess == shiprow2) |
          (my.colguess == shipcol3 & my.rowguess == shiprow3)) {
        cat("Nice! You got part of the ship!", "\n")
        board[my.rowguess,my.colguess] <- "[O]"
        print(board, quote = FALSE)
        parts_left = parts_left - 1
        cat("There are", parts_left, "parts left!", "\n")

      } else {
        cat("Aw you missed!", "\n")
        board[my.rowguess,my.colguess] <- "[X]"
        print(board, quote = FALSE)
        cat("There are still", parts_left, "parts left!", "\n")
      }

    }
## Nice! You got part of the ship! 
##   A   B   C   D   E  
## 1 [ ] [ ] [ ] [ ] [ ]
## 2 [ ] [ ] [ ] [ ] [ ]
## 3 [ ] [ ] [O] [ ] [ ]
## 4 [X] [ ] [ ] [ ] [ ]
## 5 [ ] [ ] [ ] [ ] [ ]
## There are 1 parts left!

Now a [O] appears on the board, so now the user knows they have made a correct guess. parts_left has also decreased by 1.

Creating end of the game

Once the part_left became 0, then all parts of the ship were destroyed and the game would notify the user that they had won.

if (parts_left == 0) {
  cat("Congrats you win!", "\n")
}

This would end the play function and end the game.

Discussion

Overall, I am pleased with how the game works. By randomly generating one ship, the game works surprisingly well.

There are other things can be done for future work. One aspect is more ships. Since in the real game there are 5 ships on a 10x10 board, more work can be done to add more ships. This would involve generating non-repeating coordinates so that no two ships can occupy the same spot.

Another aspect is converting this into a Shiny app. Since the game is played on the Rstudio console, it looks very simple. With Shiny, it’s possible to make the game more fluid and complete.

Probably the most ambitious and probably hardest thing that can improve this program is creating a user vs computer game. This would simulate the actual board where the user and computer try to destroy the other’s fleet first. However, I’m not sure if this kind of discussion has been taught in our class.

Updated: