Skip to content

Instantly share code, notes, and snippets.

@Asenim
Created December 26, 2024 23:52
Show Gist options
  • Save Asenim/bd5ebca04b93989a9e40bd4bb888415b to your computer and use it in GitHub Desktop.
Save Asenim/bd5ebca04b93989a9e40bd4bb888415b to your computer and use it in GitHub Desktop.

https://github.com/HelloWorld140/Hangman
Ревью от Алексея @Raketa4000az

[Hello World]

Простая реализация в процедурном стиле. Выполнено в двух классах.

Недостатки реализации:

  1. Странная нумерация уровней: от 2 до 4. Почему нумерация не от 1?
Выберите уровень сложности введя цифру от 2 до 4: 
Легко (2)
Средне (3)
Сложно (4)
  1. На старте игры выводится отгадываемое слово
Выберите уровень сложности введя цифру от 2 до 4: 
Легко (2)
Средне (3)
Сложно (4)
4
[п, о, д, н, о, с, ч, и, ц, а]
  1. При выигрыше нужно показать слово целиком
д о й н иРанее введеные буквы: [ф, д, о, й, н, и]
Количество ошибок: 1 из 8
Введите 1 русскую букву
к
Вы угадали букву!
Поздравлю, вы победили!

Для начала игры введите (д).

Хорошо:

  1. Игра запускается
  2. 3 уровня сложности
  3. Есть список введенных букв
  4. Можно ввести только одиночную русскую букву
  5. Широкое применение констант
  6. Короткие методы
  7. Простой понятный алгоритм

Замечания: 0. Мусор в гит репозитории: .idea и .iml не надо комитить, используй гитигнор

  1. Нейминг -Из название не ясно, для чего используется эта переменная. "Буква как char"- что это значит? И почему оно хранит пробел? private static char letterAsChar = ' ';

-Название вводит в заблуждение. В енаме хранятся не ошибки, а картинки хангмана

enum Mistakes
--
Oracle Java code conventions, part."Naming conventions"
Мартин, "Чистый код", гл.2
-- 
  1. Нарушение конвенции кода, константы под полями класса
private static boolean playerChoice;
private static final int EASY = 2;
  1. Нарушение конвенции кода, в любой ситуации выделяй блоки
else if (!Character.isDigit(letter.charAt(0)))
  System.out.println("Вы ввели не цифру. Введите цифру 1, 2 или 3.");
else if (Integer.parseInt(letter) > HARD || Integer.parseInt(letter) < EASY)
  System.out.println("Вы ввели неверную цифру. Введите цифру 1, 2 или 3.");

//ПРАВИЛЬНО:
else if (!Character.isDigit(letter.charAt(0))) {
  System.out.println("Вы ввели не цифру. Введите цифру 1, 2 или 3.");
} else if (Integer.parseInt(letter) > HARD || Integer.parseInt(letter) < EASY) {
  System.out.println("Вы ввели неверную цифру. Введите цифру 1, 2 или 3.");
}
  1. Нарушение инкапсуляции, все методы публичные. В class Hangman публичным должен быть только void main(String[] args).

  2. Нарушение DRY, магические буквы, числа, слова. Вводи константы. А если они уже есть- используй

System.out.println("Выберите уровень сложности введя цифру от 2 до 4: \nЛегко (2)\nСредне (3)\nСложно (4)");
//...
System.out.println("Вы вввели больше 1 цифры. Введите цифру 2, 3 или 4.");

//ПРАВИЛЬНО:
System.out.printf("Выберите уровень сложности введя цифру: \nЛегко (%d)\nСредне (%d)\nСложно (%d) \n", EASY, MEDIUM, HARD);
//...
System.out.printf("Вы вввели больше 1 цифры. Введите цифру %d, %d или %d. \n", EASY, MEDIUM, HARD);
--
Фаулер, "Рефакторинг", гл.8 п."Замена магического числа символической константой"
refactoring.guru "Замена магического числа символьной константой"
--
  1. Создавай вспомогательные методы, делай программу более простой и понятной
while (counterOfMistakes != Math.ceil((double) MAX_COUNT_OF_MISTAKES / difficulty) && !HIDDEN_WORD.equals(WORD)) {...}

//ПРАВИЛЬНО:
while (isНазваниеКотороеВсеОбъясняет(/*args or empty*/)) {...}

private boolean isНазваниеКотороеВсеОбъясняет(/*args or empty*/) {
  return counterOfMistakes != Math.ceil((double) MAX_COUNT_OF_MISTAKES / difficulty) && !HIDDEN_WORD.equals(WORD);
}
  1. Если нужно печатать текст с более, чем одним подстановочным значением или значение вставляется внутрь сообщения, используй форматированный вывод- тогда сразу будет виден весь шаблон.
System.out.println("\nДля начала игры введите (" + YES + ").\nДля закрытия введите (" + NO + ")");

//ПРАВИЛЬНО:
System.out.printf("\nДля начала игры введите (%s). \nДля закрытия введите (%s) \n", YES, NO);
  1. Если в блоке if есть return(break, continue, throw, exit и т.д.), то else не пишется - это не имеет смысла
else if (Integer.parseInt(letter) == EASY) {
  difficulty = EASY;
  break;
} else if (Integer.parseInt(letter) == MEDIUM) {
  difficulty = MEDIUM;
  break;
} else if (Integer.parseInt(letter) == HARD) {
  difficulty = HARD;
  break;
}

//ПРАВИЛЬНО:
else if (Integer.parseInt(letter) == EASY) {
  difficulty = EASY;
  break;
} 
if (Integer.parseInt(letter) == MEDIUM) {
  difficulty = MEDIUM;
  break;
} 
if (Integer.parseInt(letter) == HARD) {
  difficulty = HARD;
  break;
}
  1. Побочные эффекты, метод делает несколько дел одновременно. Один метод должен делать только одно дело(одну ответственность, SRP). В данном случае метод должен только проверять букву. Но кроме этого метод выполняет побочное действие- изменяет поле класса
public static boolean checkInputLetterInWord(char letter) {
  //...  
  HIDDEN_WORD.set(i, letter);
  //...
  return HIDDEN_WORD.contains(letter);
}

Метод нужно разделить на два: один должен проверять букву на корректность, второй- устанавливать букву в слове. И тогда вместо

if (!checkInputLetterInWord(letterAsChar)) {
  System.out.println("Вы не угадали букву!");
  counterOfMistakes++;
} else System.out.println("Вы угадали букву!");

Будет
if(isЕстьТакаяБуква(letter)) {
  открытьБукву(letter);
  System.out.println("Вы угадали букву!");
} else {
  System.out.println("Вы не угадали букву!");
  counterOfMistakes++;
}

То же самое относится к boolean checkAlreadyInputLetter(char letter).

---
Мартин, "ЧК", гл.3,п."Избавьтесь от побочных эффектов"
---
  1. Вот это крайне загадочная тема if (counterOfMistakes == Math.ceil((double) MAX_COUNT_OF_MISTAKES / difficulty)) {...} Я так понимаю, что Math.ceil(...) высчитывает максимальное количество ошибок на текущем уровне сложности. Тогда не высчитывай этот параметр каждый раз, а установи его один раз там, где устанавливается уровень сложности:
public static void mainGameLoop() {
  while (startOrEndGame()) {
    choiceDifficultyInput();
    установитьМаксимальноеКоличествоОшибокНаТекущемУровнеСложности(); 
    //...
  }
}

//...
if (counterOfMistakes == maxLevelMistakes) {...}
  1. Распечатка картинки висельницы черес switch-case или if-elseif - индусский код. Картинки нужно хранить в статическом массиве и печатать по номеру, например, так
private static final String[][] PICTURES = {
  {
    "-----   ",
    "|       ",
    "|       ",
    "|       ",
    "|       ",
    "|       ",
    "------- ",
   },
   {
     "-----   ",
     "|   |   ",
     "|   O   ",
     "|       ",
     "|       ",
     "|       ",
     "------- ",
   },
   // more pics
  };

void printPicture(int numPicture) {  
  String[] picture = PICTURES[numPicture];

  for(String line : picture) {
    //напечатать line
  }
}

Здесь каждый уровень распечатывается через свой свич-кейс, а значит это трижды индусский код

public static void printHangman() {
  if (difficulty == EASY) {
    switch (counterOfMistakes) {
      case 0 -> System.out.println(Mistakes.ZERO_MISTAKE_EASY_DIFFICULT);
      //миллион строк
    }
  } else if (difficulty == MEDIUM) {
    switch (counterOfMistakes) {
      case 0 -> System.out.println(Mistakes.ZERO_MISTAKE_MEDIUM_DIFFICULT);
      //миллион строк
    }
  } else if (difficulty == HARD) {
    switch (counterOfMistakes) {
      case 0 -> System.out.println(Mistakes.ZERO_MISTAKE_HARD_DIFFICULT);
      //миллион строк
    }
  }
}
  1. Это игра, а в любой игре должны быть методы, которые проверяют состояние игры
if(isВыиграл()) {
  //действия при выигрыше
} else if(isПроиграл()) {
  //действия при проигрыше
}
//...

private static boolean isВыиграл() {...}
private static boolean isПроиграл() {...}

Сейчас таких методов нет, а проверка состояния игры происходит косвенно

public static void mainGameLoop() {
  while (counterOfMistakes != Math.ceil((double) MAX_COUNT_OF_MISTAKES / difficulty) && !HIDDEN_WORD.equals(WORD)) {...}
  //...
}

//ПРАВИЛЬНО:
public static void mainGameLoop() {
  while (inGame()) {...}
  //...
}

