Борьба с врагами (РПГ на Java)


Я написал программу, которая имитирует бой между игроком и врагом, чтобы набраться опыта в программировании таких ролевых игр. Прежде всего, это было важно для меня, чтобы иметь хорошее, объектно-ориентированного проектирования. Критика в этом отношении явно нужные.

Описание навыков:

Fireball (needs 1 mana) - this is a skill that causes damage on an enemy. Nothing special.
Healing (needs 3 mana) - this skill heals the caster.
Knock Out (needs 2 mana) - this skill knocks the enemy out. He cant cast skills in this or in the next round. He also gets damage.
Poisoning (needs 3 mana) - the enemy gets damage and is poisoned in the next rounds. So he gets damage in the next rounds.

Извините за мой плохой английский. Я буду улучшать :)

Программа здесь можно протестировать

Вот мои исходные файлы

Game.java

public class Game {
    private Player player;
    private NPC enemy;

    private int score;

    public Game() {
        player = new Player("Player", 20, 5, 5);
        enemy = new NPC("Enemy", 20, 5, 5);
    }

    public boolean fight() {
        UF.println(player.getName() + " fights against " + enemy.getName(), 1000);
        int rounds = 1;

        while(player.getLife() > 0 && enemy.getLife() > 0) {
            UF.println(" --- Round " + rounds + " --- \n", 1000);
            UF.println(player.getName() + " Stats", 0);
            UF.println("Life: " + UF.displayGraphical(player.getLife(), player.getLifeMax(), "#"), 0);
            UF.println("Mana: " + UF.displayGraphical(player.getMana(), player.getManaMax(), "@") + "\n", 1000);
            UF.println(enemy.getName() + " Stats", 0);
            UF.println("Life: " + UF.displayGraphical(enemy.getLife(), enemy.getLifeMax(), "#"), 0);
            UF.println("Mana: " + UF.displayGraphical(enemy.getMana(), enemy.getManaMax(), "@") + "\n", 1000);
            player.isPoisoned();
            if(player.getLife() <= 0) break;
            player.chooseSkill(enemy);
            if(enemy.getLife() <= 0 || player.getLife() <= 0) break;
            enemy.isPoisoned();
            if(enemy.getLife() <= 0) break;
            enemy.chooseSkill(player);
            rounds++;
        }

        if(player.getLife() > 0 && enemy.getLife() <= 0) {
            UF.println("YOU HAVE WON! YOU ARE NOW STRONGER", 2000);
            UF.println("PREPARE FOR THE NEXT FIGHT!\n", 2000);
            score++;
            enemy.getStronger(1.2);
            player.getStronger();
            return true;
        } else {
            UF.println("Game over!", 0);
            UF.println("You reached " + score + " Points. Gratulation!", 2000);
            return false;
        }
    }

    public void mainLoop() {
        while(fight());
    }

    public static void main(String[] args) {
        Game game = new Game();
        game.mainLoop();
    }
}

UF.java

// name stands for "useful functions"
public class UF {
    public static void println(String text, int time) {
        System.out.println(text);
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static String displayGraphical(int value1, int value2, String symbol) {
        if(value2 < value1) {
            return "error";
        }

        String text = "[";
        int numberOfSymbols = 0;
        int numberOfWhiteSpace = 20;

        float filledOut = (float)value1 / (float)value2;
        numberOfSymbols = (int)(filledOut * 20);
        for(int i = 0; i < numberOfSymbols; i++) {
            text += symbol;
        }
        numberOfWhiteSpace -= numberOfSymbols;
        for(int i = 0; i < numberOfWhiteSpace; i++) {
            text += " ";
        }
        text += "] (" + value1 + " / " + value2 + ")";
        return text;
    }
}

Being.java

import java.util.Scanner;

public class Being {
    // basic values
    private String name;

    protected int lifeMax;
    protected int manaMax;
    protected int powerMax;

    protected int lifeActual;
    protected int manaActual;
    protected int powerActual;

    // skills
    protected Skill fireball;
    protected Skill knockOut;
    protected Skill healing;
    protected Skill poisoning;

    // state values
    protected boolean alive;
    protected boolean isKnockedOut;
    protected int[] isPoisoned;

    public Being(String name, int lifeMax, int manaMax, int powerMax) {
        this.name = name;
        this.lifeMax = lifeMax;
        this.manaMax = manaMax;
        this.powerMax = powerMax;

        this.lifeActual = lifeMax;
        this.manaActual = manaMax;
        this.powerActual = powerMax;

        // skills
        fireball = new Skill(this, "Fireball", 1, 1, 1);
        knockOut = new Skill(this, "Knock Out", 1, 0.5, 2);
        healing = new Skill(this, "Healing", 1, 2, 3);
        poisoning = new Skill(this, "Poisoning", 1, 0.2, 3);

        // state values
        alive = true;
        isKnockedOut = false;
        // how many rounds, how big is damage
        isPoisoned = new int[2];
    }

    @Override 
    public String toString() {
        String text = "";
        text += name + "\n";
        text += "Life:  " + lifeActual + " / " + lifeMax + "\n";
        text += "Mana:  " + manaActual + " / " + manaMax + "\n";
        text += "Power: " + powerMax + " / " + powerMax + "\n";
        return text; 
    }

    public String getName() {
        return name;
    }

    public int getLife() {
        return lifeActual;
    }

    public int getLifeMax() {
        return lifeMax;
    }

    public int getMana() {
        return manaActual;
    }

    public int getManaMax() {
        return manaMax;
    }

    public int getPower() {
        return powerActual;
    }

    public boolean isKnockedOut() {
        if(isKnockedOut) {
            UF.println(name + " is knocked out and can't cast skills this round\n", 1000);
            isKnockedOut = false;
            return true;
        } else {
            return false;
        }
    }

    public void isPoisoned() {
        if(isPoisoned[0] > 0) {
            UF.println(name + " is poisoned", 1000);
            sustainDamage(isPoisoned[1]);
            isPoisoned[0]--;
        }
    }

    public boolean isAlive() {
        return alive;
    }

    public void sustainDamage(int value) {
        if(value > 0) {
            lifeActual -= value;
            UF.println(name + " has gotten " + value + " damage\n", 1000);
        }

        if (lifeActual <= 0) {
            alive = false;
        }
    }

    public boolean useMana(int value) {
        if(value <= manaActual) {
            manaActual -= value;
            UF.println(name + " has used " + value + " mana.", 1000);
            return true;
        } else {
            UF.println(name + " has not enough mana.", 1000);
            return false;
        }
    }

    public void cureLife(int value) {
        int difference = lifeMax - lifeActual;
        if(value >= difference) {
            lifeActual = lifeMax;
            UF.println(name + " is fully healed.", 1000);
        } else {
            lifeActual += value;
            UF.println(name + " has healed " + value + " life points.", 1000);
        }
    }

    public void castFireball(Being enemy) {
        UF.println(name + " casts " + fireball.getName() + " on " + enemy.getName() + ".", 1000);
        if(useMana(fireball.getManaRequirement())) {
            enemy.sustainDamage(fireball.getValue());
        }
    }

    public void castHealing() {
        UF.println(name + " casts " + healing.getName() + ".", 1000);
        if(useMana(healing.getManaRequirement())) {
            cureLife(healing.getValue());
        }
    }

    public void castKnockOut(Being enemy) {
        UF.println(name + " casts " + knockOut.getName() + " on " + enemy.getName() + ".", 1000);
        if(useMana(knockOut.getManaRequirement())) {
            enemy.sustainDamage(knockOut.getValue());
            enemy.isKnockedOut = true;
            UF.println(enemy.getName() + " is knocked out.", 1000);
        }
    }

    public void castPoisoning(Being enemy) {
        UF.println(name + " casts " + poisoning.getName() + " on " + enemy.getName() + ".", 1000);
        if(useMana(poisoning.getManaRequirement())) {
            enemy.sustainDamage(powerActual / 2);
            enemy.isPoisoned[0] = poisoning.getLevel() + 2;
            enemy.isPoisoned[1] = poisoning.getValue();
        }
    }
}

Player.java

import java.util.Scanner;

public class Player extends Being {
    private static Scanner input = new Scanner(System.in);

    public Player(String name, int lifeMax, int manaMax, int powerMax) {
        super(name, lifeMax, manaMax, powerMax);
    }   

    public void chooseSkill(Being enemy) {
        if(!isKnockedOut()) {
            System.out.println("[1] Fireball");
            System.out.println("[2] Healing");
            System.out.println("[3] Knock Out");
            System.out.println("[4] Poisoning");
            System.out.println("[5] Give up");
            System.out.println("[*] Do nothing");
            System.out.print("Input: ");
            String command = input.next();

            if(command.equals("1")) {
                castFireball(enemy);
            } else if (command.equals("2")) {
                castHealing();
            } else if (command.equals("3")) {
                castKnockOut(enemy);
            } else if (command.equals("4")) {
                castPoisoning(enemy);
            } else if (command.equals("5")) {
                lifeActual = 0;
            }
        }
    }

    public void getStronger() {
        lifeMax += 5;
        manaMax += 2;
        powerMax += 1;

        lifeActual = lifeMax;
        manaActual = manaMax;
        powerActual = powerMax;

        isKnockedOut = false;
        isPoisoned[0] = 0;
        isPoisoned[1] = 0;
    }
}

NPC.java

import java.util.concurrent.ThreadLocalRandom;

public class NPC extends Being {
    public NPC(String name, int lifeMax, int manaMax, int powerMax) {
        super(name, lifeMax, manaMax, powerMax);
    }

    // this is actually my first KI, i am so proud
    public void chooseSkill(Being enemy) {
        if(isKnockedOut()) {
            return;
        }

        if (manaActual == 0) {
            lifeActual = 0;
            UF.println(getName() + " gives up because he has no mana.", 1000);
            return;
        }

        // decide for a skill
        int decision;
        while(true) {
            decision = ThreadLocalRandom.current().nextInt(0, 4);

            // i hope this is an accepted style, but i suspect it isnt
            if((decision == 1 && healing.getManaRequirement() > manaActual)
            || (decision == 1 && lifeMax == lifeActual)
            || (decision == 2 && knockOut.getManaRequirement() > manaActual)
            || (decision == 3 && poisoning.getManaRequirement() > manaActual)) {
                continue;
            } else {
                break;
            }
        }

        // cast the skill
        if(decision == 0) {
            castFireball(enemy);
        } else if (decision == 1) {
            castHealing();
        } else if (decision == 2) {
            castKnockOut(enemy);
        } else if (decision == 3) {
            castPoisoning(enemy);
        }
    }

    public void getStronger(double factor) {
        lifeMax *= factor;
        manaMax *= factor;
        manaMax += 3;
        powerMax += factor;

        lifeActual = lifeMax;
        manaActual = manaMax;
        powerActual = powerMax;

        isKnockedOut = false;
        isPoisoned[0] = 0;
        isPoisoned[1] = 0;
    }
}

Skill.java

public class Skill {
    private Being caster;
    private String name;
    private int level;
    private double value;
    private int manaRequirement;

    public Skill(Being caster, String name, int level, double value, int manaRequirement) {
        this.caster = caster;
        this.name = name;
        this.level = level;
        // means strength of the skill
        // multiplicated with level
        this.value = value;
        // also multiplicated with level
        this.manaRequirement = manaRequirement;
    }

    public String getName() {
        return name;
    }

    public int getValue() {
        return (int)(caster.getPower() * value * level);
    }

    public int getLevel() {
        return level;
    }

    public int getManaRequirement() {
        return manaRequirement * level;
    }
}

Редактировать

У меня есть еще два вопроса, на которые хотелось бы получить ответы.

Разных языках

Допустим, я хочу опубликовать свою программу на разных языках. Соответственно, все выходные данные отчетности должны быть скорректированы. Какие существуют методики для такого проекта?

Графическая поверхность

Предположим, я хочу построить графически все данные. Мне придется переписывать все классы. Есть ли эффективный способ, чтобы отделить обработку данных от вывода данных?



Комментарии
4 ответа

// name stands for "useful functions"
public class UF {

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

Более идиоматические имена могут быть HelperFunctions или UtilityFunctionsдаже лучше, если вы добавляете, что они для. Вы можете также опустить Functions, так как это очевидно, и этот класс представляет, что это Helper или Utility. Например IOHelper.

В качестве государственного Тимоти раболепства более подробно в комментариях, помощники не должны быть статическими, и это на самом деле больно ваши попытки хорошего объектно-ориентированного дизайна. Это то, что я хотел подразумевать под "класс представляет вещь ...", что означает, что вы лучше рассматривать их как объекты с определенным поведением. Поэтому занятия больше, чем просто набор функций. Что весь класс на самом деле ваш Outputне только некоторые UsefulFunctions.

Ваш Output следует аннотация, как и что именно написано, и иметь интерфейс с методами, как printAllAliveBeingsStats(Collection<Being> beings) (что приближается к тому, что первые 8 строк из основных петель делают), а не весь код вводится в Game класс.


NPC может быть идиоматический достаточно в контексте РПГ, но я все равно рекомендую назвав его NonPlayerCharacterи наследовать его от Character. Я настоятельно советую против, вытекающие из Beingили даже что-нибудь им Being, потому что это может в принципе быть что угодно, в зависимости от интерпретации. Лучше назвать это Creatureесли это то, что вы хотели его представлять.


Being имеет логическое поле aliveи способ isAlive(), которая возвращает значение переменной. Значение устанавливается после lifeActual устанавливается в 0. Лучшую реализацию isAlive() было бы просто return lifeActual > 0. Это снижает сложность кода и снижает риск внесения трудно найти ошибки.


alive и isKnockedOut несовместимые имена. Часто логические переменные называют просто прилагательным (alive и knockedOut), потому что они являются атрибутами объекта, а методы возвращают их называют isAlive и isKnockedOut (что ваш уже), потому что они на вопросы о состоянии объекта.


lifeActual и другие фактические переменные являются немного запутанным. Какой другой жизни может идти речь? Метафорические единицы? Лучше назвать это currentLife, потому что это текущее состояние объекта. Кроме того, currentLife читает более естественно, чем lifeCurrent. Последнее даже звучит как существительное.


У вас есть много геттеров и сеттеров. Это нарушает инкапсуляцию, а не объектно-ориентированный. Это просто немного лучше, чем обращаться к полям напрямую. Хотя в некоторых случаях они имеют смысл, это не хорошая практика, чтобы использовать их для каждого поля. Объекты должны предоставлять только информацию о себе, что им нужно.

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


        if(command.equals("1")) {
castFireball(enemy);
} else if (command.equals("2")) {
castHealing();
} else if (command.equals("3")) {
castKnockOut(enemy);
} else if (command.equals("4")) {
castPoisoning(enemy);
} else if (command.equals("5")) {
lifeActual = 0;
}

Это будет выглядеть гораздо яснее, как switch заявление. Обратите внимание, что это не работа со строками до Java 7, но вы должны быть за что.

        switch (command) {
case "1":
castFireball(enemy);
break;
case "2":
castHealing();
break:
// and so on
}


isPoisoned = new int[2];

Как вы можете иметь массив отравлен? Я не совсем понимаю, что эти 2 значения представляют, но если они разных вещей, связанных с отравлением, лучше использовать две переменные. Кроме того, isSomething предполагает логическое значение, поэтому лучше не использовать его для int значений.


        System.out.println("[1] Fireball");
System.out.println("[2] Healing");
System.out.println("[3] Knock Out");
System.out.println("[4] Poisoning");
System.out.println("[5] Give up");
System.out.println("[*] Do nothing");
System.out.print("Input: ");

Это может быть сделано более ясным путем объединения производства и даже один запросы звонок. Не забудьте добавить строк (\N).

