Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

Nim Challenge

5.00/5 (2 votes)
26 Jul 2012Apache7 min read 34.7K   1.2K  
Nim game for Android devices

Nim Challenge Screenshot

Introduction

Nim is a strategy game, where two players take turns removing objects from distinct heaps. A player can remove any number of items, as long as they all come from the same heap and they appear in consecutive places. The player who removes the last item from all the heaps wins.

I believe that most of you have played this game before and many of you would also probably know that Nim is by no means a game of luck. Instead, an algorithm exists that if applied correctly, allows one of the two players to always win. Nim Challenge is an Android application that implements this "Winning Strategy" and challenges the player to apply it in different situations and under time pressure.

Winning Strategy

Let us start the description of the Winning Strategy with an example. Consider an arrange of three heaps, one of size 1, one of size 2 and one of size 3.

 | 
|| 
|||

This arrange can be represented as follows:

1 = 0012
2 = 0102
3 = 0112
 ---
Sum: 000

For each row we count the number of elements. We convert the number to its binary representation. A binary number consists of 1s and 0s. In each column we count the number of 1s. If the result is an odd number, then in the corresponding column at the Sum we get an 1. If it is an even number, we get a 0.

If the Sum is all zeros, then the player who plays first loses. On the other hand, if there is even a single 1 in the Sum, then the first player can always find a move that it will convert the arrange of items into a form that has a zero Sum and win.

The above algorithm assumes that the last player to remove an item wins. However the game is also frequently played such as the last player loses. This is referred as a "misère " game. In a "misère " game the same algorithm can be applied, with the exception when only heaps of size 1 are to be left. In that case, the player must try to leave an odd number of size 1 heaps.

When you play, it is difficult to convert into binary and calculate the sum quickly. Even if you do so, you still need to find the move that it will lead to a zero sum arrange and this is not easy either. In order to play quickly, you can follow a small set of rules:

  • An arrange of 1, 2, 3 loses.
  • An arrange of 1, 3, 5 wins (convert it to 1, 2, 3).
  • An arrange, where each number appears an even number of times loses (e.g 1,1,2,2 and 4,4). You can quickly draw out of the picture duplicating rows.
  • A combination of a winning and a losing arrange wins.

Game modes

Nim Challenge offers three different game modes. In Classic mode the original arrange of items is used. The player can select whether to play first or not, but in order to win the player must start first. You can try this mode to practice and follow computer's moves.

In Variations mode different arranges of items are used. The player selects, if she wants to play first or not. This is a crucial decision and the specific arrange of items must be taken into account. If the player wins, she can continue playing another game. Every time the arrange of items is getting more complicated. In this mode a score is kept. The player must play as quickly as possible in order to win more points.

In Challenge mode you play against time. You only have a limited amount of time to find the best move. If the time expires, you lose. In this mode also a score is kept and faster moves win more points.

Nim Challenge also allows you to create Custom Boards. You can create any number of custom boards and edit them by long touching on them in the Custom Boards list. You can use this facility to practice by playing the same board again and again.

Settings

The following aspects of the game can be configured:

  • Single Touch: If checked each move needs not to be confirmed and it is instantly played. This is quicker, but doesn't give a chance to correct an error.
  • Theme: Selects the appearance of the board.
  • Last wins: Selects if the game will be played as a normal (checked) or misère game (unchecked). In a normal game the player who removes the last element wins. In a misère game the last player loses.

The above settings affect all game modes.

Source Code

The design and the implementation of Nim Challenge follow the same principles as Puzzles Solver. These principles were thoroughly described in a previous article. Instead of repeating them here, I would like to briefly refer to some challenging problems I faced during the implementation of Nim Challenge.

Drawing lines

All graphics in the game are using Canvas drawing and simple View objects. The nature of the game is such that fast updates aren't needed. so a more advanced solution isn't necessary. There is however a detail that needs special treatment. In order to select items from a heap the user strikes a line between two points in the screen:

Line between two points

The user first touches on the screen to select the starting point and then drags his/her finger to select the ending point. While dragging the line is dynamically updated. The code snippet below presents how the dynamic update is handled in the onTouchEvent method of the NimView.

