Сайт разработчика Александр Климова

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Виселица (Hangman)

Когда деревья были большими, я играл в "Виселицу" на листочках из тетради. Новое поколение может поиграть в эту игру на своих устройствах.

Цель игры проста - угадать слово, используя буквы. За каждую неправильную попытку рисуется фрагмент виселицы и человечка. Это очень жестокая игра, куда смотрят депутаты?

Создадим стандартный проект.

В папке res/drawable создадим новый файл letter_bg.xml.


<?xml version="1.0" encoding="utf-8"?>

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="line" >

    <stroke
        android:width="2dp"
        android:color="#FF333333" />

    <solid android:color="#00000000" />

    <padding android:bottom="20dp" />

    <size
        android:height="20dp"
        android:width="20dp" />

</shape>

В этой же папке создайте новый файл letter_up.xml.


<?xml version="1.0" encoding="utf-8"?>

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:padding="5dp"
    android:shape="rectangle" >

    <solid android:color="#FF333333" />

    <corners
        android:bottomLeftRadius="15dp"
        android:bottomRightRadius="15dp"
        android:topLeftRadius="15dp"
        android:topRightRadius="15dp" />

    <stroke
        android:width="2dp"
        android:color="#FFCCCCCC" />
</shape>

Теперь файл letter_down.xml:


<?xml version="1.0" encoding="utf-8"?>

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:padding="5dp"
    android:shape="rectangle" >

    <solid android:color="#FF000000" />

    <corners
        android:bottomLeftRadius="15dp"
        android:bottomRightRadius="15dp"
        android:topLeftRadius="15dp"
        android:topRightRadius="15dp" />
</shape>

Разметка для основной активности:


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FF000000"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:src="@drawable/android_hangman_bg" />

    <Button
        android:id="@+id/playBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Play"
        android:textColor="#FFFF0000" />

</RelativeLayout>

Создадим вторую активность GameActivity с разметкой


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="fill_parent"
    android:background="#FF000000"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin" >

</LinearLayout>

В манифесте установим для активности портретную ориентацию и укажем родителя.


<activity
    android:name="ru.alexanderklimov.hangman.GameActivity"
    android:label="@string/title_activity_game"
    android:parentActivityName=".MainActivity"
    android:screenOrientation="portrait">
</activity>

Подготовительные работы закончены. Напишем код для запуска второй активности по щелчку кнопки


public class MainActivity extends Activity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        Button playBtn = (Button)findViewById(R.id.playBtn);
        playBtn.setOnClickListener(this);
    }
    
    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.playBtn) {
            Intent playIntent = new Intent(this, GameActivity.class);
            this.startActivity(playIntent);
        }
    }
}    

Настроим внешний вид второй активности.


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FF000000"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin" >

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#FFFFFFFF"
        android:gravity="center"
        android:paddingTop="15dp" >
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/gallows"
            android:paddingLeft="0dp"
            android:paddingTop="0dp"
            android:src="@drawable/android_hangman_gallows" />

        <ImageView
            android:id="@+id/head"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/head"
            android:paddingLeft="108dp"
            android:paddingTop="23dp"
            android:src="@drawable/android_hangman_head" />

        <ImageView
            android:id="@+id/body"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/body"
            android:paddingLeft="120dp"
            android:paddingTop="53dp"
            android:src="@drawable/android_hangman_body" />

        <ImageView
            android:id="@+id/arm1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/arm"
            android:paddingLeft="100dp"
            android:paddingTop="60dp"
            android:src="@drawable/android_hangman_arm1" />

        <ImageView
            android:id="@+id/arm2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/arm"
            android:paddingLeft="120dp"
            android:paddingTop="60dp"
            android:src="@drawable/android_hangman_arm2" />

        <ImageView
            android:id="@+id/leg1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/leg"
            android:paddingLeft="101dp"
            android:paddingTop="90dp"
            android:src="@drawable/android_hangman_leg1" />

        <ImageView
            android:id="@+id/leg2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@string/leg"
            android:paddingLeft="121dp"
            android:paddingTop="90dp"
            android:src="@drawable/android_hangman_leg2" />
    </RelativeLayout>

    <LinearLayout
        android:id="@+id/word"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp"
        android:background="#FFFFFFFF"
        android:gravity="center"
        android:orientation="horizontal"
        android:padding="10dp" >
    </LinearLayout>

