Ako naprogramovať hru s umelou inteligenciou (AI) v Java – kameň, papier, nožnice

Kto z nás by nepoznal obľúbenú hru Kameň, papier, nožnice ktorú sme hrávali už ako deti. Koho by však vtedy napadlo, že jedného dňa si ju bude môcť naprogramovať vo svojom obľúbenom programovacom jazyku, napríklad v Jave.

Málokto vie, že Kameň, papier, nožnice (angl. rock, paper, scissors) je ručná hra, ktorá údajne vznikla v Číne, odkiaľ sa rozšírila do Japonska. Tam vznikla jej moderná podoba ako ju poznáme a začiatkom 20. storočia sa veľmi rýchlo rozšírila do celého sveta.

Kameň, papier, nožnice pravidlá

Pravidlá hry si určite nemusíme predstavovať. Ale ak by sa našiel niekto, kto ju ešte nepozná, tak v stručnosti ich spomenieme.

Hrajú ju najčastejšie dvaja hráči (môžu aj viacerí) a každý z nich si podvedome vyberie buď kameň, papier alebo nožnice. Na apel jedna, dva, tri, oba hráči naraz ukážu čo si vybrali – kameň (zavretá päsť), alebo nožnice (pomocou ukazováka a prostredníka), alebo papier (otvorená ruka). Výsledkom hry, ktoré sa hrá samozrejme na viac kôl, je remíza, výhra, alebo prehra.

Hráč, ktorý zahrá kameň, porazí iného hráča, ktorý si vyberie nožnice (kameň otupí nožnice), ale prehrá s tým, kto hral papier (papier obalí kameň). Hráč s papierom prehrá s hráčom, ktorý zvolil nožnice (nožnice prestrihnú papier). Ak si obaja hráči zvolia rovnaký tvar, hra je nerozhodná a zvyčajne sa opakuje, aby sa nerozhodný stav rozhodol.

Kameň, papier, nožnice sa často používa ako metóda spravodlivého výberu medzi deťmi, keď ide o to, ktoré dieťa bude niečo začínať ako prvé.

Vedeli ste že?
Pre Kameň, papier, nožnice sa organizovali aj rôzne súťažné turnaje. Napríklad víťazi turnajov si odniesli od 5000 do 20000 dolárov.

Kameň, papier, nožnice v Java

Dnes si túto hru spoločne naprogramujeme v Jave. Aby sme urobili túto hru pre hráčov zaujímavejšou na hranie, naprogramujeme si aj umelú inteligenciu (AI), ktorá sa nebude v každom kole rozhodovať náhodne, či použije kameň, papier, alebo nožnice, ale v každom kole si vyberie jednu z predvolených stratégií. Samozrejme hráč nebude tušiť, aké gesto si počítačové AI-čko práve vybralo.

Zaujímavosť

S robotom hru Kameň, papier, nožnice určite nehrajte. V roku 2012 výskumníci z laboratória Ishikawa Watanabe na Tokijskej univerzite vytvorili robotickú ruku, ktorá dokáže vyhrať každú hru proti ľudskému protivníkovi. Pomocou vysokorýchlostnej kamery robot v priebehu jednej milisekundy rozpozná, aký tvar vytvára ľudská ruka, a potom vytvorí zodpovedajúci víťazný tvar.

Koncept hry s AI

Hru budeme hrať na predom definovaný počet kôl, ktoré sa zadajú pri vytváraní inštancie hry. Keďže kolá hry sú veľmi rýchle a chceme si vyskúšať správanie AI, predvolenú (default) hodnotu na počet kôl stanovíme na 20.

Na začiatku každého kola budeme musieť zaznamenať výber hráča, vybrať AI stratégiu pre dané kolo a na základe nej určiť výber umelej inteligencie počítača. Z predvolených stratégií si AI vyberie náhodne, takže pre hráča bude problematické určiť, aké gesto počítač zahrá.

Potom skontrolujeme, kto si čo vybral a podľa toho pridelíme body, za výhru +1, za remízu alebo prehru 0 bodov. Hru vyhrá ten, kto ako prvý dosiahne 20 bodov.

Stratégie hry pre AI

Umelá inteligencia hry si bude zaznamenávať víťazné gestá hráča a na základe histórie sa rozhodne, aké gesto zahrať. Medzi základné stratégie bude patriť:

  • Náhodný vyber gesta
    (RandomStrategy)
  • Zopakovanie svojho gesta z predchádzajúceho kola
    (RepeatLastMoveStrategy)
  • Použitie proti-gesta na gesto protihráča z predchádzajúceho kola (CounterLastPlayerMoveStrategy)
  • Opakovanie najčastejšie zahratého gesta od protihráča (RepeatMostFrequentPlayerMoveStrategy)
  • Proti-gesto na najčastejšie zahraté gesto od protihráča (CounterMostFrequentPlayerMoveStrategy)