switch(event.getAction())
{
case MotionEvent.ACTION_DOWN:
    // First touch - Set the starting point, reset the isAnimationDelayed flag, start the move
    isAnimationDelayed = false;
    if (moveState == MoveState.Move_Idle) {
        startPoint.set((int) event.getX(), (int) event.getY());
        endPoint.set((int) event.getX(), (int) event.getY());
        moveState = MoveState.Move_Started;
    }
case MotionEvent.ACTION_MOVE:
    // User drags his/her finger - set the ending point and update the line
    if (moveState == MoveState.Move_Started) {
        endPoint.set((int) event.getX(), (int) event.getY());
        // Only invalidate the view if more than 100ms (animationDelay) from the previous
        // update have passed
        if (!isAnimationDelayed) {
            isAnimationDelayed = true;
            animationDelayHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (isAnimationDelayed) {
                        invalidate();
                        isAnimationDelayed = false;
                    }
                }

            }, animationDelay);
        }
    }
case MotionEvent.ACTION_UP:
    isAnimationDelayed = false;
    if (moveState == MoveState.Move_Started) {
        endPoint.set((int) event.getX(), (int) event.getY());
        moveState = MoveState.Move_Idle;
        
        // Find the objects selected by the user
    }

As you can see the screen is only updated every 100ms (or when the user lifts his/her finger) and not every time a new ACTION_MOVE event is received. ACTION_MOVE are sent too often and if one makes the mistake to respond to each one of them, then one will seriously degrade the application's performance.

Time Counter

In order to count the game time I have created a simple TimeCounter class. This class uses System.currentTimeMillis() method to measure time and a single instance of it is used by NimGame, which is the game Activity and NimView, which is the View responsible for drawing the game canvas and handling user input.

Inside NimView there is a Handler that launches a Runnable to run once a second.

// Constructor
timerHandler = new Handler();

// Start of time measurement
timeCounter.start();
timerHandler.postDelayed(timeRunnable, 1000);

// Code to run once a second
private Runnable timeRunnable = new Runnable() {
    @Override
    public void run() {
        if (gameState == GameState.User_Move || gameState == GameState.First_Player_Question) {
            // Re-draw the screen
            invalidate();
        }
        timerHandler.postDelayed(timeRunnable, 1000);
    }
};

Inside the onDraw method the updated time is rendered on the screen. The check if the time has expired, also happens there. This gives some extra milliseconds to the user to respond.

From all these it may seems that the TimeCounter is only needed by the NimView and that there is no reason to be shared with the NimGame. The reason that the NimGame needs access to the TimeCounter has to do with the life cycle handling of the application. NimGame hosts the onPause and onResume methods, where some important time related actions need to be performed. For sure the timer needs to be paused, when the onPause method is called. This will happen for example, when the phone rings during a game. We would then like the user to answer the call and then return to the game without the time having advanced. So one solution will be to resume the timer, when the onResume method is called. However, it isn't always as simple as this. In some devices the onResume method may be not called, when the uses switches off the screen. In that case the application will be behind a lock screen. Upon unlocking the screen, the onResume isn't called. One may register a Broadcast Receiver to monitor the events. Or utilize the public void onWindowFocusChanged (boolean hasFocus) method. These are all complicated solutions and may not work the same among all devices. However, I don't believe that any of these is actually needed. There is an alternative that is both simpler and provides better user experience. In order to deal with these cases, we follow this pattern:

  • When the application is paused or loses foreground focus, pause the timer. Gray-out the screen to indicate that the game is in paused mode.
  • Require user action to resume the timer. Whenever the user returns to the user, he/she will be met with a gray-out screen and will have to touch anywhere at the screen to continue.

This logic is easy to implement, works well across all devices and in all cases of the application going into background and most of all works better for the user.

@Override
public void onPause() {
    super.onPause();

    if (!gameFinished) {
        // Pause the timer
        timeCounter.pause();
        // Pause the view - the screen will be grayed out and a "Pause" message will appear
        nimView.setPaused(true);
    }
}

@Override
public void onResume() {
    super.onResume();

    // Invalidate the view for the gray-out screen to appear - wait for user input to continue
    nimView.invalidate();
}

History

  • First version of the article

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0