Начальная объектно-ориентированная текстовая RPG

В процессе написания текстовой RPG в Java я использую объектно-ориентированное программирование. Я довольно новичок в программировании и в настоящее время изучаю через CodeHS и кодируя это в своей песочнице, любая помощь была бы весьма признательна.

Моя самая большая борьба связана с отношением между классами Player и Enemy и тем, как они относятся к классу Creature. Такие методы, как roleToNumber(), также кажутся не лучшим способом делать что-то, но я не уверен, как проще реализовать подобную систему.

Это класс, который обрабатывает входы и выходы, а также создает встречи и фазы бит:

import java.util.Scanner;
public class MyProgram extends ConsoleProgram
{

    public void run()
    {
        Scanner readName = new Scanner(System.in);
        System.out.print("Enter your name: ");
        String userName = readName.nextLine();

        Scanner readRole = new Scanner(System.in);
        System.out.print("Choose your role (Fighter, Ranger, Arcanist): ");
        String userRole = readRole.nextLine();
        while(true){
            if(userRole.equalsIgnoreCase("Fighter") || userRole.equalsIgnoreCase("Ranger") || userRole.equalsIgnoreCase("Arcanist")){
                break;
            }else{
                System.out.println("Choose a valid role");
                readRole = new Scanner(System.in);
                System.out.print("Choose your role (Fighter, Ranger, Arcanist): ");
                userRole = readRole.nextLine();
            }
        }
        //a demo of all of the systems
        System.out.println("");
        Player player = new Player(userName, userRole);

        scene(player, "a mansion");
        if(!player.isDead()){
            scene(player, "a rock");
        }

    }
    public String attack(Creature one, Creature two){
        int a = one.attack(two);
        return one.getName() + " hit " + two.getName() + " for " + a + " damage.";
    }

    public void battle(Player one, Creature two){
        System.out.println(one);
        System.out.println(two);

        while(true){
            Scanner readChoice = new Scanner(System.in);
            System.out.print("\nWhat do you want to do (Attack, Run, Status, Use Potion): ");
            String userChoice = readChoice.nextLine();
            while(true){
                if(!userChoice.equalsIgnoreCase("Status") && !userChoice.equalsIgnoreCase("Run") && !userChoice.equalsIgnoreCase("Attack") && !userChoice.equalsIgnoreCase("Use Potion")){
                    System.out.println("Choose a valid choice");
                    readChoice = new Scanner(System.in);
                    System.out.print("\nWhat do you want to do (Attack, Run, Status, Use Potion): ");
                    userChoice = readChoice.nextLine();
                }else{
                    break;
                }
            }
            if(userChoice.equalsIgnoreCase("Status")){
                System.out.println(one.status());

                continue;
            }

            if(userChoice.equalsIgnoreCase("Use Potion")){
                System.out.println(one.useHealthPotion());
                System.out.println(one.status());

                continue;
            }

            if(userChoice.equalsIgnoreCase("Run")){
                int run = (int)(Math.random() * 100 + 1);
                if(run >= 50){
                    System.out.println("You successfully run.");
                    break;
                }else{
                    System.out.println("You fail at running.");

                }

            }else if(userChoice.equalsIgnoreCase("Attack")){
                System.out.println(attack(one, two));
                System.out.println(two.status());

            }
            if(!two.isDead()){
                System.out.println(attack(two, one));
                System.out.println(one.status());

                if(one.isDead()){
                    System.out.println("You died!");
                    break;
                }
            }else{
                System.out.println("You killed " + two.getName() + "\n");
                System.out.println("You gained " + one.gainXp(two) + " exp");
                if(one.checkXp()){
                    System.out.println("You leveled up, your health is restored!");
                    System.out.println("You have " + one.getXp() + " exp");
                }else{
                    System.out.println("You have " + one.getXp() + " exp");
                }
                System.out.println(one + "\n");
                break;
            }
        }
    }
    public void scene(Player one, String description){
        System.out.println(one.getName() + " arrives at " + description);

        int x = (int)(Math.random() * 3 + 1);

        for(int i = 0; i < x; i++){
            if(one.isDead()){
                break;
            }
            Enemy randEnemy = new Enemy(one.getLevel());
            System.out.println("\nYou encounter " + randEnemy.getName() + " the " + randEnemy.getRole());
            battle(one, randEnemy);
        }

    }
}

Этот класс Creature, который имеет базовые функции, разделяемые между Players и Enemies:

public class Creature{

    public String name;
    public String role;
    public int maxHp;
    public int maxAtt;
    public int minAtt;
    public int level;
    public int curHp;