O tom, ktorá herná stratégia je najlepšia, diskutujú ľudia aj dnes. Snahou našich článkov je niečo nového čitateľa naučiť, ukázať mu ako riešiť niektoré problémy a najviac nás poteší, ak začne s programom experimentovať a vymýšľať si vlastné herné stratégie. Inšpirovať sa môžeš aj v tomto videu, alebo v článku na Rempton games po anglicky.

Pustime sa teda do naprogramovania hry v Jave.

Java Implementácia hry Kameň, Papier, Nožnice

Na implementáciu AI stratégie sa výborne hodí návrhový vzor Strategy.

Tento program bude jednoduchá konzolová hra, kde si hráč vyberie kameň, papier alebo nožnice a hrá proti AI počítača, ktoré si vyberá náhodne jednu z mnohých stratégii v každom kole. Víťaz je určený na základe pravidiel hry.

Enum Move

Enum Move definuje možné ťahy (kameň, papier, nožnice) a poskytuje metódu na porovnanie ťahov hráča a AI a metódu na určenie najlepšieho proti-ťahu na daný ťah.

package games.rockpaperscissors;

public enum Move {
    ROCK, PAPER, SCISSORS;

    // Method to check who won
    public static int compareMoves(Move player, Move ai) {
        // Draw
        if (player == ai) {
            return 0;
        }

        switch (player) {
            case ROCK:
                // Rock beats Scissors, loses to Paper
                return (ai == SCISSORS) ? 1 : -1;
            case PAPER:
                // Paper beats Rock, loses to Scissors
                return (ai == ROCK) ? 1 : -1;
            case SCISSORS:
                // Scissors beats Paper, loses to Rock
                return (ai == PAPER) ? 1 : -1;
        }
        return 0;
    }

    public static Move counterMove(Move move) {
        switch (move) {
            case ROCK:
                return Move.PAPER;
            case PAPER:
                return Move.SCISSORS;
            case SCISSORS:
                return Move.ROCK;
        }
        throw new IllegalArgumentException();
    }
}

Interface AIStrategy

Rozhranie AIStrategy definuje metódu nextMove(), ktorá rozhoduje, aký ťah AI urobí na základe histórie.

Špecifické stratégie
AI má rôzne stratégie, ktoré môže použiť. Stratégia sa vyberá náhodne pred každým kolom, čo znamená, že pre hráča je ťažké predvídať, aký ťah AI zvolí.

package games.rockpaperscissors.ai.strategies;

import games.rockpaperscissors.Move;
import java.util.List;

public interface AIStrategy {
    Move nextMove(List<Move> playerHistory, List<Move> aiHistory);
}

Trieda RandomStrategy
Náhodný výber gesta.

package games.rockpaperscissors.ai.strategies;

import games.rockpaperscissors.Move;
import java.util.List;
import java.util.Random;

public class RandomStrategy implements AIStrategy {
    private final Random random = new Random();

    @Override
    public Move nextMove(List<Move> playerHistory, List<Move> aiHistory) {
        return Move.values()[random.nextInt(Move.values().length)];
    }
}

Trieda RepeatLastMoveStrategy
Zopakovanie posledného gesta hráča.

package games.rockpaperscissors.ai.strategies;

import games.rockpaperscissors.Move;
import java.util.List;

public class RepeatLastMoveStrategy implements AIStrategy {
    @Override
    public Move nextMove(List<Move> playerHistory, List<Move> aiHistory) {
        return aiHistory.isEmpty()
                ? new RandomStrategy().nextMove(playerHistory, aiHistory)
                : aiHistory.get(aiHistory.size() - 1);
    }
}

Trieda CounterLastPlayerMoveStrategy
Proti-ťah na gesto hráča z predchádzajúceho kola.

package games.rockpaperscissors.ai.strategies;

import games.rockpaperscissors.Move;
import java.util.List;

public class CounterLastPlayerMoveStrategy implements AIStrategy {
    @Override
    public Move nextMove(List<Move> playerHistory, List<Move> aiHistory) {
        if(!playerHistory.isEmpty()) {
            Move lastPlayer = playerHistory.get(playerHistory.size() - 1);
            return Move.counterMove(lastPlayer);
        }
        return new RandomStrategy().nextMove(playerHistory, aiHistory);
    }
}

Trieda RepeatMostFrequentPlayerMoveStrategy
Zopakovanie gesta, ktoré hráč najčastejšie používal.

package games.rockpaperscissors.ai.strategies;

import games.rockpaperscissors.Move;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class RepeatMostFrequentPlayerMoveStrategy implements AIStrategy {
    @Override
    public Move nextMove(List<Move> playerHistory, List<Move> aiHistory) {
        if(!playerHistory.isEmpty()) {
            Map<Move, Integer> moveFrequency = new HashMap<>();
            for(Move move : playerHistory) {
                moveFrequency.put(move, moveFrequency.getOrDefault(move, 0) + 1);
            }
            return Collections.max(moveFrequency.entrySet(), Map.Entry.comparingByValue()).getKey();
        }
        return new RandomStrategy().nextMove(playerHistory, aiHistory);
    }
}

