Skip to content

Instantly share code, notes, and snippets.

@jcrodriguez1989
Created October 30, 2020 14:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jcrodriguez1989/c5f080b918eb97d17f5edcdc6f26f56e to your computer and use it in GitHub Desktop.
Save jcrodriguez1989/c5f080b918eb97d17f5edcdc6f26f56e to your computer and use it in GitHub Desktop.
R's EmojiSweeper πŸ’£πŸ’₯
library("dplyr")
library("DT")
library("emojifont")
library("shiny")
library("tidyr")
# Icons to show on the board.
squaremoji <- emoji("black_square_button") # "πŸ”²"
numojis <- c("", emoji(c("one", "two", "three", "four", "five", "six", "seven", "eight", "nine")))
bombmoji <- emoji("bomb") # "πŸ’£"
exploji <- emoji("collision") # "πŸ’₯"
# Icons for the reset game button.
smiloji <- emoji("slightly_smiling_face") # "πŸ™‚"
deadmoji <- emoji("dizzy_face") # "😡"
wonmoji <- emoji("sunglasses") # "😎"
####### App's server functions.
# Creates the mines map (the hidden board). A cell with `-1` will be a mine.
# @param nrows the desired number of rows.
# @param ncols the desired number of columns.
# @param mines the desired number of mines.
make_board <- function(nrows = 9, ncols = 9, mines = 10) {
mines <- min(mines, ncols * nrows)
# Sample mines' position.
m <- rep(0, ncols * nrows)
m[sample(ncols * nrows, mines)] <- -10
mine_mat <- matrix(m, nrows, ncols) # Create mines board.
# Find mines and calculate neighbors.
mine_cells <- which(mine_mat < 0, arr.ind = TRUE)
for (i in seq_len(mines)) {
mrow <- intersect(1:nrows, (mine_cells[i, 1] - 1):(mine_cells[i, 1] + 1))
mcol <- intersect(1:ncols, (mine_cells[i, 2] - 1):(mine_cells[i, 2] + 1))
mine_mat[mrow, mcol] <- mine_mat[mrow, mcol] + 1
}
ifelse(mine_mat < 0, -1, mine_mat) # Return mines as `-1`.
}
# Discovers in the `showing_board`, the click's nearby un-mined cells.
# @param click list with the `row` and `col` that was clicked.
# @param showing_board matrix with the actually shown board (to be updated).
# @param hidden_board matrix with the mines map.
discover_board <- function(click, showing_board, hidden_board) {
dims <- dim(showing_board)
# Inspect all 9 neighbors (less if we are on the border of the board).
neighbors <- expand_grid(row = click$row + (-1:1), col = click$col + (-1:1)) %>%
filter(1 <= row & row <= dims[1] & 1 <= col & col <= dims[2])
# Continue while there are neighbors to inspect.
while (nrow(neighbors) > 0) {
new_neighbors <- tibble(row = numeric(), col = numeric()) # Clear the new neighbors to explore.
for (i in seq_len(nrow(neighbors))) {
# Get the current cell to explore.
act_click <- neighbors[i, ]
act_value <- hidden_board[act_click$row, act_click$col]
if (act_value < 0) next # If the cell is a mine, then don't discover it.
showing_board[act_click$row, act_click$col] <- numojis[[act_value + 1]] # Discover the cell.
if (act_value == 0) {
# If it is not neighbor of a mine, then we should keep discovering neighbors.
new_neighbors <- bind_rows(
new_neighbors,
expand_grid(row = act_click$row + (-1:1), col = act_click$col + (-1:1))
)
}
}
# Update the neighbors to explore.
neighbors <- distinct(new_neighbors, row, col) %>% # Remove duplicates.
filter(1 <= row & row <= dims[1] & 1 <= col & col <= dims[2]) %>% # Out of board cells.
rowwise() %>%
filter(showing_board[row, col] == squaremoji) # Already discovered cells.
}
showing_board
}
# Updates the `showing_board`, depending on the `click`.
# @param click list with the `row` and `col` that was clicked.
# @param showing_board matrix with the actually shown board (to be updated).
# @param hidden_board matrix with the mines map.
click_board <- function(click, showing_board, hidden_board) {
click$col <- click$col + 1 # It starts at 0, let's start it at 1.
if (hidden_board[click$row, click$col] < 0) {
showing_board[click$row, click$col] <- exploji # If there was a mine, show the explosion.
} else {
# If not, discover nearby un-mined cells.
showing_board <- discover_board(click, showing_board, hidden_board)
}
showing_board
}
####### EmojiSweeper App settings.
# Set the UI.
emojisweeper_UI <- fluidPage(
titlePanel(paste0("EmojiSweeper ", bombmoji, exploji)),
sidebarLayout(
sidebarPanel(
fluidRow(
column(4, align = "left", verbatimTextOutput("n_mines")),
column(4, align = "center", actionButton("reset_game", label = smiloji)),
column(4, align = "right", verbatimTextOutput("timer"))
),
sliderInput("nrows", "Number of rows:", min = 1, max = 16, value = 9),
sliderInput("ncols", "Number of columns:", min = 1, max = 30, value = 9),
sliderInput("mines", "Number of mines:", min = 0, max = 30 * 16, value = 10)
),
mainPanel(DT::dataTableOutput("board", width = 1, height = 1))
)
)
# Set the server.
emojisweeper_server <- function(input, output, session) {
hidden_board <- reactiveVal() # Invisible board, with the positions of mines.
showing_board <- reactiveVal() # The board that is being showed.
playing <- reactiveVal(TRUE) # Indicates if the game is active or not.
seconds_timer <- reactiveTimer(1000) # Every one second, trigger the event.
seconds <- reactiveVal(0) # Current game seconds counter.
observeEvent(seconds_timer(), if (playing()) seconds(seconds() + 1)) # Update the playing secs.
output$timer <- renderText(seconds()) # Show the playing secs.
output$n_mines <- renderText(input$mines) # Update the number of active mines.
# If any of the inputs changed, we have to update the boards, and reset the game.
observeEvent(
input$nrows + input$ncols + input$mines + input$reset_game,
{
hidden_board(make_board(input$nrows, input$ncols, input$mines))
showing_board(matrix(rep(squaremoji, input$nrows * input$ncols), ncol = input$ncols))
playing(TRUE)
seconds(0) # Reset the playing time.
updateActionButton(session, "reset_game", label = smiloji)
}
)
output$board <- DT::renderDataTable(
DT::datatable(
showing_board(),
colnames = rep("", input$ncols), # No column names.
selection = "none", # Disable cells selection.
options = list(
dom = "", # Hide DT additional UI.
ordering = FALSE, # Disable table reordering buttons.
pageLength = input$nrows, # Show all the rows.
# Center the text into each cell.
columnDefs = list(list(className = "dt-center", targets = "_all"))
)
),
server = FALSE # Execute computations on the users device.
)
observeEvent(input$board_cell_clicked, {
click <- input$board_cell_clicked
# If the click was not on a button, or the game is over, then skip.
if (length(click) == 0 || click$value != squaremoji || !playing()) {
return()
}
# Get the new board caused by this click.
new_board <- click_board(click, showing_board(), hidden_board())
# If an explosion happened, or only mines are remaining, then the game ended.
if (exploji %in% new_board || all(hidden_board()[new_board == squaremoji] < 0)) {
new_board[new_board != exploji & hidden_board() < 0] <- bombmoji # Show the remaining mines.
playing(FALSE) # End the game.
# Update the face icon depending on the result.
if (exploji %in% new_board) {
updateActionButton(session, "reset_game", label = deadmoji)
} else {
updateActionButton(session, "reset_game", label = wonmoji)
}
}
showing_board(new_board) # Update the showing board.
})
}
# Run the app!
shinyApp(emojisweeper_UI, emojisweeper_server)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment