---
title: "Simulating and Solving Number Merge Puzzles with mergeGridR"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Simulating and Solving Number Merge Puzzles with mergeGridR}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup}
library(mergeGridR)
```

## Overview

`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:

```{r launch, eval = FALSE}
run_drop_number()
```

For a browser-only copy that does not need Shiny or a live R session, export the
standalone HTML app:

```{r static-export, eval = FALSE}
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:

```{r merge-formula, echo = FALSE}
data.frame(
  component_size = 2:5,
  multiplier = 2 ^ ((2:5) - 1)
)
```

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()`:

```{r new-game}
state <- new_game(seed = 42)
state$board
state$next_tiles
```

Use `game_config(next_count = ...)` to create a game with a longer preview:

```{r preview-config}
three_preview <- new_game(game_config(next_count = 3), seed = 42)
three_preview$next_tiles
```

Drop the next tile into a one-based column:

```{r drop}
state <- drop_tile(state, column = 3)
state$board
state$score
state$last_drop
```

Continue a game-over state with `continue_game()`:

```{r continue-game, eval = FALSE}
state <- continue_game(state)
state$continues_remaining
```

You can also construct small states for reproducible rule checks. This example
creates a three-tile component of `2`s:

```{r three-tile-merge}
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
merged$score
merged$last_drop[c("created", "component_sizes")]
```

## Autoplay Strategies

`autoplay_move()` evaluates legal columns and returns the recommended move
without mutating the state.

```{r autoplay-move}
move <- autoplay_move(
  merged,
  strategy = "growth_lookahead",
  depth = 2,
  beam_width = 5,
  seed = 10
)

move[c("column", "strategy", "score_estimate")]
head(move$candidates)
```

The 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:

```{r autoplay-game, eval = FALSE}
game <- autoplay_game(
  strategy = "growth_lookahead",
  max_moves = 1000,
  depth = 3,
  beam_width = 10,
  seed = 1
)

game$final_state$score
game$history
```

## Benchmarking

`benchmark_autoplay_strategies()` runs repeated seeded games for a settings grid
and returns both per-game runs and aggregate summaries.

```{r benchmark, eval = FALSE}
bench <- benchmark_autoplay_strategies(
  n_games = 10,
  max_moves = 200,
  settings = autoplay_benchmark_settings("fast"),
  seed = 20260609,
  workers = 1
)

bench$summary
```

Set `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.

```{r high-score, eval = FALSE}
get_high_score()
get_high_score(preview_horizon = 3)
```

Call `reset_high_score()` explicitly when you want to clear a local score file.