</LinearLayout>

В разметке имеется семь компонентов ImageView, каждый из которых отвечает за отдельную часть человечка - голова, туловище, руки, ноги. Наша задача заключается в правильном позиционировании каждых элементов, чтобы они все образовали цельную фигуру. При новой игре все элементы тела должны быть невидимы и появляться при неудачной попытке.

Слова будут храниться в отдельном файле. Создайте в папке res/values файл arrays.xml.


<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="words">
        <item>KITTEN</item>
        <item>COMPUTER</item>
        <item>TABLET</item>
        <item>SYSTEM</item>
        <item>APPLICATION</item>
        <item>INTERNET</item>
        <item>STYLUS</item>
        <item>ANDROID</item>
        <item>KEYBOARD</item>
        <item>SMARTPHONE</item>
    </string-array>
</resources>

Вы можете расширить указанный список.

Объявим переменные:


private String[] words;
private Random rand;
private String currWord;
private LinearLayout wordLayout;
private TextView[] charViews;

Слова для отгадывания хранятся в ресурсах в виде массива строк. В случайном порядке (класс Random) мы выберем слово из этого массива для нового раунда игры. Также мы создадим переменную для текущего слова и для разметки, которая будет содержать ответ. Ещё нам понадобится массив текстовых меток для отдельных символов.

Для запуска новой игры создадим вспомогательный метод playGame(). В нём случайным образом выбирается слово и проверяется на сходство с текущим словом в цикле while. Это сделано для того, чтобы одно и то же слово не пришлось отгадывать два раза подряд. В успешном случае значение переменной, отвечающей за текущее слово, заменяется на новый вариант.

Далее вычисляем длину слова и программно создаём массив из TextView, который будет содержать по одному символу. Цвет символов будет белым и будет невидим на белом фоне.

Все буквы алфавита будут находиться в сетке GridView сразу после изображения виселицы. В каждом ряду будет по семь символов (атрибут android:numColumns)


<GridView
    android:id="@+id/letters"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_marginBottom="5dp"
    android:background="#FF000000"
    android:horizontalSpacing="5dp"
    android:numColumns="7"
    android:padding="5dp"
    android:stretchMode="columnWidth"
    android:verticalSpacing="5dp" />

Мы создадим отдельный адаптер, чтобы связать символы с кнопками, которые программно появятся в GridView.

Создайте файл letter.xml в папке res/layout:


<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="35dp"
    android:background="@drawable/letter_up"
    android:onClick="letterPressed" />

Это отдельная разметка для единственной кнопки. Таких кнопок у нас будет 26 по числу символов в английском языке. Также мы задали имя метода для щелчка - letterPressed().

Добавим новый класс для адаптера LetterAdapter на основе стандартного BaseAdapter.


package ru.alexanderklimov.hangman;

import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.widget.Button;

public class LetterAdapter extends BaseAdapter {
    private String[] letters;
    private LayoutInflater letterInf;

    public LetterAdapter(Context c) {
        //setup adapter
        letters = new String[26];
        for (int a = 0; a < letters.length; a++) {
            letters[a] = "" + (char) (a + 'A');
        }
        letterInf = LayoutInflater.from(c);
    }

    @Override
    public int getCount() {
        return letters.length;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        //create a button for the letter at this position in the alphabet
        Button letterBtn;
        if (convertView == null) {
            //inflate the button layout
            letterBtn = (Button) letterInf.inflate(R.layout.letter, parent, false);
        } else {
            letterBtn = (Button) convertView;
        }
        //set the text to this letter
        letterBtn.setText(letters[position]);
        return letterBtn;
    }

    @Override
    public Object getItem(int arg0) {
        return null;
    }

    @Override
    public long getItemId(int arg0) {
        return 0;
    }
}

В конструкторе адаптера инициализируем массив из 26 символов и присвоим им значения по порядку - каждому по буковке в верхнем регистре.

В методе getCount() мы возвращаем длину созданного массива.

В методе getView() программно создаём кнопку для каждого элемента в составе GridView, указывая разметку letter.xml, и сразу указываем для него нужный текст. Остальные обязательные методы адаптера оставляем без изменений.

Теперь, имя адаптер, мы можем связать его с GridView.


ltrAdapt = new LetterAdapter(this);
letters.setAdapter(ltrAdapt);