        System.out.println("[1] Fireball\n"
+ "[2] Healing\n"
+ "[3] Knock Out\n"
// ...
+ "[*] Do nothing\n");

Компилятор будет собирать строки во время компиляции, поэтому нет необходимости беспокоиться о выполнении конкатенации строк.


getStronger() выглядит как геттер. becomeStronger() могло бы быть лучше.


while(player.getLife() > 0 && enemy.getLife() > 0) {

Здесь вы могли бы использовать isAlive() метод, и, таким образом, скрывают подробности, как "быть живым" реализуется.


У вас есть int score в Game класс. Разве это не должно быть частью Player? Игры не могут забить на убийство NPC, и не может НСПК результат, убив игрок. Поэтому результат является атрибутом игрока.


public static void println(String text, int time) {

ложь о том, что он делает. Это говорит о том, что оно почти такое же, как System.out.printlnпросто может на другой выход, чем командная строка. Лучше бы имя printAndwait(String text, int milliseconds).


Отравление кто-то не должен выглядеть так: enemy.isPoisoned();. Похоже, проверяя, есть ли противник в данный момент отравлен или нет. Лучше назовите его player.setPoisoned(bool poisoned)или просто player.poison().


public static String displayGraphical(int value1, int value2, String symbol) {

Каковы эти ценности? Лучше быть более откровенными с именами параметров. При вызове метода, вы не должны читать текст метода, чтобы знать, какие параметры передавать. Это должно быть очевидно из названия параметра.


В displayGraphical класса UF вы используете конкатенацию строк много, в петли, и метод вызывается несколько раз в главном цикле. Это очень неэффективно, потому что каждый раз, когда вы объединить строки создается новый объект String, и через некоторое время старое будет удалено. Лучше использовать то StringBuilder, для повышения производительности и эффективности памяти.


Кодирование на английском языке как на неродном для себя языке, перевод ошибка в коде: Gratulation. Английское слово Congratulations. Этот риск может быть уменьшен путем считывания всех строк из файла, один файл для каждого языка, если вы хотите поддерживать несколько языков, и позволять кому-то перевести файл для тех, кто не нужно быть программистом, потому что "нити" не являются частью программы больше.

16
ответ дан 30 января 2018 в 11:01 Источник Поделиться

Конструкция нуждается в капитальном ремонте. В частности, дизайн Skill класс (и понятие)

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

У вас в игре четыре вида навыков. если у вас есть конечный набор значений одного типа, лучший способ показать это, используя enum. Итак, если мы делаем Skill класса enum и удалите ссылку на Beingдизайн отражает концепцию мастерство наилучшим образом. Например, начальные значения для разных навыков не определено в Beingно в Skill "имя" собственность является избыточным и я нашел новый атрибут, который имеет отношение к этому типу:

public enum Skill {

FIREBALL(false, 1, 1).
KNOCK_OUT(false, 0.5, 2).
HEALING(true, 2, 3),
POISONING(false, 0.2, 3);

Skill(boolean castOnSelf, double value, int manaRequirement) {
// assign to instance vars
}

private boolean castOnSelf;
private double value;
private int manaRequirement;
}

Эта конструкция также значительно упрощает код для определения того, что для бросания (подсказка: ою можете получить на перечисление экземпляр от его строковое имя valueOf())

теперь насчет maxLevel? это один и тот же для всех типов навыков или разные?

Двигаемся дальше: в своей игре, каждый Being имеет тот же набор навыков. это нормально. но каждый Being развивается по-разному и это должно быть отражено индивидуальное мастерство экземпляры вместе с Being атрибуты, которые могут изменить базовый `мастерство'

public class MySkill {
private Skill skill;
private int level;
...

Так что теперь, вместо индивидуальных переменных Being есть Set из MySkill экземпляров. (это означает, что MySkill экземпляры должны считаться равными, если они имеют одинаковые skill) такая конструкция позволяет для итерации через набор навыков одного Being и конечно, разные BeingS может легко иметь разные наборы навыков.

Двигаемся дальше: как я понимаю, формула для эффекта мастерства кастинг включает базовое значение навыков, связанных с умением только власть, будучи, по только, да и уровень игроков в этом мастерства. Возможно, в более поздней версии вы хотите добавить оборонительные навыки? кроме того, в некоторых РПГ играх, случайный фактор добавляется, так что каждый кастинг навык может варьироваться в силу. Я бы сказал, что это сложная логика заслуги собственных CastingEngine что получает два BeingС (атакующего и цели) и навык, который был выбран. Если CastingEngine это интерфейс, ты можешь писать разные двигатели, которые развиваются в сложности. двигатель будет применяться все вышеуказанные факторы (в том числе, если навык накладывается на себя....) и изменить BeingС статистика по результатам.

6
ответ дан 30 января 2018 в 12:01 Источник Поделиться

Я ориентированы на один конкретный момент: магия чисел.

public Game() {
player = new Player("Player", 20, 5, 5);
enemy = new NPC("Enemy", 20, 5, 5);
}

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

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

Посмотрите, насколько легче это читать и понимать; это почти как естественный язык.

public Game() {
final int lifeMax = 20;
final int manaMax = 5;
final int powerMax = 5;

player = new Player("Player", lifeMax, manaMax, powerMax );
enemy = new NPC("Enemy", lifeMax, manaMax, powerMax );
}

5
ответ дан 30 января 2018 в 04:01 Источник Поделиться

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

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

public Game(Player player, NPC enemy) {
this.player = player;
this.enemy = enemy;
}

...
in main:
Player player = new Player("Player", 20, 5, 5);
NPC enemy = new NPC("Enemy", 20, 5, 5);
Game game = new Game(player, enemy);
game.mainLoop();

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

public class Game {
private Being player1;
private Being player2;

...

public Game(Being player1, Being player2) {
this.player1 = player1;
this.player2 = player2;
}
...
}

4
ответ дан 30 января 2018 в 02:01 Источник Поделиться