    public Creature(String name, String role){
        this.name = name;
        this.role = role;
    }

    public int attack(Creature other){
        int att = (int)(Math.random() * (this.maxAtt - this.minAtt + 1) + this.minAtt);
        other.curHp -= att;
        return att;
    }

    public boolean isDead(){
        if(this.curHp <= 0){
            return true;
        }else{
            return false;
        }
    }
    public int getCurHp(){
        return curHp;
    }
    public void setCurHp(int hp){
        if(hp >= maxHp - curHp){
            curHp = maxHp;
        }else{
            curHp = hp;
        }
    }

    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name; 
    }
    public String getRole(){
        return role;
    }
    public void setRole(String role){
        this.role = role;
    }
    public int getMaxHp(){
        return maxHp;
    }
    public int getLevel(){
        return level;
    }
    public String status(){
        return name + " has " + curHp + "/" + maxHp + " health.";
    }

    public String toString(){
        return name +  " the " + role + " is level " + level + " with " + curHp + "/" + maxHp + " HP and an attack of " + maxAtt + "-" + minAtt;
    }

}

Это класс Player:

public class Player extends Creature{
    public int xp;
    private int hpPotions = 3;

    public Player(String name, String role){
        super(name, role);
        this.level = 1;
        rollStats();
        this.curHp = maxHp;
    }    

    public String useHealthPotion(){
        if(hpPotions >= 1 ){
            this.setCurHp(this.getCurHp() + 25);
            hpPotions--;
            return hpPotions + " potions left.";
        }else{
            return "No potions to use.";
        }
    }
    public int getHealthPotion(){
        return hpPotions;
    }
    public void setHealthPotions(int newHpPotions){
        hpPotions = newHpPotions;
    }
    public int gainXp(Creature other){
        int x = other.getLevel();
        int gainedXp = x * (int)(Math.random() * (60 - 21) + 20);
        xp += gainedXp;
        return gainedXp;
    }

    public boolean checkXp(){
        if(xp >= level * 40){
            xp = xp - (level  * 40);
            levelUp();

            return true;
        }else{
            return false;
        }
    }
    public String status(){
        return name + " has " + curHp + "/" + maxHp + " health.";
    }
    public String getXp(){
        return xp + "/" + (level * 40);
    }

    //rolling for intitial stats
    public void rollStats(){
        int hp = 0;
        int att = 0;
        switch(roleToNumber()){
            case 1: hp = 16; att = 10; break;
            case 2: hp = 13; att = 13; break;
            case 3: hp = 12; att = 14; break;
        }
        maxHp = (roll(6) + hp);
        maxAtt = (roll(6) + att); 
        minAtt = (maxAtt - 3); 

    }

    private int roll(int sides){
        int aRoll = (int)(Math.random() * sides + 1);
        return aRoll;
    }
    //Changes the inputed role to a number
    private int roleToNumber(){
        if(role.equalsIgnoreCase("Fighter")){
            return 1;
        }else if(role.equalsIgnoreCase("Ranger")){
            return 2;
        }else if(role.equalsIgnoreCase("Arcanist")){
            return 3;
        }else{
            return 0;
        }
    }
    //coding for level up with modifiers based on role
    public void levelUp(){
        level++;
        int hp = 0;
        int att = 0;
        switch(roleToNumber()){
            case 1: hp = 24; att = 14; break;
            case 2: hp = 19; att = 19; break;
            case 3: hp = 16; att = 22; break;

        }
        maxHp += (hp * Math.random()/2 + .7);
        maxAtt += (att * Math.random()/2 + .7);
        minAtt = maxAtt - 3;
        this.curHp = maxHp;

    }
}

И это класс Enemy:

public class Enemy extends Creature{

    public Enemy(int leveled){
        super("Filler", "Filler");
        this.level = 1;
        this.setName(randomName());
        this.setRole(randomRole());

        rollStats();
        if(leveled > 1){
            for(int i = 1; i < leveled; i++){
                levelUp();
            }
        }
        this.curHp = maxHp;

    }
    //pulls a random name from an array
    public String randomName(){
        String[] names = {"Spooky", "Scary", "Yup"};
        int index = (int)(Math.random() * names.length);

        return names[index];

    }
    //pulls a random role from an array, these are pased to roleToNumber
    public String randomRole(){
        String[] roles = {"Orc", "Goblin", "Dragon"};
        int index = (int)(Math.random() * roles.length);

        return roles[index];
    }
    public void rollStats(){
        int hp = 0;
        int att = 0;
        switch(roleToNumber()){
            case 1: hp = 16; att = 10; break;
            case 2: hp = 13; att = 13; break;
            case 3: hp = 12; att = 14; break;
        }
        maxHp = (roll(6) + hp);
        maxAtt = (roll(6) + att); 
        minAtt = (maxAtt - 3); 

    }

