본문 바로가기
공대생/무작정 해보는 파이썬

Python Color Match Game

by 흔한 공대생 2022. 1. 31.
728x90
반응형

전공자가 아니기 때문에 프로그래밍이 어려워지면 포기하게된다.
그래서 단순하면서도 흥미를 끌어주는 프로그램을 만들고 싶었다.
어려우면 싫지만 코딩은 하고 싶으니까.
초보자여도 해 볼 만한, 만들어 볼 만한 그런 프로그램. 시작한다.

 


 

오랜만에 파이썬 컨텐츠로 돌아왔다. 이번에 소개할 것은 간단한 게임이다.

색의 3원색은 빨강, 초록, 파랑이다. 세상의 모든 색은 이 세 가지 색을 섞어서 표현할 수 있다.
따라서 컴퓨터는 모든 색상을 R, G, B 이 3개 값의 조합으로 표현한다. 각 색상의 포함 정도를 0부터 255 사이의 값으로 표현하고, 이를 조합하여 최종 색을 표현하는 것이다.

이에 착안하여, 어떤 랜덤한 색이 주어졌을 때 RGB를 조합하여 주어진 색을 만들어내는 게임을 고안해보았다.

 

 

소스코드

깊이 고민하지 않고 떠오르는 대로 막 만들었더니 코드가 상당히 길다.. 다음은 코드 전문이다.

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QPainter, QPen, QColor, QBrush
from PyQt5.QtCore import Qt
from PyQt5 import QtGui
import random


class MyApp(QWidget):
    r = g = b = 0
    ur = ug = ub = 0
    cnt = 0
    record = []             # 버튼 클릭 데이터 기록
    CORE = 20               # 난이도 설정 (일치 확인 범위)

    def __init__(self):
        super().__init__()
        self.initUI()

# UI 구성
    def initUI(self):
        self.label = QLabel("RGB = ( ?,?,? )          RGB = ( 0,0,0 )   ")
        self.label.setAlignment(Qt.AlignCenter)
        self.label.setFont(QtGui.QFont('Hack', 12))
        labell = QVBoxLayout()
        labell.addWidget(self.label)
        
        rbtn = QPushButton(self)
        rbtn.setText('Red')
        rbtn.clicked.connect(self.rbtn_clicked)

        gbtn = QPushButton(self)
        gbtn.setText('Green')
        gbtn.clicked.connect(self.gbtn_clicked)

        bbtn = QPushButton(self)
        bbtn.setText('Blue')
        bbtn.clicked.connect(self.bbtn_clicked)

        wbtn = QPushButton(self)
        wbtn.setText('Light')
        wbtn.clicked.connect(self.wbtn_clicked)

        dbtn = QPushButton(self)
        dbtn.setText('Dark')
        dbtn.clicked.connect(self.dbtn_clicked)

        prbtn = QPushButton(self)
        prbtn.setText('Previous')
        prbtn.clicked.connect(self.prbtn_clicked)

        rebtn = QPushButton(self)
        rebtn.setText('Reset')
        rebtn.clicked.connect(self.rebtn_clicked)

        vpbox1 = QVBoxLayout()
        vpbox1.addWidget(wbtn)
        vpbox1.addWidget(dbtn)

        vpbox2 = QVBoxLayout()
        vpbox2.addWidget(prbtn)
        vpbox2.addWidget(rebtn)

        hbox = QHBoxLayout()
        hbox.addStretch(1)
        hbox.addLayout(vpbox1)
        hbox.addWidget(rbtn)
        hbox.addWidget(gbtn)
        hbox.addWidget(bbtn)
        hbox.addLayout(vpbox2)
        hbox.addStretch(1)

        vbox = QVBoxLayout()
        vbox.addStretch(19)
        vbox.addLayout(labell)
        vbox.addStretch(4)
        vbox.addLayout(hbox)
        vbox.addStretch(1)

        self.setLayout(vbox)

        self.set_color()
        
        self.setGeometry(300, 300, 400, 300)
        self.setWindowTitle('Match the Color')
        self.show()

# 버튼 클릭 이벤트 처리
    def rbtn_clicked(self):
        self.ur += 1
        self.record.append('r')
        self.paintU()

    def gbtn_clicked(self):
        self.ug += 1
        self.record.append('g')
        self.paintU()

    def bbtn_clicked(self):
        self.ub += 1
        self.record.append('b')
        self.paintU()

    def wbtn_clicked(self):
        self.ur += 1
        self.ug += 1
        self.ub += 1
        #if self.cnt > 0:
        #    self.cnt -= 1
        self.paintU()

    def dbtn_clicked(self):
        self.cnt += 1
        self.paintU()

    def rebtn_clicked(self):
        self.ur = self.ug = self.ub = self.cnt = 0
        record = []
        self.paintU()

    def prbtn_clicked(self):
        if len(self.record) != 0:
            c = self.record.pop()
            if c == 'r':
                self.ur -= 1
            elif c == 'g':
                self.ug -= 1
            elif c == 'b':
                self.ub -= 1
            else:
                self.ur -= 1
                self.ug -= 1
                self.ub -= 1
        self.paintU()

