Build and Play a Sliding Puzzle Game with Natural Touch Gestures
π What Exactly is the 15 Puzzle?
π₯ Why People Still Enjoy It in the Age of Touchscreens
- 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
π How Tap and Drag Controls Work in a Sliding Puzzle
π Bonus Ideas to Make Your Game Stand Out
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)
}
}


