본문 바로가기
카테고리 없음

스피드 연산 게임 만들기4

by 경자꿈사 2024. 7. 26.

지난 시간에 만들었던 '스피드 연산 게임' 은 구구단 문제만 풀 수 있게 되어 있었는데 오늘은 덧셈과 뺄셈 문제도 풀 수 있게 프로그램을 개선해 보자. 구구단 말고 다른 문제를 선택해서 풀 수 있게 하려면 우선 문제 유형에 대한 정보를 Game 객체가 알고 있어야 한다. 이것은 게임 시간(time) 처럼 객체 생성 시 주거나 아니면 객체를 생성한 이후 setter 로 설정해도 된다. 그리고 실제 게임 실행 시에는 그 문제 유형 정보를 이용하여 문제를 생성하면 된다.

이러한 생각을 반영하여 아래처럼 코드를 수정해 보자.

initialize 메서드에 quiz 파라미터를 하나 추가하였고 기본값으로는 구구단 문제를 내도록 :gugudan 심볼을 설정했다.

그래서 아무 인자 없이 Game 객체를 생성하여 게임을 실행하면 기존과 동일하게 10초 동안 구구단 문제를 풀게 된다.

그리고 또 달라진 부분은 make_quiz 메서드인데 이전 코드에서는 make_quiz 메서드 안에서 직접 구구단 문제를 만들었었다. 그런데 이제는 문제 유형에 맞는 문제를 생성하는 메서드가 각각 따로 있고 make_quiz 메서드는 단지 현재 알고 있는 문제 유형 정보와 대응되는 메서드를 호출하여 그 결과를 그대로 반환하기만 한다.

require 'timeout'

class Game
  attr_accessor :quiz, :time

  def initialize(quiz = :gugudan, time = 10)
    @quiz = quiz
    @time = time
    @quiz_cnt = 0
    @correct_cnt = 0
  end

  def make_quiz
    if quiz == :gugudan
      gugudan
    elsif quiz == :add
      add
    elsif quiz == :sub
      sub
    end
  end

  def gugudan
    n1 = rand(2..9)
    n2 = rand(1..9)
    ["#{n1} x #{n2} = ", n1 * n2]  
  end

  def add
    n1 = rand(1..9)
    n2 = rand(1..9)
    ["#{n1} + #{n2} = ", n1 + n2]    
  end
  
  def sub
    n1 = rand(1..9)
    n2 = rand(1..9)
    ["#{n1} - #{n2} = ", n1 - n2]    
  end

  def exam
    question, answer = make_quiz
    print question
    @quiz_cnt += 1
    input = STDIN.gets
    if answer == input.to_i
      @correct_cnt += 1
      puts "정답"
    else
      puts "오답"
    end    
  end
  
  def play
    @quiz_cnt = 0
    @correct_cnt = 0
    
    begin
      Timeout.timeout(time) do
        while true
          exam
        end
      end
    rescue
    end
    
    puts "\n총 #{@quiz_cnt}문제 중 #{@correct_cnt}문제를 맞췄습니다."    
  end
  
  private :make_quiz, :exam, :gugudan, :add, :sub
end

실행해 보면 아래처럼 구구단 뿐만 아니라 덧셈과 뺄셈도 할 수 있게 되었다.

>> require './game'
=> true
>>
>> game = Game.new
=> #<Game:0x000001e44ff5dfe0 @quiz=:gugudan, @time=10, @quiz_cnt=0, @correct_cnt=0>
>> game.play
3 x 7 = 21
정답
3 x 5 = 15
정답
4 x 1 =
총 3문제 중 2문제를 맞췄습니다.
=> nil
>> game.quiz = :add
=> :add
>> game.play
5 + 6 = 11
정답
3 + 5 = 8
정답
3 + 6 =
총 3문제 중 2문제를 맞췄습니다.
=> nil
>> game.quiz = :sub
=> :sub
>> game.play
1 - 9 = -8
정답
6 - 6 =
총 2문제 중 1문제를 맞췄습니다.
=> nil

그런데 이렇게 코드를 작성하면 새로운 문제 유형을 만들고 싶으면 Game 클래스 코드 자체를 수정해야 한다.

Game 클래스 코드는 게임 자체에 대한 틀만 유지하게 하고 문제 유형에 따른 문제 생성은 별도의 코드로 분리시켜 보자.

당연히 하나의 클래스(또는 소스 파일)에 모든 기능을 넣는 것이 항상 좋은 것은 아니다. 이것은 전체 프로그램의 크기나 복잡도 그리고 변경 가능성 등 여러 가지 상황을 고려해봐야 한다. 여기서는 학습의 목적을 위해 코드를 분리해 보자.

아래 코드를 보면 구구단, 덧셈, 뺄셈 각각에 대한 클래스가 보인다.

class Gugudan
  def make
    n1 = rand(2..9)
    n2 = rand(1..9)
    ["#{n1} x #{n2} = ", n1 * n2]  
  end
end

class Add
  def make
    n1 = rand(1..9)
    n2 = rand(1..9)
    ["#{n1} + #{n2} = ", n1 + n2]    
  end
end
  
class Sub
  def make
    n1 = rand(1..9)
    n2 = rand(1..9)
    ["#{n1} - #{n2} = ", n1 - n2]    
  end
end

아래 코드 실행 결과를 보면 quiz 변수가 참조하는 객체가 무엇인지에 따라 make 메서드의 동작이 달라지는 걸 볼 수 있다.

>> require './quiz'
=> true
>> quiz = Gugudan.new
=> #<Gugudan:0x000001e04c626208>
>> quiz.make
=> ["4 x 6 = ", 24]
>> quiz = Add.new
=> #<Add:0x000001e04cd12a80>
>> quiz.make
=> ["7 + 9 = ", 16]
>> quiz = Sub.new
=> #<Sub:0x000001e04cdecca8>
>> quiz.make
=> ["2 - 2 = ", 0]

그러면 이제 문제를 생성하는 객체를 사용하도록 Game 클래스를 아래처럼 수정해 보자.

Gugudan, Add, Sub 클래스의 코드는 game.rb 와 같은 위치의 quiz.rb 파일에 저장했고 game.rb 파일에서 'require' 를 통해 로드했다.

Game 클래스를 보면 바로 전에 만들었던 gugudan, add, sub 세 개의 메서드가 모두 사라졌고 make_quiz 메서드도 단지 @quiz 인스턴스 변수가 참조하는 객체의 make 메서드를 호출하는 일만 하고 있어 전체적으로 코드가 깔끔해졌다.

이제 Game 클래스 안에서 문제를 직접 생성하는 코드는 사라졌다.

require 'timeout'
require './quiz'

class Game
  attr_accessor :quiz, :time

  def initialize(quiz = Gugudan.new, time = 10)
    @quiz = quiz
    @time = time
    @quiz_cnt = 0
    @correct_cnt = 0
  end

  def make_quiz
    quiz.make
  end

  def exam
    question, answer = make_quiz
    print question
    @quiz_cnt += 1
    input = STDIN.gets
    if answer == input.to_i
      @correct_cnt += 1
      puts "정답"
    else
      puts "오답"
    end    
  end
  
  def play
    @quiz_cnt = 0
    @correct_cnt = 0
    
    begin
      Timeout.timeout(time) do
        while true
          exam
        end
      end
    rescue
    end
    
    puts "\n총 #{@quiz_cnt}문제 중 #{@correct_cnt}문제를 맞췄습니다."    
  end
  
  private :make_quiz, :exam
end

아래 코드를 보면 문제 유형을 설정할 때 심볼을 사용하던 것이 문제 생성을 담당하는 클래스의 객체를 사용하는 것으로만 바뀌었을 뿐 달라진 건 없다.

>> require './game'
=> true
>> game = Game.new
=> #<Game:0x000001efa408cab0 @quiz=#<Gugudan:0x000001efa408ca88>, @time=10, @quiz_cnt=0, @corre...
>> game.play
5 x 4 = 20
정답
9 x 4 =
총 2문제 중 1문제를 맞췄습니다.
=> nil
>> game.quiz = Add.new
=> #<Add:0x000001efa45233d0>
>> game.play
6 + 9 = 15
정답
3 + 1 =
총 2문제 중 1문제를 맞췄습니다.
=> nil
>> game.quiz = Sub.new
=> #<Sub:0x000001efa444a8a0>
>> game.play
5 - 9 = -4
정답
3 - 1 = 2
정답
7 - 1 =
총 3문제 중 2문제를 맞췄습니다.
=> nil

다음 글에서는 객관식 문제를 풀어볼 수 있게 만들어 보자.

See you again~~