Trieda CounterMostFrequentPlayerMoveStrategy
Proti-ťah na gesto, ktoré hráč najčastejšie používal.

package games.rockpaperscissors.ai.strategies;

import games.rockpaperscissors.Move;
import java.util.List;

public class CounterMostFrequentPlayerMoveStrategy implements AIStrategy {
    @Override
    public Move nextMove(List<Move> playerHistory, List<Move> aiHistory) {
        if(!playerHistory.isEmpty()) {
            Move mostFrequentMove = new RepeatMostFrequentPlayerMoveStrategy().nextMove(playerHistory, aiHistory);
            // Counter the most frequent player's move
            return Move.counterMove(mostFrequentMove);
        }
        return new RandomStrategy().nextMove(playerHistory, aiHistory);
    }
}

Trieda RockPaperScissors
Trieda RockPaperScissorsGame implementuje celkovú hernú logiku a spravuje herný tok, záznam skóre a výber stratégií.

package games.rockpaperscissors;

import games.rockpaperscissors.ai.strategies.*;
import java.util.*;

public class RockPaperScissors {
    private final int totalRounds;
    private final List<Move> playerHistory = new ArrayList<>();
    private final List<Move> aiHistory = new ArrayList<>();
    private int playerScore = 0;
    private int aiScore = 0;

    private final List<AIStrategy> aiStrategies = Arrays.asList(
            new RandomStrategy(),
            new RepeatLastMoveStrategy(),
            new CounterLastPlayerMoveStrategy(),
            new RepeatMostFrequentPlayerMoveStrategy(),
            new CounterMostFrequentPlayerMoveStrategy()
    );

    public RockPaperScissors(int totalRounds) {
        this.totalRounds = totalRounds;
    }

    private Move getPlayerMove(int input) {
        switch (input) {
            case 1:
                return Move.ROCK;
            case 2:
                return Move.PAPER;
            case 3:
                return Move.SCISSORS;
            default:
                Move playerMove = new RandomStrategy().nextMove(playerHistory, aiHistory);
                System.out.println("Chybny vstup, vyberam za hraca: " + playerMove);
                return playerMove;
        }
    }

    public void play() {
        Scanner scanner = new Scanner(System.in);
        Random random = new Random();

        while (playerScore < totalRounds && aiScore < totalRounds) {
            System.out.println("Vyber si: (1) Kamen, (2) Papier, (3) Noznice");
            Move playerMove = getPlayerMove(scanner.nextInt());
            Move aiMove = aiStrategies.get(random.nextInt(aiStrategies.size()))
                    .nextMove(playerHistory, aiHistory);
            System.out.println("HRAC vybral: " + playerMove);
            System.out.println("AI vybrala: " + aiMove);

            // Compare moves
            int result = Move.compareMoves(playerMove, aiMove);
            if (result > 0) {
                System.out.println("Vyhral si toto kolo.");
                playerScore++;
            } else if (result < 0) {
                System.out.println("AI vyhrala toto kolo.");
                aiScore++;
            }

            // Record moves
            playerHistory.add(playerMove);
            aiHistory.add(aiMove);

            System.out.println("Aktualne skore - Hrac: " + playerScore + " | AI: " + aiScore);
            System.out.println();
        }

        if(playerScore >= totalRounds) {
                System.out.println("Gratulujeme! Vyhral si tuto hru!");
        }
        else {
            System.out.println("AI vyhrala tuto hru! Skus ju porazit.");
        }
    }
}

Trieda Main

import games.rockpaperscissors.RockPaperScissors;

public class Main {
    public static void main(String[] args) {
        RockPaperScissors game = new RockPaperScissors(20);
        game.play();
    }
}

Ako hra funguje

  1. Hráč si vyberie ťah (kameň, papier, nožnice) pomocou vstupu.
  2. AI vyberie svoju stratégiu náhodne a na základe nej si zvolí gesto.
  3. Hra porovná ťahy a aktualizuje skóre.
  4. Hra pokračuje, až kým jeden z hráčov nedosiahne 20 bodov.

Výstup 3-kolovej hry môže vyzerať nasledovne:

Výstup 3-kolovej hry Rock Paper Scissors

V tejto Java hre hrá umelá inteligencia veľmi dobre a vo viackolovej hre vyhrať nie je také jednoduché.

Tu si môžeš stiahnuť zdrojové kódy hry RockPaperScissors v Java.

Ak hľadáš prácu a si Java programátor, prezri si naše benefity pre zamestnancov a reaguj na najnovšie ponuky práce.

O autorovi

Jozef Wagner

Java Developer Senior

Viac ako 10 rokov programujem v Jave, momentálne pracujem v msg life Slovakia ako Java programátor senior a pomáham zákazníkom implementovať ich požiadavky do poistného softvéru Life Factory. Vo voľnom čase si rád oddýchnem v lese, prípadne si zahrám nejakú dobrú počítačovú hru.

Daj nám o sebe vedieť