Hangman Tutorial Lesson 8 – Adding Hints

Hangman phrases can be hard to guess sometimes, especially when there are only a few short words. To make it a little easier, it would be good to give a hint, which would be the category of the phrase. We can then allow the user to show or hide the hints.

Let’s modify our phrases.txt file to include a hint. Below I’ve listed the file I’ve used, but you can make up your own.

Person:Ludvig van Beethoven
Person:Albert Einstein
Person:Joan of Arc
Person:George Washington
Person:Michael Jordan
Person:Steven Stielberg
Person:Jackie Robinson
Person:Michaelangelo
Person:Leonardo da Vinci
Place:Taj Mahal
Place:Great Barrier Reef
Place:Great Wall of China
Place:New Hampshire
Place:New Mexico
Place:New England
Place:Mount Everest
Food:Chocolate Chip Cookies
Food:Lasagna
Food:Spaghetti
Food:Egg Drop Soup
Food:Pepperoni Pizza
Food:Shepherds Pie
Food:Chicken Pot Pie
Food:Ice Cream Sundae
Food:Chocolate Milk Shake
Food:Peanut Butter and Jelly Sandwich
College Major:Chemistry
College Major:Computer Science
College Major:Bioology
College Major:Psychology
College Major:Humanities
College Major:Physics
College Major:Math
College Major:Political Science
College Major:Economics
College Major:Archaeology
College Major:Electrical Engineering
College Major:Mechanical Engineering
College Major:Chemical Engineering
Weather:Snow showers
Weather:Partly cloudy
Weather:Humid
Weather:Blizzard
Candy:Milky Way
Candy:Skittles
Candy:Sour Patch Kids
Candy:Jelly Beans
Candy:Starburst
Candy:Reese's Peanut Butter Cup
Candy:Reese's Pieces
Candy:Salt Water Taffy
Candy:Milk Duds
Candy:Almond Joy
Candy:Swedish Fish
Candy:Kit-Kat
Candy:Gummi Bears
Candy:Hershey's Kiss
Superhero:Iron Man
Superhero:Wonder Woman
Superhero:Green Lantern
Superhero:Captain America
Instrument:French Horn
Instrument:Grand Piano
Instrument:Trumpet
Instrument:Xylophone
Instrument:Steel Drum
United States:Massachusetts
United States:New Hampshire
United States:Idaho
United States:Mississippi
United States:California
United States:West Virginia
United States:Vermont
Large Cities:Beijing
Large Cities:Buenos Aires
Large Cities:Los Angeles
Large Cities:New York City
Large Cities:Moscow
Large Cities:Rio de Janeiro
Large Cities:Tokyo
Toys:Legos
Toys:Train Set
Toys:Monopoly
Toys:Candy Land
Toys:Beanie Babies
Toys:Snakes and Ladders
Toys:Cabbage Patch Dolls
Toys:Care Bears
Toys:My Little Pony
Vegetable:Carrot
Vegetable:Brussel Sprout
Vegetable:Pumpkin
Vegetable:Broccoli
Vegetable:Green Onion
Vegetable:Lettuce
Tree:Japanese Maple
Tree:Quaking Aspen
Tree:Peach
Palidrome:A man, a plan, a canal: Panama
Palidrome:Never odd or even

From left to right, we place the hint first, followed by a colon, and then the phrase. Let’s change the HangmanPhrase to keep track of the hint.

private String hint;
private List<HangmanLetter> letters;

public HangmanPhrase(String hint, String phrase)
{
    this.hint = hint;
    this.letters = new ArrayList();
    for (int i=0; i<phrase.length(); i++)
        letters.add(new HangmanLetter(String.valueOf(phrase.charAt(i))));
}

public String getHint()
{
   return hint;
}

We create a member variable for the hint. We pass in the hint in the constructor and assign it. Also, we create a getter method to get the hint. Making this change breaks PhraseGenerator. Let’s go to the class and update the code to read in the new hint.

public HangmanPhrase nextPhrase()
{
    String line = lines.get(random.nextInt(lines.size()));
    int colonIndex = line.indexOf(":");
    String hint = line.substring(0, colonIndex);
    String phrase = line.substring(colonIndex + 1);
    return new HangmanPhrase(hint, phrase);
}

Again, we randomly get a line from the file. We call indexOf() to get the first occurrence of a colon. We then use this index to get both the hint and the phrase, which we pass into the constructor.

