Hangman Tutorial Lesson 5 – Guessing Letters

In the previous lesson, we worked on code to hide the phrase. In this lesson, we are going to work on the code to guess letters. Like most things, there are a few alternatives you can use to accomplish this. You may think it would be easiest to use the on-screen keyboard (that you use to write emails). While this is an option, it is not ideal. We only want the user to be able to only choose letters (no numbers, punctuation, etc). Also, we don’t care about uppercase vs lowercase. After the user has guessed a letter, we also want to signify that the letter has already been chosen and not allow them to choose it again. While the on-screen keyboard can handle some of these requirements, it can’t handle everything. Instead, for each letter we’ll add a button to the screen. If the user clicks on a button, we’ll disable it to signify they’ve already guessed that letter.

We could manually add each button in the Graphical Layout, but with 26 buttons that is going to take a long time. So instead, we are going to programmatically add the buttons to our layout. We want the buttons to be placed on a grid, so we’ll use the GridLayout to begin with. We’ll add this manually. Go to the Graphical Layout, expand Layouts, and click and drag the TableLayout onto your LinearLayout.

Image

If you look in the Outline, you’ll see that it created 4 TableRows for you inside the TableLayout. We are going to create the rows ourselves in code, so go ahead and delete the 4 TableRows.

Image

Then rename the Id of the TableLayout to “@+id/tblLetters”.

Now let’s go to MainActivity.java to programmatically add the letters as buttons.

private void createLettersGrid()
{
    TableLayout tl = (TableLayout) findViewById(R.id.tblLetters);
    String letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    TableRow currentRow = null;
    for (int i=0;i<letters.length(); i++)
    {
        if (i%6==0)
        {
            //create a new row
            currentRow = new TableRow(this);
            currentRow.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.MATCH_PARENT));
            currentRow.setGravity(Gravity.CENTER_HORIZONTAL);
            tl.addView(currentRow, new TableLayout.LayoutParams(TableLayout.LayoutParams.MATCH_PARENT, TableLayout.LayoutParams.MATCH_PARENT));
        }
        Button letterButton = createLetterButton(String.valueOf(letters.charAt(i)));
        letterButtons.add(letterButton);
        currentRow.addView(letterButton);
    }
}

private Button createLetterButton(String letter)
{
    Button b = new Button(this, null, android.R.attr.buttonStyleSmall);
    b.setText(letter);
    b.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT));
    return b;
}

To get this to compile, we’ll also need to create a member variable for letterButtons (we’ll need access to this later):

private List<Button> letterButtons = new ArrayList<Button>();

We created two methods: createLettersGrid() which for each letter calls createLetterButton(). In createLettersGrid(), we first get a reference to our table layout. We create a String for all the letters we want to represent, and then iterate over it a letter at a time. Every 6 letters, we want to start a new row, so when i%6==0, then we create a row and add it to that TableLayout. For the row, we set the gravity (which is basically alignment in Android) to CENTER_HORIZONTAL so that all the letters get placed in the center. You’ll also notice that I set the LayoutParams to MATCH_PARENT. In Android, you can choose to have your widget fill all the available space that it has access to (MATCH_PARENT), or only set the size large enough to fit the content it has inside of it (WRAP_CONTENT). You’ll see another option, FILL_PARENT, which is basically synonymous with MATCH_PARENT and has been deprecated.

To add a button to the row, we call our createLetterButton method (which we’ll discuss in a second), and pass in the value of the current letter we are on. We add the button to our member variable letterButtons (which we’ll use later to reset all the buttons on a new game), and add it to the row.

In createLetterButton(), we create a Button, set the style to a predefined small style (android.R.attr.buttonStyleSmall), set the text to the specified letter, and set the layout to WRAP_PARENT this time, because we want the button only as large as needed to fit the text.

Then we need to call the createLettersGrid() method from our onCreate() method.

@Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    attachNextButton();
    createLettersGrid();

    phraseGenerator = new PhraseGenerator(this);
    TextView txtPhrase = (TextView)findViewById(R.id.txtPhrase);
    txtPhrase.setText(phraseGenerator.nextPhrase().getDisplayString());
}

Let’s deploy it and see what it looks like.

device-2014-03-29-103211

It doesn’t look pretty, but we’ll work on cleaning up the look and feel of our app later.

Currently, the hangmanPhrase is a local variable. To be able to update the phrase as the user guesses letters, we need it to be a member variable. So move the hangmanPhrase to be a member variable:

private PhraseGenerator phraseGenerator;
private List<Button> letterButtons = new ArrayList<Button>();
private HangmanPhrase hangmanPhrase;

Let’s change the onCreate() method to the to call newGame() to get rid of duplication:

@Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    attachNextButton();
    createLettersGrid();

    phraseGenerator = new PhraseGenerator(this);
    newGame();
}

Then we need to update newGame() now that hangmanPhrase is a member variable:

private void newGame()
{
    TextView txtPhrase = (TextView)findViewById(R.id.txtPhrase);
    hangmanPhrase = phraseGenerator.nextPhrase();
    txtPhrase.setText(hangmanPhrase.getDisplayString());
}

This is getting a little confusing – you can look at the final code at the bottom of the blog to make sure you’re getting everything.

We now have buttons for each letter, but when you tap on them they do nothing. Let’s create a click handler for the buttons.

private class LetterButtonClickedListener implements OnClickListener
{
    private String letter;

    public LetterButtonClickedListener(String letter)
    {
        this.letter = letter;
    }

    @Override
    public void onClick(View v)
    {
        ((Button) v).setEnabled(false);
        boolean correctGuess = hangmanPhrase.guessLetter(letter);
        updatePhrase();
    }
}

The custom ClickListener class takes the letter it represents in the constructor. When clicked, it disables the button (so you can’t click on it again), and calls guessLetter on the hangmanPhrase (which updates isRevealed to true). Lastly, we want to call updatePhrase (which we’ll define in a second) that will update the text in txtPhrase to reflect the guess.

private void updatePhrase()
{
TextView txtPhrase = (TextView)findViewById(R.id.txtPhrase);
txtPhrase.setText(hangmanPhrase.getDisplayString());
}

In updatePhrase, we get a reference to the txtPhrase, and update the text.

Let’s deploy the app again, and now see what it looks like when we guess a letter:

device-2014-03-29-111712

One more thing. When we click on the Next button, we want to reset all the buttons to enable them. Let’s update newGame() method to loop through each letterButton and enable it. Also, let’s change our code to call updatePhrase() instead of setting the txtPhrase directly (code reuse is good!).

private void newGame()
{
    hangmanPhrase = phraseGenerator.nextPhrase();
    updatePhrase();
    for (Button button : letterButtons)
        button.setEnabled(true);
}

Here is the whole file for reference:

package com.famlinkup.hangman;

import java.util.ArrayList;
import java.util.List;

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;

public class MainActivity extends ActionBarActivity
{
    private PhraseGenerator phraseGenerator;
    private List<Button> letterButtons = new ArrayList<Button>();
    private HangmanPhrase hangmanPhrase;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        attachNextButton();
        createLettersGrid();

        phraseGenerator = new PhraseGenerator(this);
        newGame();
    }

    private void attachNextButton()
    {
        Button btnNext = (Button)findViewById(R.id.btnNext);
        btnNext.setOnClickListener(new OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                newGame();
            }
        });
    }

    private void createLettersGrid()
    {
        TableLayout tl = (TableLayout) findViewById(R.id.tblLetters);
        String letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        TableRow currentRow = null;
        for (int i=0;i<letters.length(); i++)
        {
            if (i%6==0)
            {
                //create a new row
                currentRow = new TableRow(this);
                currentRow.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.MATCH_PARENT));
                currentRow.setGravity(Gravity.CENTER_HORIZONTAL);
                tl.addView(currentRow, new TableLayout.LayoutParams(TableLayout.LayoutParams.MATCH_PARENT, TableLayout.LayoutParams.MATCH_PARENT));
            }
            Button letterButton = createLetterButton(String.valueOf(letters.charAt(i)));
            letterButtons.add(letterButton);
            currentRow.addView(letterButton);
        }
    }

    private Button createLetterButton(String letter)
    {
        Button b = new Button(this, null, android.R.attr.buttonStyleSmall);
        b.setText(letter);
        b.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT));
        b.setOnClickListener(new LetterButtonClickedListener(letter));
        return b;
    }

    private class LetterButtonClickedListener implements OnClickListener
    {
        private String letter;

        public LetterButtonClickedListener(String letter)
        {
            this.letter = letter;
        }

        @Override
        public void onClick(View v)
        {
            ((Button) v).setEnabled(false);
            boolean correctGuess = hangmanPhrase.guessLetter(letter);
            updatePhrase();
        }
    }

    private void updatePhrase()
    {
        TextView txtPhrase = (TextView)findViewById(R.id.txtPhrase);
        txtPhrase.setText(hangmanPhrase.getDisplayString());
    }

private void newGame()
{
    hangmanPhrase = phraseGenerator.nextPhrase();
    updatePhrase();
    for (Button button : letterButtons)
        button.setEnabled(true);
}

    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item)
    {
        int id = item.getItemId();
        if (id == R.id.action_settings)
        {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s