R
Shiny
数独

[R]{shiny}で数独を遊ぶWebアプリを作る

1. はじめに

Rで数独関連の処理を簡単に行なうことができる{sudoku}というパッケージがあります。

今回は,{shiny}の勉強がてらに{sudoku}を{shiny}上で動かしてみました。

今回作ったものはこちらです。
https://nigimitama.shinyapps.io/R_de_SUDOKU/

image.png
2つのタブから成っており,
1つ目のタブには①数独の問題の生成,②正解の表示,③生成した問題をcsvでダウンロード,④正解をcsvでダウンロード,といった機能を付けました。

image.png
2つ目のタブでは,数独の問題をcsvでアップロードすればそれを①表示し,②正解を表示(Rに解いてもらう),③正答をcsvでダウンロードできる,といった機能を付けました。

2. {sudoku}について

sudokuは数独の問題生成や解答を行ってくれる便利な関数が入ったパッケージです。

色々な関数があるのですが,現行バージョンのRではうまく動かないものもあります。

library(sudoku)
sudoku <- generateSudoku() # 問題を生成

> sudoku
      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9]
 [1,]    0    0    0    0    2    0    3    0    0
 [2,]    1    6    0    7    0    0    0    5    0
 [3,]    9    0    7    0    5    0    6    2    0
 [4,]    0    0    0    0    1    0    0    9    0
 [5,]    6    0    0    0    0    0    5    0    0
 [6,]    0    7    0    0    8    5    2    0    0
 [7,]    0    0    5    2    6    0    9    3    0
 [8,]    0    1    6    0    0    0    0    0    5
 [9,]    0    9    0    0    4    8    1    0    0

> solveSudoku(sudoku) # 問題を解く
      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9]
 [1,]    5    4    8    1    2    6    3    7    9
 [2,]    1    6    2    7    3    9    4    5    8
 [3,]    9    3    7    8    5    4    6    2    1
 [4,]    8    5    4    3    1    2    7    9    6
 [5,]    6    2    1    4    9    7    5    8    3
 [6,]    3    7    9    6    8    5    2    1    4
 [7,]    4    8    5    2    6    1    9    3    7
 [8,]    2    1    6    9    7    3    8    4    5
 [9,]    7    9    3    5    4    8    1    6    2

3. {shiny}で{sudoku}を動かす

ui.R
library(shiny)
library(shinydashboard)
library(shinyjs) # 非表示処理のためのshinyjs

# header
header <- dashboardHeader(title = "R de SUDOKU")

# sidebar
sidebar <- dashboardSidebar(disable = F,
                            sidebarMenu(
                              menuItem("Generate Puzzle", tabName = "GeneratePuzzle", icon = icon("th")),
                              menuItem("Upload Puzzle", tabName = "UploadPuzzle", icon = icon("upload"))
                            )
)

# body
body <- dashboardBody(
  useShinyjs(), # 非表示処理のためのshinyjs
  tabItems(
    # First tab content =========================================================================
    tabItem(tabName = "GeneratePuzzle",
            fluidRow(
              # main panel

              column(width = 8,
                     box(width = NULL, status = "warning", 
                         helpText("Puzzle"),
                         tableOutput("sudokuPuzzle")
                     ),
                     box(width = NULL, status = "warning", 
                         helpText("Answer (solve by R)"),
                         tableOutput('sudokuAnswer')
                     )
              ),
              # side panel
              column(width = 4,
                     box(width = NULL, solidHeader = TRUE, 
                         actionButton("generate", "Generate a New Puzzle")
                     ),
                     box(width = NULL, solidHeader = TRUE, 
                         actionButton("solve", "Show/Hide Answer")
                     ),
                     box(width = NULL, solidHeader = TRUE, 
                         helpText("Download sudoku matrix (.csv)"),
                         downloadButton("downloadPuzzle", "Download"),
                         downloadButton("downloadAnswer", "Download (Answer)")
                     )
              )


            )
    ),
    # Second tab content =========================================================================
    tabItem(tabName = "UploadPuzzle",
            fluidRow(
              # main panel
              column(width = 8,
                     box(width = NULL, 
                         helpText("Puzzle"),
                         tableOutput("sudokuPuzzleUploaded")
                     ),
                     box(width = NULL, 
                         helpText("Answer (solve by R)"),
                         tableOutput('sudokuAnswerUploaded')
                     )
              ),
              # side panel -----------------------------------------------
              column(width = 4,
                     box(width = NULL, solidHeader = TRUE, 
                         helpText("Upload sudoku matrix (.csv)"),
                         fileInput("uploadedFile", "Choose CSV File",
                                   accept = c("text/csv",
                                              "text/comma-separated-values,text/plain",
                                              ".csv"))
                     ),
                     box(width = NULL, solidHeader = TRUE,
                         actionButton("solveUploaded", "Show/Hide Answer")
                     ),
                     box(width = NULL, solidHeader = TRUE,
                         helpText("Download sudoku matrix (.csv)"),
                         downloadButton("downloadAnswerUploaded", "Download (Answer)")
                     )


              )
            )
    )
  )
)

