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

스피드 연산 게임 만들기3

by 경자꿈사 2024. 7. 25.

오늘은 지난번 글에서 작성했던 '스피드 연산 게임' 코드를 클래스를 사용해서 수정해 보도록 하자.

아래 기존 코드와 클래스를 사용해 수정한 코드가 있다.

require 'timeout'

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

def exam
  question, answer = make_quiz
  print question
  input = STDIN.gets
  return answer == input.to_i
end

quiz_cnt = 0
correct_cnt = 0

begin
  Timeout.timeout(10) do
    while true
      quiz_cnt += 1
      if exam
        correct_cnt += 1
        puts "정답"
      else
        puts "오답"
      end
    end
  end
rescue
end

puts "\n총 #{quiz_cnt}문제 중 #{correct_cnt}문제를 맞췄습니다."

 

require 'timeout'

class Game
  attr_accessor :time

  def initialize(time = 10)
    @time = time
  end

  def make_quiz
    n1 = rand(2..9)
    n2 = rand(1..9)
    ["#{n1} x #{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
end

Game 이라는 클래스에 기존 코드에 있던 두 개의 메서드를 옮겨왔다. 그 중 make_quiz 메서드는 코드 수정 없이 그대로 옮겨왔고 exam 메서드는 메서드 안에서 두 개의 인스턴스 변수(@quiz_cnt 와 @correct_cnt)를 사용하여 총 문제 수와 정답을 맞춘 수를 카운트하도록 하였다. 그리고 정답과 오답에 대한 화면 출력도 exam 메서드 안으로 넣어서 실제 게임을 진행시키는 코드(play 메서드)가 기존 코드에 비해서 많이 깔끔해진 걸 볼 수 있다.

초기화 메서드(initialize)의 코드를 보면 Game 객체를 생성할 때 인수로 게임 시간을 줄 수 있게 하였고 인수 없이 생성할 경우 게임 시간이 10초가 되도록 기본값(default value)을 time 파라미터에 설정하였다.

아래 그림처럼 Game 클래스 코드를 irb 에서 로드한 후 테스트해 보자.

>> require './game'
=> true
>> game = Game.new
=> #<Game:0x0000020090d298c0 @time=10>
>> game.play
2 x 5 = 10
정답
2 x 4 = 8
정답
9 x 8 = 72
정답
9 x 1 = 9
정답
3 x 4 =
총 5문제 중 4문제를 맞췄습니다.
=> nil

Game 객체를 생성한 후 play 메서드를 호출하여 게임을 두 번 진행해 보았다.

게임을 새로 진행할 때는 총 문제 수와 정답을 맞춘 수를 처음부터 다시 카운트해야 하므로 play 메서드에서 두 개의 인스턴스 변수를 0으로 초기화 시켰다.

그런데 아래처럼 Game 객체를 생성한 후 play 메서드가 아니라 exam 메서드를 호출하게 되면 예외가 발생하게 된다. 빨간색 테두리로 표시된 부분을 보면 NoMethodError 예외이고 nil (nil도 NilClass 클래스의 객체임을 알 수 있다.) 에 정의되지 않은 메서드(+)를 호출했다는 내용이다. 우리가 사용한 + 연산자가 메서드라니 신기할 것이다. 이 부분에 대해서는 나중에 알아보기로 하고 우선 어디서 왜 이런 예외가 발생했는지 찾아보자.

>> require './game'
=> true
>>
>> game = Game.new
=> #<Game:0x00000205af144ae0 @time=10>
>> game.exam
6 x 4 = 
Traceback (most recent call last):
...생략
1: from D:/blog/ruby/speed_quiz/game.rb:19:in `exam'
NoMethodError (undefined method `+' for nil:NilClass)

game.exam 코드를 실행한 바로 다음 줄을 보면 "7 x 7 = " 이 출력된 걸 볼 수 있는데 이것은 exam 메서드 안에서 make_quiz 메서드를 호출한 후 print 메서드로 구구단 문제를 출력하여 표시된 것이다. 여기까지는 실행이 잘 되었고 예외가 발생한 곳은 바로 그 다음 줄의 @quiz_cnt += 1 부분이다. 이전 글에서 += 에 대해 설명을 했었는데 + 가 먼저 실행이 되고 그 결과값이 좌측 변수에 할당(=)된다고 했다. 즉 + 가 실행되면서 예외가 발생한 것이다. 루비에서는 + 도 객체에 대해 호출하는 메서드로 인식하기 때문에 @quiz_cnt 변수가 참조하는 객체에 대해 1을 인수값으로 하여 + 메서드를 호출하는 것으로 처리가 된다. 즉 @quiz_cnt.+(1) 과 같다. 결국 이 예외는 @quiz_cnt 인스턴스 변수에 대한 초기화 과정 없이 바로 사용을 하게 되어 인스턴스 변수의 값 대신 nil 을 돌려주어 발생한 것이다. nil 도 엄연히 객체이기는 하지만 NilClass 클래스에는 + 메서드가 정의되어 cl있지 않다.

아래 코드 실행 결과를 보면 실제 NilClass 클래스에는 + 인스턴스 메서드가 없지만 정수(객체)를 표현하는 Integer 클래스에는 + 인스턴스 메서드가 존재하는 걸 알 수 있다.

>> nil.class
=> NilClass
>> NilClass.instance_methods.include?(:+)
=> false
>> 1.class
=> Integer
>> Integer.instance_methods.include?(:+)
=> true
>>

이러한 예외가 발생하지 않게 하기 위해 두 가지 방법을 생각해 볼 수 있는데, 그 중 첫 번째 방법은 아래 코드처럼 Game 객체 생성 시 인스턴스 변수를 초기화해 주는 것이다.

require 'timeout'

class Game
  attr_accessor :time

  def initialize(time = 10)
    @time = time
    @quiz_cnt = 0
    @correct_cnt = 0
  end
  
  ...생략
end

 

또 다른 방법은 Game 객체를 사용하는 코드에서 굳이 필요하지 않은 메서드에 대해서는 호출하지 못하도록 막아버리는 거다. 아래 코드를 보면 Game 클래스 정의 가장 아래 부분에 'private :make_quiz, :exam' 코드가 보일 것이다.

이것은 Game 클래스의 인스턴스 메서드인 make_quiz 와 exam 에 대해 해당 객체 외부에서는 호출할 수 없도록 호출 범위를 한정하는 것이다. 즉 두 메서드를 프라이빗하게 만든다.

require 'timeout'

class Game
  attr_accessor :time

  def initialize(time = 10)
    @time = time
    @quiz_cnt = 0
    @correct_cnt = 0
  end

  ...생략
  
  private :make_quiz, :exam
end

실제 아래 테스트 결과를 보면 Game 객체에 대한 make_quiz 메서드와 exam 메서드 호출이 모두 예외를 발생시킨 것을 볼 수 있다. 예외 메시지를 보면 'private method' 를 호출했다고 나온다.

>> require './game'
=> true
>> game = Game.new(20)
=> #<Game:0x000001574a93a7e0 @time=20, @quiz_cnt=0, @correct_cnt=0>
>> game.time
=> 20
>> game.make_quiz
NoMethodError (private method `make_quiz` called for #<Game:0x000001574a93a7e0 @time=20, @quiz_cnt=0, @correct_cnt=0>)

>> game.exam
NoMethodError (private method `exam` called for #<Game:0x000001574a93a7e0 @time=20, @quiz_cnt=0, @correct_cnt=0>)

다음 글에서는 구구단뿐만 아니라 덧셈이나 뺄셈 문제도 풀어볼 수 있게 코드를 개선해 보자.

See you again~~