Next, we need to add another TextView to the layout for hints. Go to the Graphical Layout, and drag a “Small Text” widget onto the Outline’s LinearLayout so that it is first.

2014-03-29 19_36_26-Java - Hangman_res_layout_main.xml - Eclipse

Change the Id of the TextView to txtHint. Now go to the MainActivity file. Let’s update the newGame() method.

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

    TextView txtHint = (TextView)findViewById(R.id.txtHint);
    txtHint.setText("Hint: " + hangmanPhrase.getHint());
}

Next let’s work on the option to show or hide hints. Add the following member variables at the top of the class:

private boolean showHints = true;
private SharedPreferences prefs;

We have a boolean to track whether we have showHints enabled or not. We also have a variable for SharedPreferences. We’ll want to store this setting, so that we can remember what they chose last time. To do so, we make use of SharedPreferences, which is basically just a persistent Map<String,Object>. You can put into the map with a key and an object (which saves the entry into the preferences files), and get from the map using a specific key.

Next, let’s modify the onCreate() method:

protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    this.prefs = this.getSharedPreferences("hangman", Context.MODE_PRIVATE);
    showHints = prefs.getBoolean("showHints", true);
    if (!showHints)
        ((TextView)findViewById(R.id.txtHint)).setVisibility(TextView.GONE);

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

We get the sharedPrefences from the Activity, specifying the preferences file name you want to retrieve. If it doesn’t exist it creates one for you. Because this is the only app that will use these preferences, we specify Context.MODE_PRIVATE. Assuming the preferences have been saved already from a previous game, prefs.getBoolean() gets the entry with the key “showHints”, and uses the default of true if it’s not there (like on the first time Hangman is played). If showHints is false, we hide the txtHint TextView by setting the visibility to GONE. These is also an option for HIDDEN, which hides the TextView (similar to GONE), but also leaves empty space where the TextView would have been. GONE, on the otherhand, hides it and removes that space.

Next, we want to modify the action menu to contain an option for showing or hiding the hints. We want to dynamically change the name of the menu item to either “Hide Hints” or “Show Hints” based on the value of showHints variable. To do that, we are going to override the onPrepareOptionsMenu() method. This method is called any time the menu is opened.

@Override
public boolean onPrepareOptionsMenu(Menu menu)
{
    menu.getItem(0).setTitle(showHints ? "Hide Hints" : "Show Hints");
    return true;
}

We get the only item in our menu (when you create the app, it should have created a default menu item for you), and change the title. Next, we want to write the code to handle when they select the menu item. We’ll modify the onOptionsItemSelected() method, which Eclipse should have created for you by default when the application was created. This method is called anytime a menuItem is clicked on.

public boolean onOptionsItemSelected(MenuItem item)
{
    int id = item.getItemId();
    TextView txtHint = ((TextView)findViewById(R.id.txtHint));
    if (id == R.id.action_settings)
    {
        showHints = !showHints;
        prefs.edit().putBoolean("showHints", showHints).commit();
        txtHint.setVisibility(showHints ? TextView.VISIBLE : TextView.GONE);
        return true;
    }
    return super.onOptionsItemSelected(item);
}

If the itemId is the action_settings id (the one they created for you), then we toggle showHints. We also save the setting in the SharedPreferences by doing a put, and calling commit (which needs to be called to save off any changes). Then we toggle the visibility based on the showHints value.

Once last thing that I want to adjust is the location of the AlertDialog. By placing it in the center, it can hide the revealed phrase when you lose. It would be good to move it farther down. Let’s modify the checkGameOver() method.

private void checkGameOver()
{
    if (hangmanPhrase.isAllRevealed())
    {
        adjustPositionAndShowDialog(new AlertDialog.Builder(this).setMessage("Congratulations you won!").setCancelable(false).setPositiveButton("Next game", new DialogInterface.OnClickListener()
        {
            @Override
            public void onClick(DialogInterface dialog, int which)
            {
                newGame();
            }
        }).create());
    }
    else if (wrongGuesses >= 6)
    {
        hangmanPhrase.revealAll();
        updatePhrase();
        adjustPositionAndShowDialog(new AlertDialog.Builder(this).setMessage("You lost!").setCancelable(false).setPositiveButton("Next game", new DialogInterface.OnClickListener()
        {
            @Override
            public void onClick(DialogInterface dialog, int which)
            {
                newGame();
            }
        }).create());
    }
}

