Освой Android играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Однажды я написал статью на Хабре. Один из читателей сказал, что тоже написал одну статью про создание игры "Крестики-нолики", но боится её выкладывать на всеобщее обозрение. Я и несколько других комментаторов сумели убедить его опубликовать свой материал, который вы можете прочитать здесь. В статье речь шла о создании игры в IntelliJ IDEA Community Edition. Если кому-то интересно, то ознакомьтесь с оригиналом статьи, а также почитайте комментарии. Я в свою очередь немного отредактировал статью и сам код игры.
Статья затронет весь цикл разработки приложения. Вместе мы напишем простенькую игру “Крестики-Нолики” с одним экраном.
Для нашего приложения идеально подойдёт макет TableLayout с идентификатором android:id="@+id/tableLayout".
<?xml version="1.0" encoding="utf-8"?>
<TableLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/tableLayout"
android:gravity="center">
</TableLayout>
Программный доступ к TableLayout:
package ru.alexanderklimov.tictactoe;
import android.os.Bundle;
import android.widget.TableLayout;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private TableLayout tableLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tableLayout = findViewById(R.id.tableLayout);
buildGameField(); // создание игрового поля
}
}
Теперь необходимо реализовать метод buildGameField(). Для этого требуется создать поле в виде матрицы. Этим будет заниматься класс Game. Сначала нужно создать класс Player, объекты которого будут заполнять ячейки игрового поля и класс Square для самих ячеек.
package ru.alexanderklimov.tictactoe; public class Player { private String name; public Player(String name) { this.name = name; } public CharSequence getName() { return name; } }
package ru.alexanderklimov.tictactoe;
public class Square
{
private Player player = null;
public void fill(Player player)
{
this.player = player;
}
public boolean isFilled()
{
return player != null;
}
public Player getPlayer()
{
return player;
}
}
package ru.alexanderklimov.tictactoe;
public class Game
{
private Square[][] field;
private int squareCount;
public Game()
{
field = new Square[3][3];
squareCount = 0;
// заполнение поля
for (int i = 0, l = field.length; i < l; i++)
{
for (int j = 0, l2 = field[i].length; j < l2; j++)
{
field[i][j] = new Square();
squareCount++;
}
}
}
public Square[][] getField()
{
return field;
}
}
Метод buildGameField() динамически добавляет строки и колонки в таблицу (игровое поле):
private Button[][] buttons = new Button[3][3];
private void buildGameField()
{
Square[][] field = game.getField();
for (int i = 0, lenI = field.length; i < lenI; i++ ) {
TableRow row = new TableRow(this); // создание строки таблицы
for (int j = 0, lenJ = field[i].length; j < lenJ; j++)
{
Button button = new Button(this);
buttons[i][j] = button;
button.setOnClickListener(new Listener(i, j)); // установка слушателя, реагирующего на клик по кнопке
row.addView(button, new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT,
TableRow.LayoutParams.WRAP_CONTENT)); // добавление кнопки в строку таблицы
button.setWidth(160);
button.setHeight(160);
}
tableLayout.addView(row,
new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT,
TableLayout.LayoutParams.WRAP_CONTENT)); // добавление строки в таблицу
}
}
В коде создаётся объект, реализующий интерфейс View.OnClickListener. Создадим вложенный класс Listener. Он будет виден только из активности.
public class Listener implements View.OnClickListener
{
private int x = 0;
private int y = 0;
Listener(int x, int y)
{
this.x = x;
this.y = y;
}
public void onClick(View view)
{
Button button = (Button) view;
}
}
Осталось реализовать логику игры. Возвращаемся к Game.java:
package ru.alexanderklimov.tictactoe;
public class Game {
// игроки
private Player[] players;
// поле
private Square[][] field;
// начата ли игра?
private boolean started;
// текущий игрок
private Player activePlayer;
// Считает колличество заполненных ячеек
private int filled;
// Всего ячеек
private int squareCount;
public Game() {
field = new Square[3][3];
squareCount = 0;
// заполнение поля
for (int i = 0, l = field.length; i < l; i++) {
for (int j = 0, l2 = field[i].length; j < l2; j++) {
field[i][j] = new Square();
squareCount++;
}
}
players = new Player[2];
started = false;
activePlayer = null;
filled = 0;
}
public void start() {
resetPlayers();
started = true;
}
private void resetPlayers() {
players[0] = new Player("X");
players[1] = new Player("O");
setCurrentActivePlayer(players[0]);
}
private void setCurrentActivePlayer(Player player) {
activePlayer = player;
}
public Square[][] getField() {
return field;
}
public boolean makeTurn(int x, int y) {
if (field[x][y].isFilled()) {
return false;
}
field[x][y].fill(getCurrentActivePlayer());
filled++;
switchPlayers();
return true;
}
private void switchPlayers() {
activePlayer = (activePlayer == players[0]) ? players[1] : players[0];
}
public Player getCurrentActivePlayer() {
return activePlayer;
}
public boolean isFieldFilled() {
return squareCount == filled;
}
public void reset() {
resetField();
resetPlayers();
}
private void resetField() {
for (int i = 0, l = field.length; i < l; i++) {
for (int j = 0, l2 = field[i].length; j < l2; j++) {
field[i][j].fill(null);
}
}
filled = 0;
}
}
Очевидно, что в крестики-нолики выигрывает тот, кто построит X или O в линию длиной, равной длине поля по вертикали, по горизонтали или по диагонали. Первая мысль, которая приходит в голову — это написать методы для каждого случая. Думаю, в этом случае хорошо подойдёт паттерн Chain of Responsobility. Определим интерфейс:
package ru.alexanderklimov.tictactoe;
public interface WinnerCheckerInterface
{
public Player checkWinner();
}
Так как Game наделён обязанностью выявлять победителя, он реализует этот интерфейс. Настало время создать виртуальных «лайнсменов», каждый из которых будет проверять свою сторону. Все они реализует интерфейс WinnerCheckerInterface.
package ru.alexanderklimov.tictactoe;
public class WinnerCheckerHorizontal implements WinnerCheckerInterface {
private Game game;
public WinnerCheckerHorizontal(Game game) {
this.game = game;
}
public Player checkWinner() {
Square[][] field = game.getField();
Player currPlayer;
Player lastPlayer = null;
for (int i = 0, len = field.length; i < len; i++) {
lastPlayer = null;
int successCounter = 1;
for (int j = 0, len2 = field[i].length; j < len2; j++) {
currPlayer = field[i][j].getPlayer();
if (currPlayer == lastPlayer && (currPlayer != null && lastPlayer != null)) {
successCounter++;
if (successCounter == len2) {
return currPlayer;
}
}
lastPlayer = currPlayer;
}
}
return null;
}
}
package ru.alexanderklimov.tictactoe;
public class WinnerCheckerVertical implements WinnerCheckerInterface {
private Game game;
public WinnerCheckerVertical(Game game) {
this.game = game;
}
public Player checkWinner() {
Square[][] field = game.getField();
Player currPlayer;
Player lastPlayer = null;
for (int i = 0, len = field.length; i < len; i++) {
lastPlayer = null;
int successCounter = 1;
for (int j = 0, len2 = field[i].length; j < len2; j++) {
currPlayer = field[j][i].getPlayer();
if (currPlayer == lastPlayer && (currPlayer != null && lastPlayer != null)) {
successCounter++;
if (successCounter == len2) {
return currPlayer;
}
}
lastPlayer = currPlayer;
}
}
return null;
}
}
package ru.alexanderklimov.tictactoe;
public class WinnerCheckerDiagonalLeft implements WinnerCheckerInterface {
private Game game;
public WinnerCheckerDiagonalLeft(Game game) {
this.game = game;
}
public Player checkWinner() {
Square[][] field = game.getField();
Player currPlayer;
Player lastPlayer = null;
int successCounter = 1;
for (int i = 0, len = field.length; i < len; i++) {
currPlayer = field[i][i].getPlayer();
if (currPlayer != null) {
if (lastPlayer == currPlayer) {
successCounter++;
if (successCounter == len) {
return currPlayer;
}
}
}
lastPlayer = currPlayer;
}
return null;
}
}
package ru.alexanderklimov.tictactoe;
public class WinnerCheckerDiagonalRight implements WinnerCheckerInterface {
private Game game;
public WinnerCheckerDiagonalRight(Game game) {
this.game = game;
}
public Player checkWinner() {
Square[][] field = game.getField();
Player currPlayer;
Player lastPlayer = null;
int successCounter = 1;
for (int i = 0, len = field.length; i < len; i++) {
currPlayer = field[i][len - (i + 1)].getPlayer();
if (currPlayer != null) {
if (lastPlayer == currPlayer) {
successCounter++;
if (successCounter == len) {
return currPlayer;
}
}
}
lastPlayer = currPlayer;
}
return null;
}
}
Проинициализируем их в конструкторе Game:
// "Судьи". После каждого хода они будут проверять,
// нет ли победителя
private WinnerCheckerInterface[] winnerCheckers;
// Инициализация "судей"
winnerCheckers = new WinnerCheckerInterface[4];
winnerCheckers[0] = new WinnerCheckerHorizontal(this);
winnerCheckers[1] = new WinnerCheckerVertical(this);
winnerCheckers[2] = new WinnerCheckerDiagonalLeft(this);
winnerCheckers[3] = new WinnerCheckerDiagonalRight(this);
Реализация checkWinner():
public Player checkWinner() {
for (WinnerCheckerInterface winChecker : winnerCheckers) {
Player winner = winChecker.checkWinner();
if (winner != null) {
return winner;
}
}
return null;
}
Возвращаемся в класс активности. Победителя проверяем после каждого хода. Добавим код в метод onClick() класса Listener
public void onClick(View view)
{
Button button = (Button) view;
Game g = game;
Player player = g.getCurrentActivePlayer();
if (makeTurn(x, y))
{
button.setText(player.getName());
}
Player winner = g.checkWinner();
if (winner != null)
{
gameOver(winner);
}
if (g.isFieldFilled())
{ // в случае, если поле заполнено
gameOver();
}
}
Метод gameOver() реализован в 2-х вариантах.
Метод makeTurn() проверяет очерёдность хода игрока, а метод refresh() обновляет состояние поля:
Весь код активности.
// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
package ru.alexanderklimov.as34;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private TableLayout tableLayout;
private Button[][] buttons = new Button[3][3];
private Game game;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tableLayout = findViewById(R.id.tableLayout);
game = new Game();
buildGameField(); // создание игрового поля
game.start(); // будет реализован позже
}
private void buildGameField() {
Square[][] field = game.getField();
for (int i = 0, lenI = field.length; i < lenI; i++) {
TableRow row = new TableRow(this); // создание строки таблицы
for (int j = 0, lenJ = field[i].length; j < lenJ; j++) {
Button button = new Button(this);
buttons[i][j] = button;
button.setOnClickListener(new Listener(i, j)); // установка слушателя, реагирующего на клик по кнопке
row.addView(button, new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT,
TableRow.LayoutParams.WRAP_CONTENT)); // добавление кнопки в строку таблицы
button.setWidth(160);
button.setHeight(160);
}
tableLayout.addView(row,
new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT,
TableLayout.LayoutParams.WRAP_CONTENT)); // добавление строки в таблицу
}
}
public class Listener implements View.OnClickListener {
private int x;
private int y;
Listener(int x, int y) {
this.x = x;
this.y = y;
}
public void onClick(View view) {
Button button = (Button) view;
Game g = game;
Player player = g.getCurrentActivePlayer();
if (makeTurn(x, y)) {
button.setText(player.getName());
}
Player winner = g.checkWinner();
if (winner != null) {
gameOver(winner);
}
if (g.isFieldFilled()) { // в случае, если поле заполнено
gameOver();
}
}
}
private void gameOver(Player player) {
CharSequence text = "Player \"" + player.getName() + "\" won!";
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
game.reset();
refresh();
}
private void gameOver() {
CharSequence text = "Draw";
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
game.reset();
refresh();
}
private boolean makeTurn(int x, int y) {
return game.makeTurn(x, y);
}
private void refresh() {
Square[][] field = game.getField();
for (int i = 0, len = field.length; i < len; i++) {
for (int j = 0, len2 = field[i].length; j < len2; j++) {
if (field[i][j].getPlayer() == null) {
buttons[i][j].setText("");
} else {
buttons[i][j].setText(field[i][j].getPlayer().getName());
}
}
}
}
}
Оригинальные исходники от автора статьи уже недоступны, но по оставленным записям я восстановил работоспособность приложения, хотя, конечно, его можно ещё улучшить.
SavvaVyatkin/CatDogToe - правильный вариант игры с участием котов. Картинка для статьи взята из этого проекта.