# 화면 출력
    def paintEvent(self, e):
        qp = QPainter()
        qp.begin(self)
        self.draw_rect(qp)
        qp.end()

    def paintU(self):
        qp = QPainter()
        qp.begin(self)
        self.draw_rect(qp)
        qp.end()
        self.update()
        self.check()

    def draw_rect(self, qp):
        r1, g1, b1 = self.get_color()
        r2, g2, b2 = self.get_ucolor()

        # 랜덤 생성 색 & 사용자 생성 색
        qp.setBrush(QColor(r1, g1, b1))
        qp.setPen(QPen(Qt.black, 2))
        qp.drawRect(40, 20, 140, 140)

        qp.setBrush(QColor(r2, g2, b2))
        qp.setPen(QPen(Qt.black, 2))
        qp.drawRect(220, 20, 140, 140)
        self.label.setText("RGB = ( ?,?,? )          RGB = ( {},{},{} )   ".format(r2,g2,b2))
        
        # 버튼 위 가이드 색
        qp.setBrush(QColor(255, 0, 0))
        qp.setPen(QPen(Qt.black, 1))
        qp.drawRect(100, 210, 40, 40)

        qp.setBrush(QColor(0, 255, 0))
        qp.setPen(QPen(Qt.black, 1))
        qp.drawRect(180, 210, 40, 40)

        qp.setBrush(QColor(0, 0, 255))
        qp.setPen(QPen(Qt.black, 1))
        qp.drawRect(260, 210, 40, 40)

# 맞출 색코드 랜덤 생성
    def set_color(self):
        self.record = [] # 이전 게임 기록 지우기
        self.r = int(random.random()*10000)%256
        self.g = int(random.random()*10000)%256
        self.b = int(random.random()*10000)%256
        print(self.r,self.g,self.b) #보조가이드

# 저장된 색 코드 값 출력
    def get_color(self):
        return (self.r, self.g, self.b)

    def get_ucolor(self):
        ur, ug, ub = (self.ur, self.ug, self.ub)
        cntt = self.cnt+max(ur,ug,ub)
        # 버튼 클릭 비율에 따라 색 코드 값 결정
        if cntt != 0:
            ur = int(ur/cntt*255)
            ug = int(ug/cntt*255)
            ub = int(ub/cntt*255)            
        return (ur, ug, ub)

# 색 일치 여부 판단
    def check(self):
        r1, g1, b1 = self.get_color()
        r2, g2, b2 = self.get_ucolor()
        CORE = self.CORE
        # 오차범위 이내 일 때 게임종료
        if r2-CORE <= r1 and r1 <= r2+CORE:
            if g2-CORE <= g1 and g1 <= g2+CORE:
                if b2-CORE <= b1 and b1 <= b2+CORE:
                    self.ending()

# 게임종료 팝업
    def ending(self):
        r1, g1, b1 = self.get_color()
        r2, g2, b2 = self.get_ucolor()
        # 오차율 계산
        re_e = abs(round((r2-r1)/255*100,1))
        gr_e = abs(round((g2-g1)/255*100,1))
        bl_e = abs(round((b2-b1)/255*100,1))

        # 팝업창 구성
        buttonReply = QMessageBox.information(
            self, 'You Win!',
            "COM = ({},{},{})\nYOU = ({},{},{}) (±{}%)\n\nDo you want to play again?".format(r1,g1,b1,r2,g2,b2,max(re_e,gr_e,bl_e)), 
            QMessageBox.Yes | QMessageBox.No
            )

        # 팝업창 버튼 이벤트 처리
        if buttonReply == QMessageBox.Yes:
            self.set_color()
            self.ur = self.ug = self.ub = self.cnt = 0
        else:
            sys.exit(app.exec_())


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = MyApp()
    ex.show()
    sys.exit(app.exec_())

 

이번 프로그램에는 PyQt5를 사용했다. PyQt5는 GUI 프로그램 개발에 널리 사용되는 툴이다.

 

프로그램 설명(을 곁들인 개발과정)

