Skip to content

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)
2
2
1
2
1
2
2
2
2
2
1
1
4
4
Swipe Left
4
1
2
1
2
4
4
2
8
Swipe Left
4
1
2
1
2
8
2
8
2
2
1
2
1
2
2
2
2
2
1
1
4
4
Swipe Right
4
1
2
1
2
4
4
2
8
Swipe Right
4
1
2
1
2
8
2
8
2
2
1
2
1
2
2
2
2
2
1
1
4
4
Swipe Up
2
4
2
1
1
2
1
4
1
2
4
4
Swipe Up
2
4
2
1
1
2
1
8
1
2
4
2
2
1
2
1
2
2
2
2
2
1
1
4
4
Swipe Down
2
2
1
1
2
4
2
4
1
1
4
4
Swipe Down
2
2
1
2
4
2
1
1
1
4
8

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.

    kt
    Column {
       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:

kotlin
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 the Modifier class declared as follows:

    kotlin
    public 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:
    kotlin
    val 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 invoke pointerInput as follows:

    kotlin
    pointerInput(Unit, doGestureWork)

    But instead of using a named function doGestureWork, we can just use an "unnamed function" (which is a lambda function) directly replacing doGestureWork

    kotlin
    pointerInput(Unit, { handleSwipe(this) })  // Trailing lambda inside parentheses
    pointerInput(Unit) { handleSwipe(this) }   // Trailing lambda outside parentheses

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

kotlin
// 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:

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

kotlin
_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 snippet
  • numbers 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:

kotlin
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 ItemPoint
Initial placement the first number on an empty cell2
Swipe actions leaves no gap between numbers4
Neighboring numbers are merged and added correctly3
Show number of valid swaps (those which change the board)2
Insert subsequent random number only after the board changes2
Detection of winning condition2
Detection of game over (lost)3
Swipes action game won/lost shall not update the board2
Reset the game board any time2
Source code updates pushed to GitHub3
Penalty for app logic/game logic in UI-1 (per violation)
Penalty for runtime errors (null pointer, index oob, etc.)-1 (per violation)