# ページの構成
dashboardPage(
  header,
  sidebar,
  body
)
server.R
library(shiny)
library(shinydashboard)
library(sudoku)


# 数独のmatrixを見やすくするための処理を行なう関数 ---------------------------
SudokuForShow <- function(sudoku){
  # 問題に区切りの横棒を追加
  sudoku <- rbind(sudoku[1:3,], rep('―', ncol(sudoku)),
                  sudoku[4:6,], rep('―', ncol(sudoku)),
                  sudoku[7:9,]  )
  # 問題に区切りの縦棒を追加
  sudoku <- cbind(sudoku[,1:3], rep('|', nrow(sudoku)),
                  sudoku[,4:6], rep('|', nrow(sudoku)),
                  sudoku[,7:9]  )
  # 問題の0を削除
  sudoku <- gsub("0","",sudoku)
  # to DataFrame
  sudoku <- data.frame(sudoku)
  #colnames(sudoku) <- c((1:3),' ',(4:6),' ',(7:9)) # colnamesに数字を入れたい場合
  colnames(sudoku) <- rep(' ', ncol(sudoku))
  return(sudoku)
}



server <- function(input, output) {
  ### First tab ###
  # generate puzzle ===================================================
  # generate ---------------------------------------
  sudoku_raw <- reactive({
    # 実行のトリガーとなるinput
    input$generate
    # 数独の問題を生成
    generateSudoku()
  })
  # show --------------------------------------------
  output$sudokuPuzzle <- renderTable({
    SudokuForShow(sudoku_raw()) # print
    })


  # solve ============================================================
  # solve --------------------------------------------
  sudoku_solved <- reactive({
    solveSudoku(sudoku_raw(), verbose=F, print.it = F) # 数独の問題を解く
  })


  # print ---------------------------------------------
  output$sudokuAnswer <- renderTable({
    SudokuForShow(sudoku_solved()) # print
    })

  # 答えの非表示・表示処理 -----------------------------
  observeEvent(input$solve, {
    toggle("sudokuAnswer", anim = TRUE)
  }, ignoreNULL = FALSE)


  # Download csv ==================================================
  # Download Puzzle
  output$downloadPuzzle <- downloadHandler(
    filename = paste("sudoku", ".csv", sep = ""),
    content = function(file) {
      write.csv(sudoku_raw(), file, row.names = FALSE)
    }
  )
  # Download Answer
  output$downloadAnswer <- downloadHandler(
    filename = paste("sudokuAnswer", ".csv", sep = ""),
    content = function(file) {
      write.csv(sudoku_solved(), file, row.names = FALSE)
    }
  )


  ### Second tab ###
  # Upload csv ========================================================
  # upload -----------------------------------------
  sudoku_raw_Uploaded <- reactive({
    # 実行のトリガーとなるinput
    req(input$uploadedFile)
    # read
    sudokuUploaded <- read.csv(input$uploadedFile$datapath)
    # print
    as.matrix(sudokuUploaded)
  })

  # Show Puzzle -------------------------------------
  output$sudokuPuzzleUploaded <- renderTable({
    SudokuForShow(sudoku_raw_Uploaded()) # print
  })


  # solve ============================================================
  # solve --------------------------------------------
  sudoku_solved_Uploaded <- reactive({
    solveSudoku(sudoku_raw_Uploaded(), verbose=F, print.it = F) # 数独の問題を解く
  })


  # print ---------------------------------------------
  output$sudokuAnswerUploaded <- renderTable({
    SudokuForShow(sudoku_solved_Uploaded()) # print
  })

  # 答えの非表示・表示処理 -----------------------------
  observeEvent(input$solveUploaded, {
    toggle("sudokuAnswerUploaded", anim = TRUE)
  }, ignoreNULL = FALSE)


  # Download csv ====================================================
  # Download Answer
  output$downloadAnswerUploaded <- downloadHandler(
    filename = paste("sudokuAnswer", ".csv", sep = ""),
    content = function(file) {
      write.csv(sudoku_solved_Uploaded(), file, row.names = FALSE)
    }
  )

}

今回はじめて{shinyjs}というパッケージを知ったのですが,便利そうでした。

数独の正答を折りたたんだ状態にするために{shinyjs}のtoggle()を使ったのですが,anim = TRUEでヌルっとなめらかに開いてくれていい感じ。

おわりに

本当はShiny上で数独のマスに自分で入力できるようにしたりとか,よりゲーム性を高めたかったのですが,それは不可能そうでした。

(Shinyはデータ分析結果を表示するためのものですし,ゲームが作りたければ別の言語を使えって話ですよね。)

参考