프로그램 화면 구성은 간단하다.

  맨 위, 메인에는 맞춰야하는 색과 만드는 중인 색을 표시해준다. 만드는 중인 색에는 현재 RGB 값을 표시해주어 약간의 가이드가 되도록 했다.

  바로 아래에는 RGB값을 더할 수 있는 버튼을 넣었고, 보다 직관적으로 하기 위해 해당 색을 나타내주었다. 버튼을 누르면 해당 색이 첨가된다.
  여기서 첫 번째 문제가 발생했는데, 색 첨가를 어느 정도 스케일로 해야하냐는 것이었다. 우리가 조절할 수 있는 수치는 0부터 255까지. 숫자에 충실하게 색을 조절하기엔 지루하고 재미없을 것 같았다.
  그래서 비율을 계산해서 조합하기로 했다. 세 가지 색 중 가장 많이 첨가한 색을 기준으로 각각의 색이 몇 퍼센트의 비율로 첨가되었는지를 계산했다.
  예를 들면 빨강을 5번, 초록을 1번, 파랑을 3번 넣었을 때, 빨강은 (255*5/5=)255, 초록은 (255*1/5=)51, 파랑은 (255*3/5=)153의 수치를 가지게 된다.

  이렇게 해보니 하나의 색상은 언제나 255의 값을 가진다는 문제가 생겼다. 예를 들어 (R,G,B)=(127,127,127)인 회색은 만들지 못하는 것이다.
  두 번째 문제를 해결하기 위해 Dark 버튼을 도입했다. 가장 많이 첨가한 색을 기준으로 하는 것이 아니라, cnt라는 어떤 값을 기준으로 각각의 색의 비율을 계산하는 것이다. cnt는 Dark 버튼을 누른 횟수와 RGB 중 가장 많이 누른 횟수의 합이다.
  예를 들면 빨강을 1번, 초록을 1번, 파랑을 1번, Dark를 1번 넣었을 때, 빨강은 (255*1/(1+1)=)127, 초록은 (255*1/(1+1)=)127, 파랑은 (255*1/(1+1)=)127의 수치를 가지게 된다.

  Dark 버튼은 RGB 값을 낮추는 역할을 한다. 그렇다면 반대로 RGB 값을 전부 높여주는 버튼도 있어야하지 않나? 사실 RGB 버튼을 각각 한 번씩 더 눌러주면 되지만 그러기엔 너무 귀찮다. 3번의 클릭이라니.. 그래서 Light 버튼을 만들었다. 이 버튼은 단순히 RGB를 한 번씩 눌러주는 것과 같다.

  혹시 실수를 하면 어떻게 해야할까? 그래서 Reset 버튼을 만들어 내가 입력한(첨가한) RGB 그리고 Dark 값을 초기화하도록 했다. 작은 실수를 돌이키기 위해서는 Previous 버튼을 만들어 직전에 수행한 작업을 되돌리도록 했다. 이렇게 하기 위해서 record 라는 리스트를 만들어 어떤 버튼을 눌렀는지 전부 기록하도록 했다.
  Previous 버튼을 누르면 record 리스트의 가장 끝에 있는 값을 읽어 그 반대의 연산을 시켜주면 된다. 예를 들어 마지막에 Red 버튼을 눌렀다면 record 리스트의 가장 끝에는 "r"이 있을 것이고, 이때 Previous 버튼을 눌러주면 "r" 값을 읽어와 Red를 첨가한 횟수를 하나 줄여줄 것이다.

  만들어놓고 보니 RGB 값을 정확하게 맞추는 것은 불가능했다. 심지어 맞춰야하는 RGB 값을 수치로 알고 있다 하더라도 색 조합 방법이 이렇다보니 정확히 맞출 수 없었다. 그래서 게임종료 조건을 주어진 RGB 값에 어떤 오차범위 이내로 RGB 값을 만들어내면 되는 것으로 만들었다. 색의 값이 255까지 있기 때문에 10%인 25를 적용했으나 너무 쉬워서 20으로 적용해보았다.
  즉 (R,G,B)=(120,130,140)이 주어졌다면, (R,G,B)=(120±20,130±20,140±20)의 범위 안에 들어오게끔 만들어주면 게임이 끝난다.

  게임이 종료되면 팝업 창이 뜬다. 주어진 RGB 값과 내가 만든 RGB 값, 오차율을 표시해주고, 게임을 다시 할 것인지 여부를 묻는다. Yes를 누르면 게임을 다시 시작하고 No를 누르면 프로그램이 종료된다. 이때 오차는 RGB 각각에 대한 오차의 max 값으로 표현해주었다.

 

마치며

  코드가 길긴 하지만 사실 절반이 UI 구성이다. 머리 쓰기 싫어서 전부 직접 지정해주느라 그렇다.. 나머지는 처음 프로그램을 구성해주는 메소드들과 버튼을 누를 때 어떤 버튼이 눌렸다는 정보를 저장하고 이에 따라 변경된 값을 화면에 다시 띄워주는 메소드들이다. 위에서 장황하게 설명한 게임이 만들어진 과정을 참고하고 소스코드 자체에 있는 주석을 참고한다면 프로그램을 이해하기 어렵진 않을 것으로 생각된다. 그래도 이해가 되지 않는 부분이 있다면 제보를 받아 설명을 덧붙여보려고 한다.

 

  어디가서 내세울만한 작품은 아니지만, 그래도 내 생각 속의 게임을 표현해냈다는 점과 제작 과정에서 부딪힌 문제들을 해결하는 과정이 나름 괜찮았다고 생각하여 공유한다.


 

728x90
반응형

댓글