    private int roll(int sides){
        int aRoll = (int)(Math.random() * sides + 1);
        return aRoll;
    }

    private int roleToNumber(){
        if(role.equalsIgnoreCase("Orc")){
            return 1;
        }else if(role.equalsIgnoreCase("Goblin")){
            return 2;
        }else if(role.equalsIgnoreCase("Dragon")){
            return 3;
        }else{
            return 0;
        }
    }

    public void levelUp(){
        level++;
        int hp = 0;
        int att = 0;
        switch(roleToNumber()){
            case 1: hp = 24; att = 14; break;
            case 2: hp = 19; att = 19; break;
            case 3: hp = 16; att = 22; break;
        }
        maxHp += (hp * Math.random()/2 + .5);
        maxAtt += (att * Math.random()/2 + .5);
        minAtt = maxAtt - 3;
        this.curHp = maxHp;
    }
}
29 голосов | спросил WatCow 15 32017vEurope/Moscow11bEurope/MoscowWed, 15 Nov 2017 06:00:38 +0300 2017, 06:00:38

4 ответа


23

Во-первых, позвольте мне сказать: «Хорошая работа». Этот код подходит для тех, кто находится в середине обучения.

С учетом этого, теперь я разорву его на клочки. ; - >

Давайте проигнорируем вашу основную программу, у которой есть свой набор проблем, в пользу сосредоточения на иерархии классов по вашему запросу.

Используйте основные функции OOP

Первое, что я замечаю, это то, что вы действительно не «делаете» классы правильно. Например, рассмотрите Creature:

public class Creature{

    public String name;
    public String role;
    public int maxHp;
    public int maxAtt;
    public int minAtt;
    public int level;
    public int curHp;

