Introduction to 1024
The 1024 game is a single-player game where the player slides numbered tiles on a grid (usually of size 4x4) to combine them in order to get a value of 1024. By sliding the tiles, all adjacent two tiles of the same value add up creating a new tile double their values. The best (and easy) way to understand the game is to play it online.
The following illustrations show several cases of how the numbers are merged (if possible). Each row shows:
- The initial board configuration (the leftmost)
- The next board configuration after the first swipe (the center)
- The next board configuration after the second swipe (the rightmost)
Game Setup and Rules
The default board size is 4x4
The board is initialized with a number 2 randomly placed in an empty cell
When a swipe action on the screen causes the board to change, a new random number is placed at a random blank spot. The subsequent random numbers can be a 2 or randomly selected from a power of 2 candidates {1, 2, 4, ...}.
WARNING
When a swipe does not change the board, no new random number shall be generated
The user won the game when one the cells hits the target sum (default to 1024). But your app should allow the player to change the target sum to a lower number. This feature is useful during your testing phase (and also for instructor grading).
The user lost the game when the board is full and none of the numbers can be merged
Starter Code
The starter code for this assignment has been posted on GitHub Classroom
- Don't use this old Link:
https://classroom.github.com/a/dyz0rsEN
- Use this new Link Z34JYDjp
When you accept the GH classroom invitation link, you will be prompted for a team name.
IMPORTANT
Please include your GVSU userid in the team name, so your instructor can easily identify your work.
Android Studio Code Assist
As you add new widgets (Button, layout, etc.), the compiler may trigger errors due to missing imports. It is highly recommended that you depend on Android Studio code assist to fix them. By hovering your mouse on the error will, most of the time, the Android Studio code assistance will show hints how to fix the error. Use this to your advantage.
Running the Starter Code
After accepting the assignment on GH classroom, you can clone your GitHub repo to your local machine and open it in Android Studio. The starter code is designed with a minimum feature of handling swipe gestures in all four directions. It also comes with a simple (and incomplete) ViewModel that includes a LiveData
to notify the UI for the game logic internal changes; swiping in each direction will update all the cells to show different character.
The following screenshot shows the game board after the user swiped to the right.