Запустив проект, вы можете увидеть внешний вид будущей игры. Но пока кнопки не будут реагировать на нажатия, так как мы не написали для них код.

Перейдём непосредственно к логике игры. Перед началом нам не нужно показывать человечка, висящего на виселице. И появляться отдельная часть тела будет при неудачной попытке угадать букву. Создадим необходимые переменные:


// ImageView для частей тела
private ImageView[] bodyParts;
// сколько всего частей тела
private int numParts = 6;
// счетчик попыток
private int currPart;
// число символов в текущем слове
private int numChars;
// число угаданных вариантов
private int numCorr;

Проинициализируем все ImageView, которые содержат части тела и в методе playGame() программно сделаем их невидимыми.


for (int p = 0; p < numParts; p++) {
    bodyParts[p].setVisibility(View.INVISIBLE);
}

Осталось написать код, обрабатывающий щелчки на кнопках с символами. При нажатии получаем текст с кнопки:


String ltr = ((Button) view).getText().toString();
В оригинальной статье почему-то использовался TextView вместо кнопки.

Из строки извлекаем символ, а также делаем кнопку недоступной и меняем цвет, чтобы пользователь не пытался использовать уже использованные буквы.


char letterChar = ltr.charAt(0);
view.setEnabled(false);
view.setBackgroundResource(R.drawable.letter_down);

В цикле проверяем, имеется ли выбранный символ в слове. Если имеется, то выводим его на экран, меняя цвет символа на чёрный.


for (int k = 0; k < currWord.length(); k++) {
    if (currWord.charAt(k) == letterChar) {
        correct = true;
        numCorr++;
        charViews[k].setTextColor(Color.BLACK);
    }
}

В ходе игры ведём подсчёт попыток. Если число угаданных символов сравнялось с длиной слова, то пользователь выиграл.