    public Creature(String name, String role){
        this.name = name;
        this.role = role;
    }

Одна вещь, которую вы должны были узнать о классах /объектах: когда вы конструируете экземпляр класса, он должен быть готов к работе! Есть несколько исключений из этого , и все они неудобны.

В вашем Creature вы нарушаете это правило. Ваш конструктор устанавливает name и role и оставляет все данные другого экземпляра не установленными.

Существует несколько способов. Самый простой из них - передать все данные экземпляра в качестве параметров или вычислить его на основе параметров. Например, currHp = maxHp будет вычислять, а передача maxHp в качестве аргумента конструктора будет передавать его.

В качестве альтернативы вы можете зависеть от некоторых методов класса (sub) для возврата нужных вам значений: maxHp = getMaxHp()

Не используйте case, используйте подклассы

Если вы пишете OO-код, и вы обнаруживаете, что используете аргумент case, есть хороший шанс, что вам нужен какой-то подкласс. Не всегда - ваш метод может включать какие-то внешние данные, но если вы включаете внутренние данные для изменения своего поведения, вы, вероятно, можете использовать подкласс для получения этого результата.

В вашем случае вы включаете roleToNumber(), что вдвойне дорого, так как вы каждый раз выполняете чистую функцию без кэширования результата.

Вместо этого создайте подклассы. Нажимайте большую часть «реального» кода в классы Enemy и Player и используйте класс class Orc extends Enemy, чтобы предоставить описание строки, числовое статистика и любой специальный текст атаки, необходимый для игры:

class Orc
    extends Enemy
{
    public Orc() {
        super(randomName(), "Orc");
    }

    public getMaxHp() { return roll(6) + 16; }
    public getMaxAttack() { return roll(6) + 10; }
}

Вы также можете сделать то же самое для классов игрока, за исключением того, что имя не является случайным:

class Fighter
    extends Player 
{
    public Fighter(String name) 
    {
        super(name, "Fighter");
    }

    ... stats ...
}

Это должно позволить вам исключить roleToNumber и все, что его использует, - просто закодировать переключатель switch как метод, который возвращает правильный ответ напрямую.

Способы записи, которые фактически выполняют то, что вы делаете

Я замечаю, что вы попали в ловушку написания методов, которые не делают то, что пытается сделать ваш код:

public String useHealthPotion(){
    if(hpPotions >= 1 ){
        this.setCurHp(this.getCurHp() + 25);
        hpPotions--;
        return hpPotions + " potions left.";
    }else{
        return "No potions to use.";
    }
}

Рассмотрим setCurHp(). Что оно делает? Он устанавливает текущий Hp. Это замечательно, но каков ваш код на самом деле? Ваш код поднимает текущий HP с максимальным значением.

Почему бы вам не написать метод, который поднимает текущий HP, с максимальным значением maxHp? Его можно было бы назвать public raiseCurrHp(int howmuch);

И тогда вы можете написать:

public String useHealthPotion(){
    if(hpPotions >= 1 ){
        this.raiseCurrHp(25);
        hpPotions--;
        return hpPotions + " potions left.";
    }else{
        return "No potions to use.";
    }
}

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

(С другой стороны, gainXp - хороший метод, который делает это. Итак, продолжайте делать это!)

ответил Austin Hastings 15 32017vEurope/Moscow11bEurope/MoscowWed, 15 Nov 2017 07:07:41 +0300 2017, 07:07:41
12

Вы написали хорошую программу, и ее код читается как хорошая история. В некоторых местах это немного длиннее, но это можно обойти. Вы ввели понятий игры (игроков, врагов) в понятном для понимания способом.


Я согласен с ответом Остина в большинстве случаев. Но у меня другое мнение о количестве необходимых на самом деле классов.

В настоящее время все игроки ведут себя одинаково. Они просто отличаются количеством очков здоровья и некоторыми другими небольшими деталями. Один из способов моделирования этого - действительно создавать подклассы. Я предпочитаю создавать подклассы только в том случае, если поведение не отличается от параметров data ​​em> или .

Простым способом является определение enum для ролей. Это выглядит так:

public enum PlayerRole {
    Fighter, Ranger, Arcanist
}

Самое приятное в перечислениях заключается в том, что вы можете использовать их непосредственно в операторах switch. Это делает ненужным метод roleToNumber.

switch (player.getRole()) {
case Fighter: return 17;
case Ranger: return 23;
case Arcanist: return 20;
}
throw new IllegalStateException();

(Большинство программистов на Java записывают константы enum во всех прописных строках, как и все остальные константы. Я предпочитаю использовать C # для смешанного случая.)


Использование Math.random более сложное, чем необходимо. Вы должны рассмотреть создание своего собственного класса для этого, чтобы сосредоточиться на случайности, в которой вы действительно нуждаетесь. Я думаю о чем-то вроде этого:

public class GameRandom {
    private final Random rnd;

    public GameRandom(Random rnd) {
        this.rnd = rnd;
    }

    public int d6() {
        return between(1, 6);
    }

    public int between(int minInclusive, int maxInclusive) {
        return rnd.nextInt(1 + maxInclusive - minInclusive) + minInclusive;
    }
}

Одна хорошая вещь в этом классе заключается в том, что вы можете использовать генератор чисел fixed , который вы создаете с помощью new GameRandom(new Random(0)). Это полезно для тестирования вашего кода. Потому что во время тестирования случайность трудно проверить. Это намного проще, когда ваши тестовые случаи всегда ведут себя одинаково. Класс Random генерирует точный результат в течение многих лет, и, вероятно, это даже гарантировано.


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

private final Scanner input = new Scanner(System.in);
private final PrintStream output = System.out;

public <T> T choose(String prompt, T... choices) {
    String choiceNames = Stream.of(choices).map(Object::toString).collect(Collectors.joining(", "));
    String actualPrompt = String.format("%s (%s): ", prompt, choiceNames);

    while (true) {
        output.print(actualPrompt);
        String answer = input.nextLine();

        for (T choice : choices) {
            if (answer.equalsIgnoreCase(choice.toString())) {
                return choice;
            }
        }

        output.println("Invalid choice.");
    }
}

Вы можете использовать этот метод следующим образом:

String role = choose("Choose your role", "Warrior", "Archer", "Alchimist");
switch (role) {
case "Warrior": hp = 5; break;
case "Archer": hp = 8; break;
case "Alchimist": hp = 27; break;
}

Или даже так:

PlayerRole role = choose("Choose your role", PlayerRole.values());

Больше нет возможности для опечаток - этот код, и вы можете просто добавить новую роль в PlayerRole, и он работает немедленно. Это удобство является одной из причин записи констант перечисления в Mixed Case вместо ALL_UPPERCASE.


Вместо System.out.println вы можете использовать System.out.printf. Выгода заключается в том, что вы сразу видите общую схему вывода, афактические данные отформатированы в этот шаблон. Пример:

System.out.printf("%s hit %s with a %s, causing %d damage.%n", attacker, attackee,  weapon, damage);

Заполнители % могут казаться трудными сначала, но миллионы людей уже узнали их, и они весьма полезны.

ответил Roland Illig 15 32017vEurope/Moscow11bEurope/MoscowWed, 15 Nov 2017 08:40:25 +0300 2017, 08:40:25
7
  

В процессе написания текстовой RPG в Java я использую объектно-ориентированное программирование. Я довольно новичок в программировании и в настоящее время изучаю через CodeHS и кодируя это в своей песочнице, любая помощь была бы весьма признательна.

Это отличный ранний проект для обучения программированию. Если вас интересует современное состояние в текстовом приключенческом программировании, рассмотрите возможность чтения на Inform7. Это удивительный язык программирования и среда.

Как я уже отмечал в комментарии, я предостерегаю вас от использования OO чрезвычайно тщательно в этом домене. Очень легко попытаться кодировать правила вашей игры в правила системы типов, которые не имеют никакого отношения к вашему игровому домену.

  

Моя самая большая борьба связана с отношениями между классом Player и Enemy и тем, как они относятся к классу существа

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

Несколько вещей, которые нужно искать, когда критиковать иерархии классов: (1) где я дублирую код? и (2) какие проектные проспекты я отрезаю?

Например: игрок и враг расширяют свое существо; это хорошо. Оба игрока и врага имеют почти одинаковые методы повышения уровня, которые не имеют никакого отношения друг к другу; почему это? В то же время, по-видимому, только игроки могут пить зелья; почему это?

Рассмотрите возможность сделать шаг назад и более абстрактно рассказать о домене вашей игры. Ваша игра должна:

  • Следите за местами игровых объектов, таких как мечи, фонари и сумки ... и игроки и монстры. Это понимание должно сказать вам, что иерархия классов еще недостаточно глубока; у игроков есть что-то общее с мечами; они могут быть внутри чего-то.
  • Следите за содержимым контейнеров, таких как коробки и сумки ... и комнаты! Какая разница между комнатой и коробкой? Не важно. Они содержат вещи. Таким образом, коробки имеют что-то общее с комнатами.
  • Разрешить попытки действий игроков: если игрок пытается поместить меч в сумку, либо он преуспеет, либо он терпит неудачу; в любом случае, что-то происходит. Если игрок пытается атаковать вампира, что-то происходит. Действия - это проблема программы, поэтому, возможно, должен быть класс для действий.
  • Разрешить попытки врагов - и не все NPC - это «враги», поэтому, возможно, «враг» - это неправильное имя. У игроков и противников есть что-то общее: они оба принимают действия, которые разрешаются в контексте состояния игры и правил игры.

Моя точка зрения заключается в том, что прямо сейчас вы концентрируетесь на кодировании слишком большой специфики в своей системе. Подумайте о том, что реальные отношения «есть своего рода» и «могут делать» находятся в вашем игровом домене и соответствующим образом разрабатывают иерархию наследования и интерфейса.

ответил Eric Lippert 15 32017vEurope/Moscow11bEurope/MoscowWed, 15 Nov 2017 19:14:09 +0300 2017, 19:14:09
1

Гибкость

Класс Враг . Я бы назвал его NPC (персонажем без игрока). Вы не совершаете всех нечеловеческих существ как врагов , не так ли? Да, некоторые атакуют вас, или вам нужно будет атаковать их. Некоторым вам просто нужно будет использовать, обмануть ... Но, возможно, некоторые из них действительно будут предлагать помощь, например, давать предметы или информацию. Или они могут сопровождать вас в квесте. Затем другие предоставят вам задание /попросят вас о помощи. С некоторыми вы будете торговать ...

Насколько гибко вам нравится ваш RGB? Одиночный игрок или мультиплеер (MUD)? У вас есть определения существ, жестко закодированные в вашей программе, которые будут двигателем RPG. Поэтому, чтобы изменить /расширить, вам нужно будет отредактировать настоящий движок. Вместо этого я бы предложил, чтобы ваш движок RPG реализовал виртуальную машину . Таким образом, редактирование игрового мира обычно не потребует редактирования движка; вы даже можете делать изменения в текущей игре.

Если вы создаете MUD, такие механизмы существуют и свободно загружаются. Но тогда вы не можете делать что-то в Java. LP-MUD, например, написан на C и позволяет создавать очень сложные миры в форме виртуальной машины, определенной на объектно-ориентированном языке LPC; Администраторы, которые вступают в систему, как обычные игроки, могут редактировать /перепрограммировать мир, не прерывая его. Обычно один админ-плеер - бог, с максимальным доступом к редактированию и другими игроками-админами - это обычные игроки, которые были повышены до мастеров и имеют более или менее ограниченный доступ к редактированию.

ответил Heimdall 16 42017vEurope/Moscow11bEurope/MoscowThu, 16 Nov 2017 15:22:12 +0300 2017, 15:22:12

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132