private void adjustPositionAndShowDialog(AlertDialog dialog)
{
    dialog.getWindow().setGravity(Gravity.BOTTOM);
    dialog.getWindow().getAttributes().verticalMargin = 0.2F;
    dialog.show();
}

We wrap each construction of our AlertDialog with a call to our new method, adjustPositionAndShowDialog(). Also, we change the show() to create(), so that we can first adjust the dialog before showing it. In the adjustPositionAndShowDialog method, we set the gravity to BOTTOM, and set the verticalMargin to 0.2F. This sets the vertical margin, as a percentage of the container’s height, between the container and the widget.

Let’s deploy and see our changes.

device-2014-03-29-210418

 

That’s much better placement. We’ll, that’s all for this post. We have one final lesson, where we’ll put a couple final touches on the app.

Here is MainActivity in it’s entirety:

package com.famlinkup.hangman;

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

import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
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.view.WindowManager;
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;
    private int wrongGuesses = 0;
    private boolean showHints = true;
    private SharedPreferences prefs;
    
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        this.prefs = this.getSharedPreferences("hangman", Context.MODE_PRIVATE);
        showHints = prefs.getBoolean("showHints", true);
        if (!showHints)
            ((TextView)findViewById(R.id.txtHint)).setVisibility(TextView.GONE);
        
        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);
            if (!correctGuess)
                wrongGuesses++;
            updatePhrase();
            checkGameOver();
        }
    }
        
    private void updatePhrase()
    {
        TextView txtPhrase = (TextView)findViewById(R.id.txtPhrase);
        txtPhrase.setText(hangmanPhrase.getDisplayString());
        
        TextView txtWrongGuesses = (TextView)findViewById(R.id.txtWrongGuesses);
        txtWrongGuesses.setText(wrongGuesses + " wrong guesses");
        
        ImageView imgHangman = (ImageView)findViewById(R.id.imgHangman);
        switch (wrongGuesses)
        {
            case 0: imgHangman.setImageResource(R.drawable.hangman0); break;
            case 1: imgHangman.setImageResource(R.drawable.hangman1); break;
            case 2: imgHangman.setImageResource(R.drawable.hangman2); break;
            case 3: imgHangman.setImageResource(R.drawable.hangman3); break;
            case 4: imgHangman.setImageResource(R.drawable.hangman4); break;
            case 5: imgHangman.setImageResource(R.drawable.hangman5); break;
            case 6: imgHangman.setImageResource(R.drawable.hangman6); break;
        }
        imgHangman.refreshDrawableState();
    }
        
    private void newGame()
    {
        wrongGuesses = 0;
        hangmanPhrase = phraseGenerator.nextPhrase();
        updatePhrase();
        for (Button button : letterButtons)
            button.setEnabled(true);
        
        TextView txtHint = (TextView)findViewById(R.id.txtHint);
        txtHint.setText("Hint: " + hangmanPhrase.getHint());
    }
    
    private void checkGameOver()
    {
        if (hangmanPhrase.isAllRevealed())
        {
            adjustPositionAndShowDialog(new AlertDialog.Builder(this).setMessage("Congratulations you won!").setCancelable(false).setPositiveButton("Next game", new DialogInterface.OnClickListener()
            {
                @Override
                public void onClick(DialogInterface dialog, int which)
                {
                    newGame();
                }
            }).create());
        }
        else if (wrongGuesses >= 6)
        {
            hangmanPhrase.revealAll();
            updatePhrase();
            adjustPositionAndShowDialog(new AlertDialog.Builder(this).setMessage("You lost!").setCancelable(false).setPositiveButton("Next game", new DialogInterface.OnClickListener()
            {
                @Override
                public void onClick(DialogInterface dialog, int which)
                {
                    newGame();
                }
            }).create());
        }
    }
    
    private void adjustPositionAndShowDialog(AlertDialog dialog)
    {
        dialog.getWindow().setGravity(Gravity.BOTTOM);
        dialog.getWindow().getAttributes().verticalMargin = 0.2F;
        dialog.show();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
    
    @Override
    public boolean onPrepareOptionsMenu(Menu menu)
    {
        menu.getItem(0).setTitle(showHints ? "Hide Hints" : "Show Hints");
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) 
    {
        int id = item.getItemId();
        TextView txtHint = ((TextView)findViewById(R.id.txtHint));
        if (id == R.id.action_settings) {
            showHints = !showHints;
            prefs.edit().putBoolean("showHints", showHints).commit();
            txtHint.setVisibility(showHints ? TextView.VISIBLE : TextView.GONE);
            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