private static boolean inGame() {
 return !isВыиграл() && !isПроиграл();
}
  1. enum Mistakes, картинки хангмана -Хранит сразу три набора картинок для разных уровней сложности. Нужно сделать три енама, каждый из которых будут хранить свой набор картинок.

-В картинках должны храниться только картинки, а не картинки+текст

EIGHT_MISTAKE_EASY_DIFFICULT("""
  ____
  |    |
  |    o
  |   /0\\
  |   / \\
  |
  YOU DEAD!\
  """);

Печатать картинки и сопроводительный текст- разные задачи!
Например так
printHangman();
if(isПроиграл()) {
  //выполнить действия при проигрыше в том числе напечатать "YOU DEAD!"
}

=== Архитектура

А1. Итак, в игре есть уровни сложности, но реализованы они не очень удачно- мегаенам с одной стороны и тройной свичкейс- с другой.

a) Оставаясь в рамках процедурного стиля можно сделать чуть лучше. Допустим, мы решили, что у нас есть три уровня сложности

public enum Level {
  EASY, MEDIUM, HARD
}

Чем они будут отличаться? Сейчас уровни сложности это цифры, которые принимают участие в хитрой формуле, где высчитывается максимальное количество ошибок на каждом уровне Math.ceil((double) MAX_COUNT_OF_MISTAKES / difficulty) Таким образом в результате рассчетов этот параметр равен: 6 ошибок EASY, 4 ошибки MEDIUM, 3 ошибки HARD.

б) Избавимся от промежуточных рассчетов, засуним эти параметры сразу в енам

public enum Level {
  EASY(6),
  MEDIUM(4),
  HARD(3);
  private final int maxMistakes;

  Level(int maxMistakes) {
    this.maxMistakes = maxMistakes;
  }

  public int getMaxMistakes() {
    return maxMistakes;
  }
}

в) Получение номера уровня сложности от юзера

System.out.printf("Выберите уровень сложности введя цифру: \n%d - Легко \n%d - Средне \n%d - Сложно ",
  Level.EASY.ordinal(),
  Level.MEDIUM.ordinal(),
  Level.HARD.ordinal()
);
int numLevel = //ввод номера уровня от юзера
if(numLevel < Level.EASY.ordinal() || numLevel > Level.HARD.ordinal()) {
  //ввели неправильный номер уровня
}

//РЕЗУЛЬТАТ:
Выберите уровень сложности введя цифру: 
0 - Легко 
1 - Средне 
2 - Сложно 

г) Перевод номера в уровень сложности

Level currentLevel = Level.values()[numLevel];

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

д) Проверка на проигрыш

if(counterOfMistakes == currentLevel.getMaxMistakes()) {
  //таки проиграл
}

е) Распечатка картинок хангмана

класс ХангманРендерер{

  private static final String[] MEDIUM_PICTURES = {
      """
      ____
      |    |
      |    o
      |   /0\\
      |   /
      |\
      """),
      //oth's pic's
  };

  public static void print(Level level, int pictureNumber) {
    String[] pictures = switch(level) {
      EASY -> EASY_PICTURES[];
      MEDIUM -> MEDIUM_PICTURES[];
      //...
    }
     String picture = pictures[pictureNumber];
    //распечатать picture
  }
}

ё) И тогда распечатка хангмана выглядит совсем просто

public class Hangman {
  private static void какойтоМетод() {
    //...
    ХангманРендерер.print(currentLevel, counterOfMistakes);
    //...
  }
}

В ООП стиле это делается еще красивее, но и в процедурном выглядит неплохо.

А2. Ну и бонусом лайфхак про вывод подсказки

Выберите уровень сложности введя цифру от 2 до 4: 
Легко (2)
Средне (3)
Сложно (4)
4
[п, о, д, н, о, с, ч, и, ц, а]

Это больше ООП стиль, но да ладно. Допустим, мы пилим виселицу и делаем для ее запуска классический майн

public class Main {
  public static void main(String[] args) {
    Словарь словарь = new Словарь(ПУТЬ_К_ФАЙЛУ);
    String word = словарь.get(); 
    Game game = new Game(word);
    game.start();
  }
}

Все хорошо, но для тестирования мы должны знать отгадываемое слово- чтобы быстро выиграть и проиграть и посмотреть как при этом поведет себя программа. Тогда делаем второй тестовый майн

public class ТестMain {
  public static void main(String[] args) {
    Словарь словарь = new Словарь(ПУТЬ_К_ФАЙЛУ);
    String word = словарь.get(); 
    System.out.println("Подсказка: " + word);
    Game game = new Game(word);
    game.start();
  }
}

Этот майн можно поместить в гитигнор и тогда он не попадет в репозиторий, а значит качающий игру юзер не будет видеть подсказок. А разработчик c помощью этого тестового майна будет тестить игру. ---

Вывод: изучить конвенцию кода. В целом для процедурного стиля весьма неплохо. Понравилось, что методы в основном небольшие.

#ревью #виселица #уровнисложности #gamelevel #архитектура #читаетфайл

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment