Created
October 30, 2020 14:24
-
-
Save jcrodriguez1989/c5f080b918eb97d17f5edcdc6f26f56e to your computer and use it in GitHub Desktop.
R's EmojiSweeper π£π₯
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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