Simulating and Solving Number Merge Puzzles with mergeGridR
Source:vignettes/mergeGridR.Rmd
mergeGridR.RmdOverview
mergeGridR provides a generic falling-block number merge
puzzle as a Shiny interface and as a deterministic R engine. The Shiny
app is the interactive interface; the R functions are useful for tests,
reproducible play, autoplay experiments, and benchmarking.
Launch the app with:
For a browser-only copy that does not need Shiny or a live R session, export the standalone HTML app:
out <- file.path(tempdir(), "mergeGridR-static.html")
export_static_app(out, overwrite = TRUE)Use the Shiny app when you want the Rcpp-backed package engine, R objects, benchmarking, and package-level workflows. Use the static HTML app when you want a single local file for play or sharing on a machine with a modern WebGL-capable browser. Static-app high scores are browser-local and separated by preview horizon.
Puzzle Rules
The default board has five columns and seven rows. Row 1
is the bottom row. The preview horizon defaults to one tile, so only the
next provided tile is shown unless
game_config(next_count = ...) selects a larger horizon. On
each move, the next provided tile is dropped into a selected column and
lands in the lowest open row.
After landing, the engine checks the active tile. Any equal-value orthogonally connected component that includes the active tile merges when its size is at least two. If the active tile is stable, the engine then scans the board from bottom to top and left to right for by-product equal-value components created by gravity or earlier cascade steps. Those components also merge, one at a time, until the board is stable. The created value is:
#> component_size multiplier
#> 1 2 2
#> 2 3 4
#> 3 4 8
#> 4 5 16
For example, three 64 tiles merge into 256,
because 64 * 2^(3 - 1) = 256. The score increases by the
created tile value for every merge. Gravity is applied after each merge,
and cascades repeat from the newly created active tile before the next
board-wide by-product scan.
A game is over when no column has an open top cell. New provided
tiles are drawn from powers of two between 2 and the
minimum of the largest tile observed so far and
max_spawn_value, with a default cap of 256.
The spawn distribution can be weighted toward lower allowed values,
weighted toward higher allowed values, or uniform.
By default, a game has three manual continues. A continue is available only after game over, clears the top three rows, and does not change score, move count, queue, or random-number state. The largest observed tile is preserved for future spawn eligibility even if that tile was cleared.
In the Shiny app, the preview horizon can be changed before play starts. If it is changed after the first move, the current game keeps its active horizon and the selected value applies on restart.
Programmatic Play
Create a reproducible game with new_game():
state <- new_game(seed = 42)
state$board
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 0 0 0 0 0
#> [2,] 0 0 0 0 0
#> [3,] 0 0 0 0 0
#> [4,] 0 0 0 0 0
#> [5,] 0 0 0 0 0
#> [6,] 0 0 0 0 0
#> [7,] 0 0 0 0 0
state$next_tiles
#> [1] 2Use game_config(next_count = ...) to create a game with
a longer preview:
three_preview <- new_game(game_config(next_count = 3), seed = 42)
three_preview$next_tiles
#> [1] 2 2 2Drop the next tile into a one-based column:
state <- drop_tile(state, column = 3)
state$board
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 0 0 2 0 0
#> [2,] 0 0 0 0 0
#> [3,] 0 0 0 0 0
#> [4,] 0 0 0 0 0
#> [5,] 0 0 0 0 0
#> [6,] 0 0 0 0 0
#> [7,] 0 0 0 0 0
state$score
#> [1] 0
state$last_drop
#> $column
#> [1] 3
#>
#> $row
#> [1] 1
#>
#> $value
#> [1] 2
#>
#> $final_value
#> [1] 2
#>
#> $merges
#> [1] 0
#>
#> $created
#> integer(0)
#>
#> $component_sizes
#> integer(0)Continue a game-over state with continue_game():
state <- continue_game(state)
state$continues_remainingYou can also construct small states for reproducible rule checks.
This example creates a three-tile component of 2s:
state <- new_game(seed = 1)
state$board[1, 1] <- 2L
state$board[1, 2] <- 2L
state$next_tiles <- c(2L, 2L, 2L)
merged <- drop_tile(state, column = 1)
merged$board
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 8 0 0 0 0
#> [2,] 0 0 0 0 0
#> [3,] 0 0 0 0 0
#> [4,] 0 0 0 0 0
#> [5,] 0 0 0 0 0
#> [6,] 0 0 0 0 0
#> [7,] 0 0 0 0 0
merged$score
#> [1] 8
merged$last_drop[c("created", "component_sizes")]
#> $created
#> [1] 8
#>
#> $component_sizes
#> [1] 3Autoplay Strategies
autoplay_move() evaluates legal columns and returns the
recommended move without mutating the state.
move <- autoplay_move(
merged,
strategy = "growth_lookahead",
depth = 2,
beam_width = 5,
seed = 10
)
move[c("column", "strategy", "score_estimate")]
#> $column
#> [1] 2
#>
#> $strategy
#> [1] "growth_lookahead"
#>
#> $score_estimate
#> [1] 250.1
head(move$candidates)
#> column score_estimate score_gain heuristic depth simulations growth_bonus
#> 2 2 250.1 0 170.1 2 NA 0
#> 3 3 250.1 0 167.1 2 NA 0
#> 4 4 248.6 0 167.1 2 NA 0
#> 5 5 248.6 0 168.6 2 NA 0
#> 1 1 245.6 0 165.6 2 NA 0The available strategies are:
-
greedy: rank immediate outcomes using score gain and board-quality features. -
lookahead: depth-limited beam search using the base board-quality scorer. -
growth_lookahead: lookahead with an extra reward for three-or-more tile merges, especially when they create larger tiles. -
monte_carlo: average seeded rollout outcomes for each first move.
To play a full game automatically:
game <- autoplay_game(
strategy = "growth_lookahead",
max_moves = 1000,
depth = 3,
beam_width = 10,
seed = 1
)
game$final_state$score
game$historyBenchmarking
benchmark_autoplay_strategies() runs repeated seeded
games for a settings grid and returns both per-game runs and aggregate
summaries.
bench <- benchmark_autoplay_strategies(
n_games = 10,
max_moves = 200,
settings = autoplay_benchmark_settings("fast"),
seed = 20260609,
workers = 1
)
bench$summarySet workers > 1 to use base R PSOCK parallel workers.
Parallel mode requires the package to be installed in the library paths
visible to worker processes.
Local High Score
The Shiny app persists the best score locally under
tools::R_user_dir("mergeGridR", "cache"). Scores are
separated by preview horizon, so a one-tile preview and a three-tile
preview do not share the same best score.
get_high_score()
get_high_score(preview_horizon = 3)Call reset_high_score() explicitly when you want to
clear a local score file.