tkinter를 이제 결합해보려고 합니다.
https://076923.github.io/posts/Python-tkinter-19/
tkinter 와 관련된 강좌글은 이 게시글을 참고하여 작성했습니다.
설명을 정말 친절하면서도, 자세하고, 이해하기 쉽게 잘 해주셨더라구요ㅎㅎ
프로그래밍 과정은 사실 이것보다 더 복잡하게 수많은 디버깅을 거치며 진행했지만,
현재까지의 결과물을 토대로 간단하게 과정을 적어보고자 합니다.
cpu_try = list()
cpu_num = [0]
answer_cmp = set()
layout = list()
우선 변수를 선언해주었습니다.
tkinter의 버튼 동작과 관련해 함수를 사용하게 되는데,
파이썬의 경우 함수내에서 함수 외부 변수를 컨트롤하는 방법은 없으나
함수 외부의 객체 중, list, dictionary, set 같은 mutable 객체는 함수 내에서도 읽고 쓰기가 가능하다고 알고 있습니다.
그래서 함수 내부에서도 컨트롤이 가능하도록 리스트를 활용했습니다.
cpu_try 는 컴퓨터가 사람이 생각한 숫자를 맞추기 위해 시도하는 숫자들을 저장하는 리스트입니다.
이 리스트에는 계속 시도 값들이 누적하여 들어가며, 이 리스트에 들어있는 값의 개수가 곧 시도횟수가 됩니다.
사람이 먼저 시도하고 컴퓨터가 나중에 시도하는 경우,
사람이 먼저 정답을 맞춘다면 시도횟수와 cpu_try 리스트의 원소 개수가 다르지 않냐고 할 수도 있겠습니다만,
초기 리스트의 개수를 1로 설정하여 이 문제를 방지했습니다.
어차피 첫 시도는 가능한 모든 경우의 수중에 하나를 랜덤으로 찍을 것이기에 미리 넣어놔도 괜찮기 때문이죠.
cpu_num 은 컴퓨터가 생각한, 사람이 맞춰야 할 숫자를 저장하는 리스트입니다.
이 리스트에는 값이 누적되지 않고, 오직 한개만이 존재합니다.
함수 내부에서 읽고 수정할 수 있게 하기위해 리스트를 덧입힌 것 뿐입니다.
answer_cmp 는 (변수 이름을 잘못 지은 느낌이 없잖아 있지만) 정답군 후보 튜플들이 들어갈 집합입니다.
이전 포스팅에 적은 대로 각 시도 후 만들어진 정답군들과의 교집합이 저장될 집합이기도 합니다.
layout 은 매 게임마다 생성되는 라벨, 메세지 등 tkinter 위젯들을 한번에 저장하는 리스트입니다.
이 리스트를 만든 이유는 한 게임이 끝나면 위젯들을 모두 지워야 하는데, 일일히 위젯마다 네이밍을 할 수 는 없죠.
그래서 위젯을 만들 때마다 layout 리스트에 넣어두고,
게임이 끝나면 반복문을 돌려 리스트에 들어있는 모든 위젯을 삭제한 다음 리스트를 비우려고 합니다.
from tkinter import *
from random import *
from tkinter.font import *
window = Tk()
window.geometry('700x500')
window.resizable(0,0)
window.title('인공지능 숫자야구')
font_title = Font(family="나눔 고딕",size = 20,weight='bold')
font_menu = Font(family='나눔 고딕',size = 10)
menu_frame = Frame(window,relief='solid',width = 700,height = 300)
menu_frame.pack(expand = True, fill = 'both')
Label(menu_frame,text = '인공지능 숫자야구',font=font_title).pack()
select_frame = LabelFrame(menu_frame,width = 400,height = 50,relief='groove',text = '<메뉴>'
,labelanchor='n',font=font_menu)
select_frame.pack()
play_frame = LabelFrame(window,text='<Score Board>',relief='groove',labelanchor='n',width = 600, height = 200)
play_frame.place(anchor = 'center',x = 350,y = 380)
s_cpu_btn = Button(select_frame,text = '컴퓨터와 대전',command = lambda : msg(1))
s_cpu_btn.place(anchor='center',x=80,y=15)
s_ply_btn = Button(select_frame,text = '친구들과 대전',command = lambda : msg(2))
s_ply_btn.place(anchor='center',x=180,y=15)
s_set_btn = Button(select_frame,text = '설정'); s_set_btn.place(anchor='center',x=260,y=15)
info = Label(window, text='선택 대기중...'); info.place(anchor='center',x=350,y=235)
_t = Entry(window,state = 'disabled'); _t.place(x=260, y=250)
ok_btn = Button(window, text='확인', command=lambda: enter(_t.get()), state = 'disabled')
ok_btn.place(x=410, y=246)
window.mainloop()
코드는 정말 길지만 하나하나 위젯을 일일히 넣다보니 생기는 노가다의 결과일 뿐입니다.
윈도우 창 크기 설정, 창크기 변경 가능여부, 윈도우 창 이름, 폰트 설정을 모두 해주고
크게 메뉴선택이 이루어질 프레임과, 게임 진행상황을 보여줄 프레임을 위 아래로 나누어 만들었습니다.
s_cpu_btn 은 '컴퓨터와 대전' 버튼
s_ply_btn 은 '사람과 대전' 버튼
s_set_btn 은 '설정' 버튼입니다.
s 는 select , btn 은 button 을 의미합니다.
info 라벨은 Entry 창 위에서 무엇을 입력해야할지 안내하는 문구입니다.
처음엔 메뉴를 선택해야하므로 선택을 대기하는 문구를 넣었습니다.
_t는 try 의 줄인말로 이름지은 Entry 위젯입니다.
초기에는 비활성화 시켜두었습니다.
ok_btn은 확인 버튼입니다 Entry 위젯에 적힌 숫자와 문자를 함수로 전달해 줄 친구입니다.
처음엔 enter() 함수를 연결지어뒀습니다.
그리고 s_cpu_btn, s_ply_btn 에는 msg() 함수를 연결했습니다.
선택에 따라 msg에 들어가는 인자가 다르죠?
사실 굳이 이렇게 할 필요는 없겠지만,
함수에 인자가 있을 때 버튼에 연결짓는 방법을 공부하려고 이렇게 해보았습니다.
이게 기본 틀의 끝입니다. 나머지는 다 함수정의거든요.
아직 함수에 익숙하고, 구조체나 클래스에는 익숙하지가 않아 함수 위주로 사용하게 되네요..
실행시켰을 때의 모습입니다.
메뉴창에 있는 버튼들 위치가 좀 불편하긴 하지만,
제일 오른쪽에는 프로그램 종료 버튼을 넣을 생각입니다.
그 버튼을 넣고나서 위치를 좀 조정해볼까 합니다.
우선 게임을 시작하면 처음으로 누르게 될 버튼은 어떤 상대와 대전할지 결정하는 버튼이므로
msg함수부터 설명해보고자 합니다.
def msg(n):
if n == 1:
cpu_try.append(make_num()) # 컴퓨터가 사람의 수를 맞추는 시도
cpu_num[0] = make_num() # 사람이 맞춰야하는 컴퓨터의 수
print('cpu num : ', cpu_num[0])
s_cpu_btn['state'] = 'disabled'; s_ply_btn['state'] = 'disabled'; s_set_btn['state'] = 'disabled' #버튼 비활성화
ok_btn['state'] = 'active'; _t['state'] = 'normal' #입력창 활성화
ok_btn['command'] = lambda : enter(_t.get())
layout.append(Message(window,text='''컴퓨터와 대전하기는 플레이어와 컴퓨터가 번갈아가면서 숫자를 제시합니다.
플레이어는 컴퓨터가 맞출, 규칙에 맞는 숫자를 미리 생각해두시기 바랍니다.
플레이어부터 시작하여 숫자를 제시합니다. 플레이어가 제시한 숫자를 보고 컴퓨터가 결과를 알려줄 것입니다.
컴퓨터는 결과를 알려준 후에 숫자를 제시합니다. 플레이어는 컴퓨터가 제시한 숫자를 보고 결과를 알려주셔야 합니다.
결과는 스트리아크의 개수와 볼의 개수를 공백을 두고 입력하여 '스트라이크 볼' 형태로 알려주시면 됩니다.
만약 결과가 1스트라이크 1볼이라면 입력창에 '1 1' 을 입력한 후 확인을 누르면 됩니다.'''
,width = 690,relief = 'solid')); layout[len(layout)-1].place(anchor='center',x=350,y=155)
info['text'] = '당신의 차례 입니다. 추측한 컴퓨터의 숫자를 입력하세요'
layout.append(Label(play_frame, text='<플레이어1>')); layout[len(layout)-1].place(x=50, y=0)
layout.append(Label(play_frame, text='<CPU>')); layout[len(layout)-1].place(x=160, y=0)
layout.append(Label(play_frame, text='회차')); layout[len(layout)-1].place(x=10, y=20)
layout.append(Label(play_frame, text='입력')); layout[len(layout)-1].place(x=50, y=20)
layout.append(Label(play_frame, text='결과')); layout[len(layout)-1].place(x=90, y=20)
layout.append(Label(play_frame, text='입력')); layout[len(layout)-1].place(x=150, y=20)
layout.append(Label(play_frame, text='결과')); layout[len(layout)-1].place(x=190, y=20)
elif n == 2:
layout.append(Message(window, text='''사람과 대전하기는 플레이어가 서로 번갈아가면서 숫자를 제시합니다.
플레이어는 서로 맞출, 규칙에 맞는 숫자를 미리 각자 생각해두시기 바랍니다.
플레이어1부터 시작하여 숫자를 제시합니다. 플레이어가 제시한 숫자를 보고 컴퓨터가 결과를 알려줄 것입니다.
컴퓨터는 결과를 알려준 후에 숫자를 제시합니다. 플레이어는 컴퓨터가 제시한 숫자를 보고 결과를 알려주셔야 합니다.
결과는 스트리아크의 개수와 볼의 개수를 공백을 두고 입력하여 '스트라이크 볼' 형태로 알려주시면 됩니다.
만약 결과가 1스트라이크 1볼이라면 입력창에 '1 1' 을 입력한 후 확인을 누르면 됩니다.'''
,width = 690,relief = 'solid')); layout[len(layout)-1].place(anchor='center',x=350,y=155)
info['text'] = '사람과 대전합니다.'
길지만 다 예쁘게 꾸밀려고 넣은 문구들이 대부분입니다.
컴퓨터와 대전하기를 누르면 메뉴에서 선택할 수 있는 세 개의 버튼이 모두 비활성화 됩니다.
그리고 Entry 위젯과 확인 버튼이 활성화 되죠.
또한 컴퓨터가 생각한 수를 만들어 cpu_num[0]에 넣고,
컴퓨터가 시도할 숫자를 미리 리스트에 넣어둡니다.
이 두가지는 모두 랜덤을 이용해 만들었고, 그 기능을 make_num() 함수가 담당합니다.
그 함수는 다음과 같습니다.
def make_num():
frt = randint(1, 9)
sec = randint(1, 9)
while sec == frt:
sec = randint(1, 9)
trd = randint(1, 9)
while (trd == frt or trd == sec):
trd = randint(1, 9)
return (frt,sec,trd)
그 다음으론 게임을 설명하고, 아래 score board 에 진행상황을 보여줄 틀을 짭니다.
짜여진 틀, 메세지는 모두 아까 만들어뒀떤 layout 리스트에 생성과 동시에 들어가게 됩니다.
이 게임이 끝나면 모두 한번에 지워야 하니까요!
아직 사람과 대전하는 기능은 구현을 안해놓아서 설명만 적어놓았습니다.
컴퓨터와 대전을 누르면 이렇게 나옵니다.
우선 처음엔 사람이 먼저 시도하므로 사람이 시도한 결과를 토대로 컴퓨터가 결과를 알려주어야 합니다.
우선 그 기능을 수행하는 함수를 확인 버튼에 연결했습니다.
def enter(a):
_t.delete(0,3)
S = 0; B = 0
for i in range(3):
if int(a[i]) in cpu_num[0]:
if int(a[i]) == cpu_num[0][i]:
S += 1
else:
B += 1
layout.append(Label(play_frame, text = '{}회'.format(len(cpu_try))))
layout[len(layout)-1].place(x = 10, y = 20+20*len(cpu_try))
layout.append(Label(play_frame, text = a)); layout[len(layout)-1].place(x = 50, y = 20+20*len(cpu_try))
layout.append(Label(play_frame, text = '{}S {}B'.format(S, B)));
layout[len(layout)-1].place( x = 85, y = 20+20*len(cpu_try))
if S == 3: #사람이 정답을 맞췄을 경우
info['text'] = '당신이 정답을 맞췄습니다. 플레이어 WIN!!'
ok_btn['text'] = '메뉴선택으로'
_t['state'] = 'disabled'
ok_btn['command'] = lambda : initialize()
else:
cpu()
그 함수가 바로 초기에 확인 버튼에 연결되어있던 enter 함수입니다.
_t.delete(0,3) 은 Entry 를 비우는 기능을 합니다.
사람이 입력한 숫자와 컴퓨터가 생각한 숫자를 비교하여 S, B 개수를 카운트하고 알려줍니다.
만약 사람이 숫자를 맞췄다면 확인 버튼의 이름을 '메뉴선택으로' 라고 바꾸고
연결 된 함수를 initialize 함수로 바꿔줍니다.
그렇지 않다면 cpu() 함수를 실행시킵니다.
123을 넣고 확인을 누른 결과입니다.
cpu가 0S 0B 라고 결과를 알려주고, 그 값을 출력하였습니다.
지금 사진을 보면 CPU가 사람이 생각한 숫자에 대하여 153을 시도했네요.
이 기능은 위에 언급한 cpu() 함수를 통해 실행되었습니다.
def cpu():
print('cpu try: ', cpu_try[len(cpu_try)-1]) #test
layout.append(Label(play_frame, text = "".join(map(str,cpu_try[len(cpu_try)-1]))));
layout[len(layout)-1].place(x = 150, y = 20+20*len(cpu_try))
info['text'] = 'CPU에게 결과를 알려주세요.'
ok_btn['command'] = lambda : enter_r(_t.get())
매우 간단한 코드입니다.
저 print 문은 디버깅용으로 넣은 코드라 빼도 무관합니다.
컴퓨터가 시도한 숫자를 출력하고 info에 들어갈 텍스트를 수정한다음
확인 버튼을 눌렀을 때 실행시킬 함수를 바꿔줍니다.
컴퓨터가 시도한 숫자는 처음 msg함수가 실행되었을 때 이미 담겨있었죠?
그래서 위 사진에서도 볼 수 있듯, 회차에 1이라고 적혀있습니다.
(회차에 적혀있는 숫자는 코드를 보면 알수 있듯 cpu_try 리스트의 원소개수입니다)
이제 결과를 알려주고 알려준 결과를 enter_r() 함수를 통해 컴퓨터에게 전달합니다.
def enter_r(a):
_t.delete(0, 3) #입력창 비우기
layout.append(Label(play_frame, text='{}S {}B'.format(a[0],a[2])));
layout[len(layout)-1].place(x=190, y=20 + 20*len(cpu_try))
ok_btn['command'] = lambda: enter(_t.get())
info['text'] = '당신의 차례 입니다. 추측한 CPU의 숫자를 입력하세요'
result = tuple(map(int, a.split())) # send result
answer_group(result)
역시 입력창을 다시 비워주고, 우리가 알려준 결과를 화면에 표시합니다.
이제는 우리가 다시 컴퓨터의 숫자를 맞춰야 할 차례이므로
확인 버튼에 연결된 함수를 처음에 연결했던 enter() 함수로 바꿔줍니다.
우리가 입력했던 결과는 튜플로 만들어 result 변수에 담고,
담은 변수를 answer_group 함수에 인자로 전달합니다.
사실 result 변수는 필요없는 변수입니다.
다만 인자로 무엇이 전달되었는지 명시적으로 알려준다는 점에서는 좋은 것 같아 남겨두었습니다.
이제 이 함수로 인해 실행되는 answer_group 함수를 볼까요?
함수가 함수를 부르고 함수가 함수를 부르는 과정의 연속이죠? ㅋㅋ
def answer_group(result):
frt, sec, trd = cpu_try[len(cpu_try)-1]
answer = set() #contemporary answer set
if result == (3, 0): # answer = 3S
info['text'] = 'CPU가 정답을 맞췄습니다. 컴퓨터 WIN!!'
ok_btn['text'] = '메뉴선택으로'; _t['state'] = 'disabled'
ok_btn['command'] = lambda : initialize()
return
elif result == (2, 0): # 2S 0B
for i in range(1, 10):
if i == frt or i == sec or i == trd:
continue
answer.add((frt, sec, i))
answer.add((frt, i, trd))
answer.add((i, sec, trd))
elif result == (1, 2): # 1S 2B
answer.add((frt, trd, sec))
answer.add((trd, sec, frt))
answer.add((sec, frt, trd))
elif result == (1, 1): # 1S 1B
for i in range(1, 10):
if i == frt or i == sec or i == trd:
continue
answer.add((frt, trd, i))
answer.add((frt, i, sec))
answer.add((trd, sec, i))
answer.add((i, sec, frt))
answer.add((sec, i, trd))
answer.add((i, frt, trd))
elif result == (1, 0): # 1S
for i in range(1, 10):
if i == frt or i == sec or i == trd:
continue
for j in range(1, 10):
if j == i or j == frt or j == sec or j == trd:
continue
answer.add((frt, i, j))
answer.add((i, sec, j))
answer.add((i, j, trd))
elif result == (0, 3): # 3B
answer.add((sec, trd, frt))
answer.add((trd, frt, sec))
elif result == (0, 2): # 2B
for i in range(1, 10):
if i == frt or i == sec or i == trd:
continue
answer.add((i, trd, sec))
answer.add((trd, i, sec))
answer.add((sec, trd, i))
answer.add((i, trd, frt))
answer.add((trd, i, frt))
answer.add((trd, frt, i))
answer.add((sec, frt, i))
answer.add((sec, i, frt))
answer.add((i, frt, sec))
elif result == (0, 1): # 1B
for i in range(1, 10):
if i == frt or i == sec or i == trd:
continue
for j in range(1, 10):
if j == i or j == frt or j == sec or j == trd:
continue
answer.add((i, frt, j))
answer.add((i, j, frt))
answer.add((sec, i, j))
answer.add((i, j, sec))
answer.add((trd, i, j))
answer.add((i, trd, j))
elif result == (0, 0):
for i in range(1, 10):
if i == frt or i == sec or i == trd:
continue
for j in range(1,10):
if j == i or j == frt or j == sec or j == trd:
continue
for k in range(1,10):
if k == j or k == i or k == frt or k == sec or k == trd:
continue
answer.add((i, j, k))
if len(answer_cmp) == 0:
answer_cmp.update(answer)
else:
answer_cmp.intersection_update(answer)
cpu_try.append(choice(list(answer_cmp)))
제가 쓴 코드의 총 줄 수가 200줄이 조금 넘는데, 그 중에 절반을 차지하는 친구되겠습니다.
컴퓨터와 대전하기의 기능의 핵심코드이기도 하죠.
이전 포스팅에서 떠올리고 만든 알고리즘을 그대로 옮겼습니다.
차이점이 있다면
answer_cmp.update(answer) 을 사용했다는 점이겠네요.
함수를 사용하지 않았을 때는
answer_cmp = answer
로 작성해도 충분히 집합이 복사가 되었지만,
함수 내부에서는 이렇게 쓸 경우
answer_cmp 를 함수 외부에 만들어뒀던 그 집합 객체가 아닌
함수내부에서 새로 만든 객체로 인식하더군요.
그래서 메소드를 무조건 사용해야 했습니다.
때문에 교집합을 넣어주는 과정도 intersection_update 메소드를 이용했습니다.
여기에서 맨 마지막에 cpu_try 리스트에 최종적으로 만들어진 정답 후보군 answr_cmp 집합에서
랜덤하게 하나를 골라 리스트에 추가하도록 했습니다.
함수 밖에서 어제 따로 코딩했을 때는 집합을 대상으로 choice를 해도 오류가 없었던 것 같은데,
이상하게 이렇게 옮기니 집합은 시퀀스 자료형이 아니라면서 오류를 뿜어내네요
그래서 그냥 list() 함수를 이용해 리스트로 바꿔서 뽑도록 했습니다.
3자리 코드가 이렇게 긴데, 자리수를 4자리로 바꾸면 얼마나 더 길어질지... 끔찍하군요
찾아보니 경우의 수가 13개던데,, 정말 노가다가 아닐 수 없겠습니다..
+++++++++++++++++++++++++++++++++++++++
이 함수가 실행되고나면 CPU가 시도할 다음숫자가 이미 결정되어 있는 상태로
다시 우리는 컴퓨터의 숫자를 맞추려고 시도합니다.
그렇기 때문에 회차도 2회차로 나오는 것이죠.
(cpu_try의 길이가 2 이므로)
이제 정답을 맞추면 사진으로 보듯, 버튼이 '메뉴선택으로' 라고 바뀝니다.
그 버튼을 누르면 실행될 initialize() 함수만 보면 끝입니다.
def initialize():
s_cpu_btn['state'] = 'active'; s_ply_btn['state'] = 'active'; s_set_btn['state'] = 'active' # 버튼 활성화
info['text'] = '선택 대기중...'
ok_btn['text'] = '확인'
ok_btn['state'] = 'disabled'
for i in range(len(layout)):
layout[i].destroy()
layout.clear()
cpu_try.clear()
비활성화 된 버튼들을 활성화시키고,
Entry와 확인버튼은 비활성화, 위젯의 이름을 바꿔준 다음,
게임이 진행하면서 만든 모든 위젯을 제거하고, cpu_try 리스트를 비워줍니다.
이렇게 됩니다.
그냥 맨 처음화면과 똑같죠ㅋㅋ
'팀 프로젝트 > [2020] 인공지능 숫자야구' 카테고리의 다른 글
[팀프로젝트 01] 인공지능 숫자야구 만들기(3) : 알고리즘 수정 (0) | 2020.06.02 |
---|---|
[팀프로젝트 01] 인공지능 숫자야구 만들기(2) : 인공지능 구현 (0) | 2020.05.28 |
[팀프로젝트 01] 인공지능 숫자야구 만들기(1) : 주제 구상 (0) | 2020.05.24 |