if (correct) {
    // удачная попытка
    if (numCorr == numChars) {

Когда пользователь выиграл или проиграл, мы блокируем все кнопки с помощью вспомогательного метода disableBtns() и выводим диалоговое окно с предложением продолжить игру или выйти из неё.


public void disableBtns() {
    int numLetters = letters.getChildCount();
    for (int l = 0; l < numLetters; l++) {
        letters.getChildAt(l).setEnabled(false);
    }
}

Во время неудачных ходов мы постепенно делаем видимыми элементы тела:


else if (currPart < numParts) {
    //some guesses left
    bodyParts[currPart].setVisibility(View.VISIBLE);
    currPart++;
}

Игра готова. Осталось добавить различные возможности, например, показать справку через пункт меню и т.п.

Hangman

Полностью код класса будет следующим.

Ткните лапкой, чтобы развернуть код

package ru.alexanderklimov.hangman;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.content.res.Resources;
import android.graphics.Color;
import android.view.Gravity;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import android.widget.GridView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.app.AlertDialog;
import android.content.DialogInterface;
//import android.support.v4.app.NavUtils;
import android.view.View;
import android.widget.ImageView;

import java.util.Random;


public class GameActivity extends Activity {

    private String[] words;
    private Random rand;
    private String currWord;
    private LinearLayout wordLayout;
    private TextView[] charViews;
    private GridView letters;
    private LetterAdapter ltrAdapt;

    // ImageView для частей тела
    private ImageView[] bodyParts;
    // сколько всего частей тела
    private int numParts = 6;
    // счетчик попыток
    private int currPart;
    // число символов в текущем слове
    private int numChars;
    // число угаданных вариантов
    private int numCorr;

    private AlertDialog helpAlert;

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

        getActionBar().setDisplayHomeAsUpEnabled(true);

        Resources res = getResources();
        words = res.getStringArray(R.array.words);
        rand = new Random();
        currWord = "";
        wordLayout = (LinearLayout) findViewById(R.id.word);
        letters = (GridView) findViewById(R.id.letters);

        bodyParts = new ImageView[numParts];
        bodyParts[0] = (ImageView) findViewById(R.id.head);
        bodyParts[1] = (ImageView) findViewById(R.id.body);
        bodyParts[2] = (ImageView) findViewById(R.id.arm1);
        bodyParts[3] = (ImageView) findViewById(R.id.arm2);
        bodyParts[4] = (ImageView) findViewById(R.id.leg1);
        bodyParts[5] = (ImageView) findViewById(R.id.leg2);

        playGame();
    }

    private void playGame() {
        //play a new game
        // Выбираем случайным образом новое слово
        String newWord = words[rand.nextInt(words.length)];
        // Если слово совпадает с текущим, то повторяем еще раз, пока не выберем новое слово
        while (newWord.equals(currWord)) newWord = words[rand.nextInt(words.length)];
        currWord = newWord; // выбрали
        // программно создадим столько TextView, сколько символов в слове
        charViews = new TextView[currWord.length()];
        // но сначала удалим все элементы от прошлой игры
        wordLayout.removeAllViews();

        // создаём массив новых TextView
        for (int c = 0; c < currWord.length(); c++) {
            charViews[c] = new TextView(this);
            charViews[c].setText("" + currWord.charAt(c));

            charViews[c].setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
            charViews[c].setGravity(Gravity.CENTER);
            charViews[c].setTextColor(Color.WHITE);
            charViews[c].setBackgroundResource(R.drawable.letter_bg);
            // добавляем в разметку
            wordLayout.addView(charViews[c]);
        }

        ltrAdapt = new LetterAdapter(this);
        letters.setAdapter(ltrAdapt);
        currPart = 0;
        numChars = currWord.length();
        numCorr = 0;

        for (int p = 0; p < numParts; p++) {
            bodyParts[p].setVisibility(View.INVISIBLE);
        }
    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {

        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.game, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                //NavUtils.navigateUpFromSameTask(this);
                return true;
            case R.id.action_help:
                showHelp();
                return true;
        }

        return super.onOptionsItemSelected(item);
    }

    public void letterPressed(View view) {
        //user has pressed a letter to guess
        String ltr = ((Button) view).getText().toString();
        char letterChar = ltr.charAt(0);
        view.setEnabled(false);
        view.setBackgroundResource(R.drawable.letter_down);
        boolean correct = false;

        for (int k = 0; k < currWord.length(); k++) {
            if (currWord.charAt(k) == letterChar) {
                correct = true;
                numCorr++;
                charViews[k].setTextColor(Color.BLACK);
            }
        }

        if (correct) {
            // удачная попытка
            if (numCorr == numChars) {
                // блокируем кнопки
                disableBtns();

                // выводим диалоговое окно
                AlertDialog.Builder winBuild = new AlertDialog.Builder(this);
                winBuild.setTitle("Поздравляем");
                winBuild.setMessage("Вы победили!\n\nОтвет:\n\n" + currWord);
                winBuild.setPositiveButton("Сыграем ещё?",
                        new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int id) {
                                GameActivity.this.playGame();
                            }
                        }
                );

                winBuild.setNegativeButton("Выход",
                        new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int id) {
                                GameActivity.this.finish();
                            }
                        }
                );

                winBuild.show();
            }
        } else if (currPart < numParts) {
            //some guesses left
            bodyParts[currPart].setVisibility(View.VISIBLE);
            currPart++;
        }else{
            //user has lost
            disableBtns();

            // Display Alert Dialog
            AlertDialog.Builder loseBuild = new AlertDialog.Builder(this);
            loseBuild.setTitle("Увы");
            loseBuild.setMessage("Вы проиграли!\n\nБыло загадано:\n\n"+currWord);
            loseBuild.setPositiveButton("Сыграем ещё?",
                    new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int id) {
                            GameActivity.this.playGame();
                        }});

            loseBuild.setNegativeButton("Выход",
                    new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int id) {
                            GameActivity.this.finish();
                        }});

            loseBuild.show();
        }
    }

    public void disableBtns() {
        int numLetters = letters.getChildCount();
        for (int l = 0; l < numLetters; l++) {
            letters.getChildAt(l).setEnabled(false);
        }
    }

    public void showHelp() {
        AlertDialog.Builder helpBuild = new AlertDialog.Builder(this);

        helpBuild.setTitle("Help");
        helpBuild.setMessage("Guess the word by selecting the letters.\n\n"
                + "You only have 6 wrong selections then it's game over!");
        helpBuild.setPositiveButton("OK",
                new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        helpAlert.dismiss();
                    }});
        helpAlert = helpBuild.create();

        helpBuild.show();
    }
}

Перевод цикла статей (там же можно скачать исходники):
Create a Hangman Game: Project Setup
Create a Hangman Game: User Interface
Create a Hangman Game: User Interaction

Реклама