Pythonで大人気ゲームのWordleを遊べる簡易ゲームアプリを作ってみました。
PySimpleGUIを使ったデスクトップアプリでの実装です。
ルールは知っている人も多いかと思いますが…
四角の中にアルファベットを入力していき、5文字の英単語を当てるゲームです。
5文字の英単語を入力するたびヒントが表示されていきます。
チャンスは6回まで。このヒントを元に正解を目指します。
Wordle公式
https://www.powerlanguage.co.uk/wordle/
プログラミングの勉強も兼ねて、再現してみようと思いました。
プログラムを起動させた後は、キーボードボタンを押して、単語を入力していきます。
単語ができたらENTER
ボタンを押します。
入力に応じて判定が走り、ヒントが表示されます。
単語リストにない単語が入力されたら、先にいけません。
ここも本家の仕様と同じです。
6回チャレンジして正解できない場合、ゲームオーバーとなり正解が表示されます。
正解するとコングラチュレーションです。
長いです。
こちらはwordle.txtの単語データから、単語リストを返すだけのファイルです。
"""wordle.txtからwordリストを返す"""
def generate_word():
with open('wordle.txt') as f:
for word in f:
yield word.strip().upper()
def get_word_list():
return [word for word in generate_word()]
wordle.txtは以下のようにひたすら5文字の英単語が並んだテキストファイルです。
本家でリストが公開されているので、コピペしてます。
https://github.com/alex1770/wordle/blob/main/wordlist_hidden
aback
abase
abate
abbey
abbot
abhor
abide
...
Wordleのロジックをまとめているファイルです。
入力された単語の判定を主に行っています。
"""Wordleのロジックを定義するファイル"""
import random
class Wordle:
"""Wordleのロジック"""
def __init__(self, word_list: list):
self.word_list = word_list
self.answer = None
def set_answer(self):
"""answerをセットする"""
max_index = len(self.word_list)
index = random.randrange(start=0, stop=max_index)
self.answer = self.word_list[index]
def is_word_in_word_list(self, word: str):
"""単語がword listの中に存在していたらTrue"""
return word in self.word_list
def is_word_collect(self, word: str):
"""
5文字の単語が答えに合っていればTrue
"""
return word == self.answer
def is_char_in_answer(self, char: str):
"""
英語一文字が答えに含まれていたらTrue
"""
return char in self.answer
def is_char_right_position(self, char: str, pos: int):
"""
英語一文字が答えと照らし合わせて位置が合っていたらTrue
"""
return self.answer[pos] == char
こちらはGUIの見た目の定義をまとめています。
"""GUIの見た目を定義するファイル"""
import PySimpleGUI as sg
class Widget:
"""widgetを定義"""
"""スタイル"""
# relief style
relief_size = (30, 1)
relief_font = ("Helvetica", 15)
relief_text = "PysimpleGUI Wordle!"
# input box style
input_box_color = 'black'
input_box_color_bg = 'white'
input_box_size = (4, 1)
# keyboard button style
keyboard_btn_size = (4, 1)
keyboard_btn_color = ('#FFFFFF', '#283b5b')
# active row mark style
active_row_mark_text = "●"
active_row_mark_size = (4, 1)
active_row_mark_font = ('', 10)
# window style
window_title = 'PySimpleGui Wordle'
window_size = (800, 500)
def widget_relief(self):
"""リリーフ"""
return sg.T(text=self.relief_text,
size=self.relief_size,
justification='center',
font=self.relief_font,
relief=sg.RELIEF_RIDGE)
def widget_active_row_mark(self, color: str, key: str):
"""現在の入力行を示すマーク"""
return sg.T(text=self.active_row_mark_text,
size=self.active_row_mark_size,
text_color=color,
font=self.active_row_mark_font,
key=key)
def widget_input_box(self, key: str, disabled: bool):
"""input box widget"""
return sg.InputText('',
key=key,
size=self.input_box_size,
disabled=disabled,
text_color=self.input_box_color,
justification='c',
background_color=self.input_box_color_bg,
enable_events=True)
def widget_keyboard_button(self, char: str, key: str):
"""キーボードボタンwidget"""
return sg.Button(char,
key=key,
size=self.keyboard_btn_size,
button_color=self.keyboard_btn_color)
@staticmethod
def popup_does_not_exist(word: str):
"""単語リストにありませんpopup"""
sg.popup(f'Sorry, "{word}" does not exist in my word list')
@staticmethod
def popup_game_over(answer: str):
"""ゲームオーバーpopup"""
msg = f'Game Over!\nThe answer is {answer}\nDo it again?'
return sg.popup_ok_cancel(msg)
@staticmethod
def popup_congratulation():
"""ゲームクリアpopup"""
msg = 'Congratulation!\nDo it again?'
return sg.popup_ok_cancel(msg)
@staticmethod
def popup_see_you_later():
"""see you later popup"""
return sg.popup('See you later')
class GuiFrontEnd(Widget):
"""GUIの見た目を定義"""
def input_box_widgets(self):
"""縦6 x 横5の入力マス"""
layout = []
for row in range(1, 7):
disabled = False if row == 1 else True
color_mark = 'green' if row == 1 else 'white'
active_row_mark = self.widget_active_row_mark(color=color_mark,
key=f'row{row}')
widgets = [active_row_mark]
for col in range(1, 6):
# keyはr1c1形式で記入
key = f'r{row}c{col}'
widget = self.widget_input_box(key=key, disabled=disabled)
widgets.append(widget)
layout.append(widgets)
layout = sg.Column(layout=layout, justification='c')
return layout
def key_boards_widgets(self):
"""キーボードボタン"""
layout = []
keyboards = ['QWERTYUIOP', 'ASDFGHJKL', 'ZXCVBNM']
for chars in keyboards:
widgets = []
for char in list(chars):
widget = self.widget_keyboard_button(char=char, key=char)
widgets.append(widget)
widgets = sg.Column(layout=[widgets], justification='c')
layout.append([widgets])
return layout
def layout(self):
"""レイアウトを返す"""
col_relief = sg.Column([[self.widget_relief()]], justification='c')
col_control = sg.Column(layout=[[sg.Button('ENTER'),
sg.Button('BACK'),
sg.Button('PREV'),
sg.Button('NEXT'),
sg.Button('CLEAR')]],
justification='c')
layout = [[col_relief],
[self.input_box_widgets()],
[self.key_boards_widgets()],
[col_control]]
return layout
def window(self):
"""ウィンドウを返す"""
window = sg.Window(title=self.window_title,
layout=self.layout(),
size=self.window_size,
finalize=True)
return window
実際のゲーム進行と付随するロジックをまとめています。
"""実際にゲームするときに使うファイル"""
import PySimpleGUI as sg
import words
from wordle import Wordle
from frontend import GuiFrontEnd
class Game:
"""ゲームの進行とロジック"""
def __init__(self, word_list: list):
self.frontend = GuiFrontEnd()
self.window = self.frontend.window()
self.wordle = Wordle(word_list)
# 入力可能行の制御
self.turn = 1
@staticmethod
def get_input_widget_key():
"""入力widgetのキーをyieldして返す"""
for r in range(1, 7):
for c in range(1, 6):
key = f'r{r}c{c}'
yield key
@staticmethod
def get_keyboard_key():
"""キーボードwidgetのキーをyieldして返す"""
chars = "QWERTYUIOPASDFGHJKLZXCVBNM"
for c in chars:
yield c
def filter_values_by_turn(self, values: dict):
"""ターンに応じてvaluesをフィルタ"""
return dict(filter(lambda x: f'r{self.turn}' in x[0], values.items()))
def get_word_by_5chars(self, values: dict):
"""5文字の英単語を作成する"""
row = self.filter_values_by_turn(values)
word = ''
for char in row.values():
word += char
return word
def set_focus_next(self, key: str):
"""次のキーにフォーカスする"""
row_num = int(key[1])
col_num = int(key[-1])
# 最後の場合は最初に戻る
if col_num == 5:
next_key = f'r{row_num}c1'
else:
next_key = f'r{row_num}c{col_num + 1}'
self.window[next_key].SetFocus()
def set_focus_prev(self, key: str):
"""前のフォーカスのキーを返す"""
row_num = int(key[1])
col_num = int(key[-1])
# 最初の場合は最後に送る
if col_num == 1:
prev_key = f'r{row_num}c5'
else:
prev_key = f'r{row_num}c{col_num - 1}'
self.window[prev_key].SetFocus()
def clear_row(self, values: dict):
"""入力されている行をクリア"""
row = self.filter_values_by_turn(values)
for key in row.keys():
self.window[key].update('')
self.window[f'r{self.turn}c1'].SetFocus()
def update_active_row_mark_color(self):
"""入力可能行表示マークの色をアップデート"""
for row_num in range(1, 7):
color = 'white'
if row_num == self.turn:
color = 'green'
elif row_num < self.turn:
color = 'gray'
key = f'row{row_num}'
self.window[key].update(text_color=color)
def update_widget_bg_color(self, values: dict):
"""答えが入力された後のウィジェットの色を変える処理"""
row = self.filter_values_by_turn(values)
for i, char in enumerate(row.values()):
col_num = i + 1
input_key = f'r{self.turn}c{col_num}'
pos = i
if self.wordle.is_char_right_position(char=char,
pos=pos):
bg_color = 'green'
elif self.wordle.is_char_in_answer(char):
bg_color = 'orange'
else:
bg_color = 'gray'
self.window[input_key].Update(text_color='white',
background_color=bg_color)
# キーボードは既にgreenのものが上書きされないようにする
c = self.window[char].ButtonColor
if 'green' not in c:
self.window[char].Update(button_color=('white', bg_color))
def goto_next_turn(self, values: dict):
"""次のターンに移行"""
self.turn = self.turn + 1
row = self.filter_values_by_turn(values)
self.update_active_row_mark_color()
for key in row.keys():
self.window[key].Update(disabled=False)
self.window[f'r{self.turn}c1'].set_focus()
def refresh_game(self):
"""ゲームを初期化する"""
# 入力マスを初期化
for key in self.get_input_widget_key():
disabled = False if 'r1' in key else True
self.window[key].update('',
text_color=self.frontend.input_box_color,
background_color=self.frontend.input_box_color_bg,
disabled=disabled)
# キーボードボタンを初期化
for key in self.get_keyboard_key():
self.window[key].update(button_color=self.frontend.keyboard_btn_color)
# リフレッシュ
self.window.refresh()
# 答えを新しくセット
self.wordle.set_answer()
# ターンを戻す
self.turn = 1
# 入力可能行マークを更新
self.update_active_row_mark_color()
# フォーカスを一番最初に
self.window['r1c1'].SetFocus()
def start_game(self):
"""ゲームスタート"""
# wordleに答えをセット
self.wordle.set_answer()
keyboards_events = list(self.get_keyboard_key())
input_box_event = list(self.get_input_widget_key())
while True:
event, values = self.window.read()
if event == sg.WIN_CLOSED:
break
# inputボックスのフォーカスされているマス
focus_input = self.window.find_element_with_focus()
# 小文字で入力された場合、大文字に変換
if event in input_box_event:
char = values[event]
self.window[event].update(char.upper())
if event in keyboards_events:
# 違うターンのマスは入力できない
if f'r{self.turn}' not in focus_input.Key:
continue
focus_input.update(event)
self.set_focus_next(key=focus_input.Key)
# フォーカスされているマスを消去
if event == 'BACK':
focus_input.update('')
# 前のマスに移動
if event == 'PREV':
self.set_focus_prev(key=focus_input.Key)
# 次のマスに移動
if event == 'NEXT':
self.set_focus_next(key=focus_input.Key)
# 入力されている行をクリア
if event == 'CLEAR':
self.clear_row(values)
if event == 'ENTER':
# 入力値を5文字の英単語に
word = self.get_word_by_5chars(values=values)
# 5文字入力されているかの確認
if len(word) != 5:
continue
# 入力値が単語リストに存在しなかったら次にいけない
if not self.wordle.is_word_in_word_list(word=word):
self.frontend.popup_does_not_exist(word=word)
continue
# widgetの背景を更新
self.update_widget_bg_color(values=values)
# wordが正解していたらゲーム終了
if self.wordle.is_word_collect(word=word):
res = self.frontend.popup_congratulation()
if res == 'OK':
self.refresh_game()
continue
else:
self.frontend.popup_see_you_later()
break
# 正解していなくて既に6ターン目だったらゲームオーバー
if self.turn == 6:
res = self.frontend.popup_game_over(answer=self.wordle.answer)
if res == 'OK':
self.refresh_game()
continue
else:
self.frontend.popup_see_you_later()
break
# 次のターンに移動
self.goto_next_turn(values=values)
def job():
# 単語リストを取得
word_list = words.get_word_list()
game = Game(word_list)
# ゲーム開始
game.start_game()
if __name__ == '__main__':
job()
最新版をgithubに公開しています。
https://github.com/qlitre/pysimplegui-wordle
適当なディレクトリでgit cloneします。
git clone https://github.com/qlitre/pysimplegui-wordle/
仮想環境を作成してライブラリをインストールします。
cd pysimplegui-wordle
python -m venv myvenv
myvenv\scripts\activate
pip install -r requirements.txt
後はgame.pyを起動します。
game.py
Wordleはシンプルなゲームですので、プログラミングの題材にとても良いと思いました。
本家のWordleは一日に一回しか遊べないので、練習も兼ねていかがでしょうか。