/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Когда деревья были большими, я играл в "Виселицу" на листочках из тетради. Новое поколение может поиграть в эту игру на своих устройствах.
Цель игры проста - угадать слово, используя буквы. За каждую неправильную попытку рисуется фрагмент виселицы и человечка. Это очень жестокая игра, куда смотрят депутаты?
Создадим стандартный проект.
В папке 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();
Из строки извлекаем символ, а также делаем кнопку недоступной и меняем цвет, чтобы пользователь не пытался использовать уже использованные буквы.
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++;
}
Игра готова. Осталось добавить различные возможности, например, показать справку через пункт меню и т.п.
Полностью код класса будет следующим.
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