Build and Play a Sliding Puzzle Game with Natural Touch Gestures
Sliding puzzles have stood the test of time. They may seem simple, but the deeper you go, the more they challenge your brain. One of the most iconic versions is the 15 Puzzle, and in this post, we're going to unpack how it works, why it's still fun today, and how you can create your own version that feels great to play—especially with modern touch controls like tap and drag.
🔎 What Exactly is the 15 Puzzle?
At its core, the 15 Puzzle is a square board made up of 16 spaces, where 15 of them are filled with numbered tiles, and one spot is left blank. The objective is to slide the tiles around using that one blank space until the numbers are in perfect sequence, from 1 to 15.
It starts out shuffled, and each move slides a tile into the empty space. The magic lies in how each move affects the puzzle—there’s no way to solve it randomly; it takes logic, planning, and patience.
🔥 Why People Still Enjoy It in the Age of Touchscreens
You’d think a puzzle over 100 years old would fade away, but it’s still just as fun. Here’s what keeps it relevant:
- It’s Instantly Understandable – You don’t need instructions. Just start sliding.
- It’s Deceptively Challenging – You may think you're close, then realize you're not.
- It’s Travel-Friendly – No timer, no stress. Just pick it up whenever.
- It Feels Natural on Phones – Touchscreens make sliding tiles smooth and satisfying.
🚀 Features That Make a Sliding Puzzle App Feel Modern
To truly enjoy this game on a mobile device, it should go beyond basic mechanics. Here are a few must-have elements:
Tap or Drag to Move
Some players prefer tapping; others like dragging. Supporting both lets players interact in the way that feels best for them.
Switch Between Numbers and Images
Instead of just numbers, players could try solving puzzles made from sliced-up images—whether it’s a photo, a landscape, or custom art.
Visual Smoothness
The tiles should move with polish—snapping into place, animating cleanly, and giving just the right amount of feedback.
Shuffle and Restart Anytime
Starting fresh should be easy. A good puzzle app makes restarting and reshuffling just a single tap away.
Winning Feedback
Recognizing when a player solves the puzzle and showing a celebration or message adds closure and reward.
🖐 How Tap and Drag Controls Work in a Sliding Puzzle
✔ Tap-to-Move
This is the simplest and most familiar approach. A player taps a tile, and if it’s directly next to the blank space, it slides in. It’s fast and ideal for quick play.
✔ Drag-to-Move
Here, a tile is pressed and physically dragged toward the empty space. The game checks if the movement is valid—only allowing a drag if the tile is in the same row or column and can slide into the gap. It’s a bit more tactile and can feel more satisfying on touchscreens.
Combining both methods in one app gives players flexibility, making the puzzle feel more responsive and interactive.
🌟 Bonus Ideas to Make Your Game Stand Out
If you're planning to create your own version, here are a few extra touches to make it feel fresh:
Upload Your Own Photos
Let users turn their selfies or favorite pictures into a puzzle grid.
Track Moves and Time
Add a counter and a timer so players can challenge themselves to beat their personal best.
Themed Designs
Use seasonal graphics, color themes, or even animated tiles to give your app a unique look.
Sound and Vibration
Subtle audio cues and gentle vibrations can make every move feel more interactive.
Leaderboards and Challenges
Integrate online rankings or daily puzzles to give players a reason to come back regularly.
Main Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
CarWalaTheme {
/*Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}*/
SlidingPuzzle()
}
}
}
}
Sliding Puzzle
@Composable
fun SlidingPuzzle() {
var tiles by remember { mutableStateOf(shuffleTiles()) }
var showWinDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
val bestMoves = remember { mutableIntStateOf(ScoreManager.getBestMoves(context)) }
var currentMoves by remember { mutableIntStateOf(0) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("By: www.codingbihar.com", fontSize = 18.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
Text(text = "Moves: $currentMoves", fontSize = 18.sp)
Text(text = "Best: ${bestMoves.intValue}", fontSize = 18.sp)
Spacer(modifier = Modifier.height(16.dp))
for (row in 0 until 4) {
Row {
for (col in 0 until 4) {
val index = row * 4 + col
val tile = tiles[index]
Box(
modifier = Modifier
.size(80.dp)
.padding(4.dp)
.background(
if (tile == 0) Color.Gray else Color.Blue,
RoundedCornerShape(8.dp)
)
.clickable {
val updated = moveTile(tiles, index)
if (tiles != updated) {
currentMoves++
tiles = updated
if (isPuzzleSolved(updated)) {
ScoreManager.saveBestMoves(context, currentMoves)
bestMoves.intValue = ScoreManager.getBestMoves(context)
showWinDialog = true
}
}
},
contentAlignment = Alignment.Center
) {
if (tile != 0) {
Text(
text = tile.toString(),
fontSize = 24.sp,
color = Color.White
)
}
}
}
}
}
// 🎉 Win Dialog
if (showWinDialog) {
AlertDialog(
onDismissRequest = { showWinDialog = false },
confirmButton = {
TextButton(onClick = {
tiles = shuffleTiles()
currentMoves = 0
showWinDialog = false
}) {
Text("Play Again")
}
},
title = { Text("You Win! 🎉") },
text = { Text("Puzzle Solved Successfully!") }
)
}
}
}
fun shuffleTiles(): List<Int> {
val list = (1..15).toMutableList() + 0
return list.shuffled()
}
fun moveTile(tiles: List<Int>, index: Int): List<Int> {
val blankIndex = tiles.indexOf(0)
val row = index / 4
val col = index % 4
val blankRow = blankIndex / 4
val blankCol = blankIndex % 4
val isAdjacent = (row == blankRow && (col - blankCol).absoluteValue == 1) ||
(col == blankCol && (row - blankRow).absoluteValue == 1)
return if (isAdjacent) {
tiles.toMutableList().apply {
this[blankIndex] = tiles[index]
this[index] = 0
}
} else tiles
}
fun isPuzzleSolved(tiles: List<Int>): Boolean {
return tiles == (1..15).toList() + 0
}
object ScoreManager {
private const val PREF_NAME = "puzzle_prefs"
private const val KEY_BEST_MOVES = "best_moves"
fun saveBestMoves(context: Context, moves: Int) {
val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
val currentBest = prefs.getInt(KEY_BEST_MOVES, Int.MAX_VALUE)
if (moves < currentBest) {
prefs.edit { putInt(KEY_BEST_MOVES, moves) }
}
}
fun getBestMoves(context: Context): Int {
val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
return prefs.getInt(KEY_BEST_MOVES, 0)
}
}