Understanding the Starter Code
The file MainActivity.kt
includes two @Composable
functions:
Game1024
is the main page of the game app, the overall structure of this function shows three widgets stacked vertically. On the screen they appear top to bottom in order.ktColumn { Text("Welcome....") NumberGrid(...) Text("You swipe ....") }
NumberGrid
is the function that (obviously) renders the N-by-N grid of cells.
Handling Swipe Gestures
To handle swipe actions we attach a "drag gesture listener" to the Column
widget above as shown in the following snippet:
Column(modifier
.pointerInput(Unit) { // the arg "Unit" can be replaced with "null"
handleSwipe(this) {
vm.doSwipe(it) // invoke the viewmodel function
}
})
The above snippet may require an extra explanation to make sense. So, I will unpack the snippet "line-by-line"
The
pointerInput
function is an extension to theModifier
class declared as follows:kotlinpublic fun Modifier.pointerInput( ___: Any?, ___: suspend PointerInputScope.() -> Unit ): Modifier
- The first argument is a variable that will be observed for changes by
pointerInput
- The second argument is a (suspending/suspendable) function that actually handles the gesture, and its FIRST argument must be of type
PointerInputScope
, so the following "function" declaration works:
kotlinval doGestureWork: suspend PointerInputScope.() -> Unit { // In this function "this" refers to a PointerInputScope object handleSwipe(this) } suspend fun handleSwipe(scope: PointerInputScope) { TODO() }
With
doGestureWork
defined, we can invokepointerInput
as follows:kotlinpointerInput(Unit, doGestureWork)
But instead of using a named function
doGestureWork
, we can just use an "unnamed function" (which is a lambda function) directly replacingdoGestureWork
kotlinpointerInput(Unit, { handleSwipe(this) }) // Trailing lambda inside parentheses pointerInput(Unit) { handleSwipe(this) } // Trailing lambda outside parentheses
- The first argument is a variable that will be observed for changes by
Communicating the Swipe
After the swipe direction is determined, how do we "announce" it to the rest of the app? Ideally we can pass a variable which is updated (internally) by handleSwipe
but to the asynchronous nature of suspendable function calls, this does not work easily. The trick used in the starter code is to invoked a lambda (called by handleSwipe
) when the swipe direction is detected. Hence we update the signature of handleSwipe
as fellows
// BEFORE
fun handleSwipe(scope: PointerInputScope) { /* code */}
// AFTER
fun handleSwipe(scope: PointerInputScope,
iAmSwiped: (Swipe) -> Unit) {
/* code to detect swipe direction */
iAmSwiped(Swipe.DOWN)
}
Consequently, invoking handleSwipe
now requires a second lambda argument:
pointerInput(Unit) {
// Trailing lambda inside parentheses
handleSwipe(this, { x ->
printf ("You swiped $x") // You swiped DOWN
} )
}
pointerInput(Unit) {
// Trailing lambda outside parentheses
handleSwipe(this) {
printf ("You swiped $it") // You swiped DOWN
vm.doSwipe(it)
}
}
Updating the UI
In response to each swipe, the ViewModel updates a List
(wrapped in a LiveData
) with the content that you want to show on the grid. The starter ViewModel includes the following snippet:
_numbers.value = (1..16).toList().map { "^" }
You will notice that the ViewModel defines two copies of "LiveData"
_numbers
is a private mutable live data which is internally modified as shown in the above snippetnumbers
is a non-private immutable live data which is accessible to the @Composable function
In Game1024
composable function, the numbers
live data must be transformed to a state variable which can be observed for changes to trigger recompositions (UI updates). This transformation is achieved by the following line:
val cellValues = vm.numbers.observeAsState()
When the ViewModel updaes the live data numbers
, the hosting @Composable
function will be notified the changes and it will begin UI recomposition (UI update).
Overall App Specifications
Game Logic
- Your UI should not contain any application logic. The UI should be design to only handle user input events (swipes, taps, input, etc.) and updating the UI.
- Application logic and game logic should be written elsewhere (in viewmodel)
- Swipe actions do not always end up if similar numbers being merged. Those which do not shall not insert a new random number
- Resetting the game shall clear the grid an insert a new number in an empty spot
TIP
The board update is easier when handled as a 2D array (of integers). But the FlowRow
widget used by NumberGrid
is designed to work with a 1D list. In your game logic design of handling the swipe, you will have to keep the array private to the GameModelView
class. After this 2D array is updated, refresh the numbers
LiveData with the content of your 2D array
Game UI Design
In addition to the widgets currently used in the starter code:
- Change the text at the top to include your name
- Add a text that shows the number of valid swipes, i.e. those swipes that causes the board to change
- Add a text to show the final game status (WIN / LOSE)
- Add a RESET button to restart the game at any time (not only after game won/lost)
The following screenshot is an example implementation by your instructor when no more numbers can be merged and the board is already full. The exact layout of the widgets of your implementation may differ, but the require information should be there.

Grading Rubrics
Grading Item | Point |
---|---|
Initial placement the first number on an empty cell | 2 |
Swipe actions leaves no gap between numbers | 4 |
Neighboring numbers are merged and added correctly | 3 |
Show number of valid swaps (those which change the board) | 2 |
Insert subsequent random number only after the board changes | 2 |
Detection of winning condition | 2 |
Detection of game over (lost) | 3 |
Swipes action game won/lost shall not update the board | 2 |
Reset the game board any time | 2 |
Source code updates pushed to GitHub | 3 |
Penalty for app logic/game logic in UI | -1 (per violation) |
Penalty for runtime errors (null pointer, index oob, etc.) | -1 (per violation) |