見出し画像

Retro-gaming and so on

Pythonで作るハングマン CLI編

こういう記事を読んでたわけだが。




まぁ・・・うん(笑)。
いや、俺も最初はそう思ってました(爆

実はこれには複数理由が存在する。
大まかには次のようなものだろう。

  1. CLI(※1)に比べるとGUIのソフトウェアは作るのがメンド臭くて初心者向けじゃない。
  2. GUIのガワのプログラミングが長くなる割にはあまりにも定型的過ぎて、「プログラミング」という程面白くない
  3. 本の著者がUNIX系プログラマであって、実はGUIには詳しくない。
  4. GUIツールキットと呼ばれるライブラリにスタンダードが存在しない。
  5. プログラマにとってはGUIのソフトよりCLIのソフトの方が便利。
主に考えられる理由は上の5つだろう。
まず、1番から、だが、CLIのソフトウェアは比較的簡単に作れる。一方、GUIのソフトは大変なんだ。例えば端末相手になら"Hello, world"と表示させるのはハナクソなんだけど、GUIの場合、ウィンドウ作成・ウィンドウのサイズ決定、表示領域の設定、文字の大きさの設定等等等、たかがHello, worldと表示させる為だけにクソの量程の行数のコードが必要となる。
言い換えると、変数や引数が分からない初心者が書くにはあまりに敷居が高い。そしてそこに2番が絡む。書く量が多い割には決まりきったモノしかなくってつまらない。書くのが大変な割にはプログラミング、って程のクリエイティヴィティもクソもないんだ(※2)。CLIのソフトを書く時のような「様々な事柄を華麗に短く記述する」ようなマジックもあまりない。極論、画像配置の設定だけ、だしな。
3番も結構あり得る。実際、一概にプログラマ、っつっても、大まかには3種類くらいいる。Windowsプログラマ、UNIX系プログラマ、そしてゲームプログラマだ。同じプログラマとは言っても実はかなり違う(※3)。
ゲームプログラマを別にすると、UNIX系プログラマはコンピュータサイエンス的な、言わば「難しい問題を解く」事には強いけど、一般にはGUIには弱い。っつーか、そもそもその方面のプログラミングをあまりしない。
いわゆるWeb系プログラマ、と言うのはUNIX系プログラマで、Windows系プログラマとは住んでる世界が違う。クライアント(※4)で直接動かすようなプログラムは書かない。ExcelとかWordとか、要は「パッケージで買ったりする」プログラムだな、そういうのは書かないんだ。
反面、彼らは文字列処理は得意で、言っちゃえばHTMLなんかも「文字列処理範疇」なんでUNIX系プログラマの独断場なんだよ。文字列処理した「結果」はブラウザ上で「GUIっぽい動き」をするが、それはブラウザの能力であって、プログラマ側の能力じゃない。
そして、一旦クライアント向けのプログラミングになると、やっぱり行うのは「端末対象」でのプログラミングとなり、そもそもGUIのプログラミングなんざやらないわけだ。そして、新しくどんどん出てくるプログラミング言語は圧倒的にUNIX系OSで生まれている。結果、トピック的にはどうしても「得意な」CLIに寄せた話にならざるを得ないわけだ。
いずれにせよ、実はプログラマの種類によって「得意ジャンル」が違い、全員が全員、普段からGUIのソフトをプログラミングしてる、たぁ限らないわけだ。
次は4番だ。実は端末に比べると、GUI、と言うよりグラフィック制御自体からして共通基盤がこれ、と言ってあるわけじゃない。言わば一般論がしづらいわけ。
端末でも表示ではC言語ならprintf、Pythonならprint、Schemeならdisplay、Rustならprintln!と違いがあるわけだけど、一応端末ならそこそこ「違いを吸収してくれる」。一方、画像表示関連はまだまだそこまで標準化してない。
ハードウェアでもNVIDIA、AMDと最低でも2つあるし、OSレベルだとWindowsのDirect X、MacのCocoa、UNIX/LinuxのXWindow(※5)、と全く違うブツが走ってるし、それらをラップするライブラリも多岐に渡る。
と言うわけで、繰り返すが「GUIのプログラミング」の一般論はCLIのそれと比較すると「物凄くしづらい」んだ。
1〜4を見てくると、例えば、特にWindowsなんだけど、「簡単に」「Windows標準の」GUIプログラミングをしたい、っつーのなら、わざわざPythonとかRustなんかの「UNIXと深い関係を持った」プログラミング言語を選択しなくても、Visual Basicとか、もうちょっと高機能のプログラミング言語を使いたい、ってぇのならC#を使った方がマシだ、ってのが分かるんじゃないだろうか。Visual BasicとかC#なら「GUIアプリを作る方法が書かれている本」なんかもあるだろう。Microsoftのお膝下だし、全てが統一的に組み込まれてる。「Visual Basicで書かれたGUIアプリをMacに持っていこう」とでもしない限りトラブルは生じない。そして世の中のパソコンの90%はWindowsなんだ。

Microsoft Visual Basic。GUIアプリを作りたいのなら左から「ウィジェットを選んで」貼り付けてそれにプログラムを仕込めばいい。わざわざ無理してPythonやRustで「コードを書かなくても」比較的簡単にGUIアプリが作れるし、それが「Visual」Basicのウリだ。

Microsoft以外の商用製品ではEmbacadero Delphiと言うプログラミング言語処理系が有名。Visual Basicと同様に、ウィジェットをペタペタ貼っていき、それにコードを関連付けてGUIアプリを作成出来る。

5番目はちょっと解説が必要だろう。
確かに一般人にとってはGUIアプリが使いやすく便利だ。一見、CLIのアプリで「コマンドを打ちながら操作する」のは圧倒的にメンド臭い。それはプログラマにとっても同じじゃないか?って思うだろう。
ところが、CLIのアプリだとこういう事が出来るんだ。
例えばPythonで次のような単純なプログラムを書いてfoo.pyと言う名前を付けて保存する。

#!/usr/bin/env python3

def foo():
 print(1 + 2)

if __name__ == '__main__':
 foo()

1 + 2を計算して表示するだけのくだらないプログラムだ。
ついでに次のような、また単純なプログラムを書いてbar.pyとして保存する。

#!/usr/bin/env python3

def bar():
 x = int(input())
 print(x + 3)

if __name__ == '__main__':
 bar()

これも標準入力から数値を取り、それに3を足して表示するだけ、の至極くだらないプログラムだ。
さて、この2つのプログラムは互いに独立してて全く関係がない。
ところが、例えばLinux上だと端末で次のような事が出来る。



6、と出力されてるが、意味が分かるだろうか?
foo.pyは3を出力するプログラムだった。bar.pyは標準入力を数値に変換した数に3を足すプログラムだった。結果3 + 3は6で6が表示されている。
つまり、上の例ではfoo.pyの標準出力をbar.pyの標準入力へと直接流し込んでいる。要は2つの独立したプログラムを連結させた結果を表示してるわけだ。Linuxの端末でのこの機能をパイプと呼ぶ(※6)。上の画像の"|"はパイプコマンドだ。
これは一例だが、意味が分かるだろうか。そう、CLIアプリなら端末上で複数連結して一つの結果を得る事が可能、つまり、アプリ同士の連携が出来ると言う事だ。これがGUIアプリだと不可能、あるいはやりづらい、って事になる。
CLIのプログラムだと一つのプログラムで複雑な事をやらなくていい。むしろ一つのプログラムは至極単純な計算をするようにして複数のプログラムを書く、あるいは必要に応じてOSの機能を呼び出し、それらを組み合わせて複雑な結果を得る事が出来る。これは非常にパワフルで、結果物事を簡単にやり遂げる事が出来、まさしくプログラマにとっては「ラクな」環境を提供している。繰り返すが、こういう利便性はGUIだと得ることが出来ない、あるいは得るのが難しいんだ。
プログラマは一般の人に比べるとこういう「プログラミング上のショートカット」をたくさん知ってるし、活用出来るし、実際多く使う。結果、GUIアプリよりCLIのアプリの方が、プログラマにとっては実用上のメリットがデカいんだ。
そういうわけで、「人に使ってもらうならいざ知らず」、自分で作って使うならCLIで作る方がラクだし便利だ、って事があるわけ。

とまぁ、そういう複数の理由が背景にある。
とは言っても、「普段使ってるソフト」がGUIで、それが当たり前な以上、「プログラミングがやりたい!」って思った場合、「GUIのソフトを作りたい!」ってなるのは当然と言えば当然だ。「目の前で動いてる何か」に憧れていて「作ってみたい!」ってなるのはキッカケとしては当然で、事実、昔の人は、CLIが当然だった時代、目の前で動いてるCLIのソフトを見続けた経験上、「"Hello, World!"」って端末で表示されるだけの事に感動してたわけだ。


要は、1980年代くらいだと「目の前で使ってるモノ」と「これくらい出来るんじゃねぇの?」と言う感想が一致してたわけだ。言い換えると「敷居が低かった」。一転して、今だと「目の前で使ってるモノ」と「これくらい出来るんじゃないか?」が一致していない。敷居が上がってる。
初音ミクの音楽作りが流行ったのも(全般的な「出来」はともかくとして)、「これくらい出来るんじゃねぇの?」ってのが大きい。敷居が低く見えたからこそ「やりたい」と言うモティヴェーションに繋がったわけだ。「なろう」の小説もそうだよな。
余談だけど、かつてのドラクエのプログラマ、中村光一も「ディグダグみたいなゲームを作りたい」事がデビュー作「ドアドア」に繋がったらしい。中村光一のアタマの中では「ディグダグのようなゲームを作る」事は充分実現可能な範囲だったわけだ。

 
ナムコのディグダグ(左)と中村光一のデビュー作ドアドア(右)。ディグダグはアーケードゲームだったがこれをプレイしまくった中村光一にはこれが「実現可能」なモノに思えたらしいし、昔のゲームは単純だったんで、そういう人は昔はたくさんいた。

単純なモノだからこそ「一人でも実現可能」と思えるわけだが、反面、今のゲーム、例えば「FINAL FANTASY XVI」なんかを子供が遊んでみて、「これを作りたい!」と思えるのだろうか、と言うと違うと思う。あまりにも「目の前で動いているモノ」が複雑だとそもそも「実現可能性」なんかに思いもよらないわけだ。
それくらい、現代では「目の前で動いてるモノ」と「実現可能性」は乖離している。

とまぁこれが前フリだ。
一方、GUIアプリとは言えど、作り方は面倒くさいけど「理論的な話をすれば」CLIアプリの延長線上に無いわけでもない。
ここではCLIのゲームをPythonで作ってみて、それをGUIに変更していく過程を紹介してみよう。お題は「ハングマン」だ。
ハングマンは単純な英単語当てゲームだ。もとこんぐさん向けのネタだな(笑)。絞首刑台がある。アルファベットを一文字づつ入力していき、決められた回数内に「出題された英単語」を当てる。入力した文字が出題された英単語に含まれてない度に絞首刑台に吊るされた男がパーツ毎に追加されていく。誤回答回数が規程回数に達した時点で絞首刑台に吊るされた男の絵が完成するわけだ。
何故に絞首刑と英単語当てが関係してんのか?と問われても「分からん」としか言いようがない(笑)。白人らしい悪趣味さと言えよう。何故これを?と言われたら、それは「僕個人が初めて作ったGUIアプリだから」だ。いや、作った、っつーより本に書かれた通りに打ち込んだ、って言った方が正しいが。

 古い本だが良書。原作は英国にあるWebサイト。ただし、既にハングマンのお題は消えて久しい。

GUIアプリ作成法にはサッパリ慣れなかったが、個人的に思い出深いのでこれを取り上げる。当然、この本の中で紹介されているアプローチとは違ったアプローチをする。
また、「作れそうだ」と言う印象でも難易度が低いだろう。具体的には紹介しないが、スマホなんかでの無料ゲームの類でも数多く流通してるんで、まずは検索/インストールして「やってみて」欲しい。「この程度のゲームならハナクソで作れるんじゃないか?」と思ってもらえればいい。
では始めようか。

まず、何度も繰り返すが、CLIアプリの基本的な構成法をREPL(Read-Eval-Print Loop)と呼ぶ。アプリを読み込み部(Read)、評価部(Eval)、印字部(Print)と3つの部品に分けて設計し、最終的にそれらを組み合わせたモノをループ(Loop)させて作る。
これを基本として押さえておくべきだ。これを知らないと、徒手空拳でプログラムを「あーでもないこーでもない」と弄りまくり、結果ドツボにハマる事も珍しくない。REPLはインタプリタの設計法として有名だが、極論、全てのアプリはインタプリタ、あるいはその変種だ。
そしてある意味、コンピュータサイエンス及びソフトウェア工学のキモは「あるソフトウェアを作り上げる方法論」を論じてる、と言うより「目的とするブツをどうパーツとして分割していくか」を論ずる事だ。手続き型言語が「関数」や「手続き」と言うパーツ作成法を示したり、オブジェクト指向言語が(一般的には)「クラス」と言うパーツ作成法を基本として提供している理由も「どう分割していくか」と言う発想と深く結びついている。まとめるのはあとでどうにでも出来る。「分割法」が重要なんだ。
繰り返す。REPLの各部品は次のような役割がある。

  • 読み込み部: ユーザーから入力を受け、基本的にはそれをそのまま返す。
  • 評価部: アプリで行う「計算処理」を司る。そしてその計算結果を返す。
  • 印字部: 評価部から受け取った結果、あるいはその一部を表示する。更に、ゲームなんかのプログラムでは評価部から受け取った計算結果をそのまま返す。
コンピュータサイエンス的には評価部を更に分割する事が可能だけど、そのテクニックはそのうちチャンスがあったら紹介する。ブログには何度もヒントは書いてるけどね。
いずれにせよ、最重要部は評価部で、これがあらゆるアプリにとっての心臓部だ。ゲームで言うとゲームエンジンになる。
評価部は通常、2引数関数となり、1つ目の引数は読み込み部から手渡される入力情報、2つ目の引数は通常「環境」と呼ばれるモノとなる。
「環境」はゲームで言うと現在のゲームの進行状況を全て詰め込んだデータだ。仮にゲームにセーブ機能があるとすれば、この「環境」情報をファイルに書き出せば「保存」が成立する。逆にファイルに書き出された「環境」を読み込めば「ロード」が出来た、と言う事だ。
アプリとは、極論、この「環境」と呼ばれるデータの加工が目的のプログラムだ。そして評価部の仕事はこの「環境」を「データ加工」する事だ。
よってまずはハングマンに於ける「環境」を定義しよう。ハングマンに必要な最低限のデータとは何だろうか。ちと考えてみよう。

  • 答えとなる英単語
  • ユーザー入力によって作成された単語パーツ
  • 何回目の間違いか、を保持するカウンター
  • 既存の入力文字を保持するエリア
この4つとなるだろう。当然「答えとなる英単語が何か」は持ってないとならないし、ユーザーが作りかけの「回答」も無いとならない。ゲームに回数制限がある以上カウンターもなきゃいけないし、また「既に入力した文字」に関しては入力を受け付けない(あるいはカウンターを増加させない)仕組みの何らかの基礎データが必要だ。
これら基本的には4つのデータをまとめて一つにせなアカンわけだが、こういう場合にフツーの言語で使うのが構造体、とかレコード型と言われるデータ型だ。
一方Pythonには構造体もレコード型も無い。代替としてクラス、と呼ばれるモノを使う(※7)。
ここではいくつかデータを追加して「環境」を定義してみる(※8)。



「環境」のデータ型であるEnvクラスだが、先程の4要素が含まれていれば基本どうデザインしてもいい。注意点は一つだけ、だ。クラス設計をする際には特殊メソッドである__repr__は設定しておいた方がいい。
と言うのも、クラスはそのままだと中身を見る事が出来ない。と言う事はデバッグの際、「現時点の環境の中身がどうなってるのか」知る事が出来ない。これは非常に歯がゆいんだ。
一方、__repr__さえ設定しておけば、Envクラスのインスタンスをprintに渡せばその中身を設定に準じ表示してくれる。従って、プログラム上でどうミスしたのか、も一目瞭然になるわけだ。

>>> env = Env("hoge", "----")
>>> print(env)
<the Word: hoge, Word: ----, count: 1, Guessed: [], Word #: 1, Current: 0, Overall: 0, msg: >

さて、インスタンス変数であるtheWordだが、ここには出題である「英単語」が入る。その英単語をどこから引っ張ってくるのか、と言うと、当然英単語の辞書からだ。これをどうすんのか、って問題がある。
結論から言うと、Web上に出回ってるフリーの英英辞書辺りから編集して作る、なんつー手もあるが面倒くさい。幸いな事にハングマン用辞書なんつーのも存在して公開されている。これをパクって来よう(笑)。40,000語以上収録されているスグレモノだ。
これをコピペでも何でもして、dictionary.txtと名付けてワーキングディレクトリに保存する。プログラム側ではそのファイルを読み込んで辞書リストとして保持しておくわけだ。

# 英語辞書定義
with open('dictionary.txt', 'r') as f:
 dictionary = [line.rstrip() for line in f]

辞書リストを作りそこからランダムに英単語を選んで出題とする。具体的にはrandomモジュールのchoice関数dictionaryに適用すればいい。

>>> choice(dictionary)
'vigil'
>>> choice(dictionary)
'neologism'
>>> choice(dictionary)
'unconnected'

これを利用して「環境」の初期化関数を書く。

# 初期化
def initialize():
 theWord = choice(dictionary)
 return Env(theWord, ''.join(['-' for i in theWord]))

ポイントは、まずはdictionaryから一つ単語をランダムに選び、回答用のインスタンス変数Wordにその単語の長さ分のハイフンで構成された文字列を設定する事だ。その部分が''.join(['-' for i in theWord]となる。

>>> print(initialize())
<the Word: airdrop, Word: -------, count: 1, Guessed: [], Word #: 1, Current: 0, Overall: 0, msg: >
>>> print(initialize())
<the Word: trustingly, Word: ----------, count: 1, Guessed: [], Word #: 1, Current: 0, Overall: 0, msg: >
>>> print(initialize())
<the Word: astraddle, Word: ---------, count: 1, Guessed: [], Word #: 1, Current: 0, Overall: 0, msg: >

いずれにせよ、これでゲームの初期状態での環境を設定出来る。
あとは、大域変数として、「何回間違っても良いのか」を設定しておこう。

# カウンター上限値
count_max = 10

ここでは「絞首刑が行われる」までの失敗最大回数を10としよう。

これで基本的な材料は揃った。そして今回は「読み込み部」はPython組み込みのinputを使う事としてわざわざ作らない。
よって評価部であるinterp関数から書いていこう。

もう一度確認するが、評価部は読み込み部から手渡された文字情報を1つ目の引数とし、環境を2つ目の引数とする。そして返り値も環境とする。
手渡された文字情報は当然アルファベットじゃないとならない。数値とか渡された場合には入力エラーだとユーザーに通知し、また入力を促さないとならない。
これらを含め、取り敢えず条件を箇条書きにしてみよう。

  • ゲーム終了条件を満たした場合
  • 入力がアルファベットの場合
  • それ以外
大枠の条件は上のようになるだろう。もうちょっと細かくしてみようか。
  • ゲーム終了条件を満たした場合
    • 入力が"y"なら新しくゲームを始める
    • 入力が"n"ならゲームを終了する
    • それ以外なら"Please type 'y' or 'n'"と表示して入力を促す
  • 入力がアルファベットの場合
    • 既にされた入力なら"Already guessed"と表示して新しい入力を促す
    • ゲームを進める計算処理をする
  • それ以外は不正な入力なので"Not a valid guess"と表示して新しい入力を促す
これを実装すれば評価部のプログラミングが終わるわけだ。
具体的にどう実装するか、ってのは後回しにするが、例えば「ゲームが終了する」と言う条件は、勝つにせよ負けるにせよ、環境のインスタンス変数、theWordWordが一致した時、あるいはcountが10になった時を指す。
また、「ある入力が既に成された入力なのか」調べるのは、入力で手渡された文字が環境のインスタンス変数Guessedに含まれてるかどうか調べれば済む。
さて、評価部の実装をする前にメッセージを作っておこう。メッセージの「内容」はプログラム本体と分けておいて参照先として設定する。これをメッセージ分離方式と言う。
何故メッセージを分離しておくのか、と言うと、単純に、メッセージを本体に埋め込んでおくと修正するのが面倒くさいからだ。
例えば、ここではメッセージは全部英文にしてるが、仮に「日本語にしたい」となった場合、本体内にメッセージがあっちこっちにあると、探し回って修正するのは手間だろう。出来れば一箇所にあった方がいい。
また、今回もやってないが、最近のワールドワイドで使われるプログラムだと言語によってメッセージをファイルに分けておいて、それを読み出して各言語に合わせたメッセージを表示する事が多い。そういう「仕組み」が普遍化している以上、本体にメッセージを「埋め込む」と言うのはキレイなプログラムにならないから、だ。
今回もメッセージは、Pythonの辞書型を使って次のように設定しておく。

# メッセージ
msg = {'': '',
   'a': '\nAlready guessed \'{}\' ',
   'n': '\nNot a valid guess: \'{}\' ',
   'p': '\nPlease type \'y\' or \'n\' ',
   's': '\nSorry, the word was \"{}\"\nAnother word? ',
   'y': '\nYou got it!Another word? '}

後に印字部で使うメッセージも合わせて設定しておく。キー's'とキー'y'に結び付けられたメッセージは印字部で使うモノだ。
さて、評価部を書こう。評価部は次のようになる。

# Eval
def interp(x, env):
 x = x.lower()[0]
 if env.count == count_max or env.theWord == env.Word:
  if x == 'y':
   theWord = choice(dictionary)
   return Env(theWord, ''.join(['-' for i in theWord]),
        1, [], env.Word_No + 1,
        env.Current, env.Current + env.Overall, msg[''])
  elif x == 'n':
   sys.exit()
  else:
   return Env(env.theWord, env.Word, env.count, env.Guessed,\
        env.Word_No, env.Current, env.Overall, msg['p'])
 elif x.isalpha():
  if x in env.Guessed:
   return Env(env.theWord, env.Word, env.count, env.Guessed,\
        env.Word_No, env.Current, env.Overall, msg['a'].format(x))
  else:
   return Env(env.theWord,
        ''.join([x if x == i[0] else i[1] for i in\
           zip(env.theWord, env.Word)]) if x\
        in env.theWord else env.Word,
        env.count if x in env.theWord\
        else env.count + 1,
        sorted([x] + env.Guessed),
        env.Word_No,
        env.Current if x in env.theWord\
        else env.Current + 1,
        env.Overall, msg[''])
 else:
  return Env(env.theWord, env.Word, env.count, env.Guessed,\
       env.Word_No, env.Current, env.Overall, msg['n'].format(x))

まず、入力は何が手渡されるか分からないが、lowerメソッドを用いて小文字に変換しようとする。仮にアルファベット以外が手渡されてもlowerメソッドはガン無視してくれるからラクだ。
また、入力のinputは文字列を渡してくるので、その先頭だけを受け付けるようにしておく。

ゲームが終了条件を満たしてた場合、入力がyだったら新しくゲームを始める。
ここのロジックは基本的に初期化関数initializeと同じだ。新しく英単語を一つdictionaryから選んできてtheWordとして設定、その長さを用いてハイフンで文字列を作る。またcountを1に戻し、Guessedも空リストに戻す。
終了したゲームがn回目のプレイなら次はn + 1回目のプレイになるので、Word_Noを+1にする。OverallCurrentを退避させる。具体的にはOverall = Overall + Currentにする。msgは大域変数msgからキーが""のブツを引っ張ってくる。
入力がnだったらゲームを終了する。sysモジュールexitを使ってゲームを終了しよう。
それ以外の場合は、基本的には何もしないんで、現時点での環境を組み立て直して返す。ただし、エラーメッセージを表示したいんで、インスタンス変数msgに大域変数msgからキー"p"の値を引っ張ってくる。

入力情報xがアルファベットかどうか調べるにはisalphaを用いる。
入力情報が既に使われてる(x in env.Guessed)場合、基本的には何もしないが、エラーメッセージとしてインスタンス変数msgに大域変数msgからキー"a"の値を引っ張ってくる(msg['a']はフォーマット文字列なんで入力xをformatとして渡す)。
そうじゃない場合、ゲームに関する処理を行う。大まかには次のようにすればいい事が分かるだろう。

  • 入力情報xがインスタンス変数theWordに含まれてない場合countを1増やす。そうじゃない場合はcountのまま。
  • 入力情報xをインスタンス変数Guessedに追加する。abc...順にしたいのでsortedする。
  • 入力情報xがインスタンス変数theWordに含まれてない場合Currentを1増やす。そうじゃない場合はCurrentのまま。
その他はインスタンス変数Word以外は弄らなくて良い、ってのは分かるだろう。そしてWordの計算がちとややこしい筈だ。

''.join([x if x == i[0] else i[1] for i in zip(env.theWord, env.Word)]) if x                        in env.theWord else env.Word

例えばインスタンス変数theWordが"cab"でWordが"---"だったとしよう。入力情報xがtheWordに含まれてなければWordはそのままでいい。
含まれてた場合は、だ。まずはzipで次のようなイテレータを生成する。

>>> list(zip("cab", "---"))
[('c', '-'), ('a', '-'), ('b', '-')]

仮に入力情報が'a'だった場合、リスト内包表記で次のように処理出来る。

>>> ['a' if 'a' == i[0] else i[1] for i in zip("cab", "---")]
['-', 'a', '-']

joinを用いて生成したリストを文字列に直す。

>>> ''.join(['a' if 'a' == i[0] else i[1] for i in zip("cab", "---")])
'-a-'

こうしておけば正解する度にハイフンが解除されて文字が正しい位置に現れるように見える。

あとは入力した文字がアルファベット以外だった場合だが、これも基本的には何もせず、環境を組み立て直してインスタンス変数msgmsg['n']にしておけばいいだけ、だ(ここもフォーマット文字列なんでxを渡しておく)。
これで評価部の実装は終わる。

次に印字部を作るが、絞首刑とちょいとした情報を表示させたいんで、次のような文字列テンプレートを作る。



右側のデータは何となく分かるだろうが左側はちと意味が分かりづらいだろう。
これは次のような図を元にしている。


首吊りした像の完成形の部分部分を{}、つまりプレースホルダーで置き換えている。
プレースホルダーはformatの引数の順番に従って番号で指定した文字を埋め込んでいく。あるいは、formatの引数に名前を付けておけばその名前に従った文字を埋める。
ハングマンは回答を間違える度に徐々に完成に向かっていくわけだが、そのギミックを作成する為にハングマンの「部品」を次のように定義する。

# ハングマン表示部品
parts_table = {1: ('|', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
      2: ('|', '______', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
      3: ('|', '______', '|', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
      4: ('|', '______', '|', '0', ' ', ' ', ' ', ' ', ' ', ' '),
      5: ('|', '______', '|', '0', '|', ' ', ' ', ' ', ' ', ' '),
      6: ('|', '______', '|', '0', '|', '|', ' ', ' ', ' ', ' '),
      7: ('|', '______', '|', '0', '|', '|', '/', ' ', ' ', ' '),
      8: ('|', '______', '|', '0', '|', '|', '/', '/', ' ', ' '),
      9: ('|', '______', '|', '0', '|', '|', '/', '/', '\\', ' '),
      10: ('|', '______', '|', '0', '|', '|', '/', '/', '\\', '\\')}

ここでも辞書が活躍するが、キーの数値は環境のインスタンス変数countと連動してる。と言うより連動するように書く。

# Print
def display(env):
 os.system('cls' if os.name == 'nt' else 'clear')
 print(temp.format(''.join(env.Guessed), *parts_table[env.count],\
         Word_No = env.Word_No,
         Current = env.Current / env.Word_No,
         Overall = env.Overall if env.Word_No == 1 else\
         env.Overall / (env.Word_No - 1) , Word = env.Word,
         msg = (msg['s'].format(env.theWord)\
         if env.count == count_max\
         else msg['y'] if env.theWord == env.Word\
           else "") + env.msg),
   end = '')
 return env

まずは端末表示をキレイにする必要があるが、Windowsでのコマンドはcls、UNIX系OSでのコマンドはclearとなる。要は、ユーザーが使ってるOSによって切り替えないとならないが、それはos.nameで判別可能だ。'nt'とはWindowsの事だ(※9)。
format内でキーワードとして名前を付けたモノはその名前に従ってテンプレート内のプレースホルダーへとはめ込まれていく。その辺はまぁ問題はないだろう。
Currentはインスタンス変数Currentをゲームの挑戦回数で割ったモノとし、Overallはそれの通算版だ。割る回数が0の時(つまり初回)だけ気をつければいい。
msgはゲームで勝った場合、負けた場合、経過中の場合、と適したメッセージを大域変数msgから引っ張ってくる。
その辺はまあいいだろう。問題はparts_tableから引っ張ってきたタプルだ。素の引数として扱う為にタプルのカッコを外さないとならない。アスタリスク(*)はそのカッコを外す効果がある(アンパックと呼ぶ)。



環境のインスタンス変数countに従って画像が完成するのが分かるだろう。
これでほぼ完成、だ。あとはRead、Eval、Printを組み合わせてループさせるだけ、だ。
CLI版ハングマンの完成コードはここに置いておこう。次回はこれを利用してGUI版を作っていく。

※1: コマンド・ライン・インターフェース。何度も書くが、CUI(キャラクター・ユーザー・インターフェース)なんつー英語は存在しない。正しくはCLIだ。

※2: 実際問題、GUIの構成自体にオリジナリティは全くない。というよりオリジナリティなんざ邪魔だ。大体、凝った「独自の」GUIなんか作られても使いづらくてしゃーない。定形に則ったモノじゃないと、今までの「経験」で直感的に使えなくなる。とすれば、「オリジナリティ溢れるGUI」なんつーのはクソでしかない。
余談だが、例えばウィンドウのクローズボックスなんかはMacは左上部にあるが、Windowsは右上部にある。これは、右利きの人間からいうとWindowsが正しい。Macは左上部にあるため、マウスポインタでの移動距離が長く直感的じゃなくなる。この辺がWindows慣れしてると「Macが使いづらい」原因で、「独自性」なんつーのはストレスの元にしかならない、という好例だろう。

Macのクローズボックス(左)と典型的なWindows(やLinux)のクローズボックスの配置(右)。

なお、それからいうと、プルダウンメニューなんかも右側にあった方が使いやすい筈だが、それらが左にあるのは、欧米の「文」の書き方が左から始まる、と言う流れから「見やすさ」でそうなってると思われる。
言い換えると、アラブ人とかなら、右にメニューがあった方が実は使いやすいし読みやすいんじゃないか(アラブ系の文字の書き方だと昔の日本と同じで、横書きだと右から左に文字を書いていく為)。

※3: 歴史的には、いわゆる「アカデミックな」教育を受けたプログラマの代表格がUNIX系プログラマだったわけだが、90年代半ばになるまで、彼らは「一般人が知ってる」表舞台に出てくる事は稀だった。一般人が使うPC(MS-DOSとかMacとか)でプログラミングをせず、「どっか知らない場所で良く分からんモノを」プログラミングしてたらしい(笑)。
ところが状況が一変したのが90年代のインターネットの普及で、それによって、極端な事を言うと、「ワープロも表計算も作らず文字列処理ばっかしてる良く分からんUNIX系プログラミング」が表舞台に立つ。そう、インターネットでは「文字列処理」が中心で、それはMS-DOS -> Windowsでの「クライアント向けのGUIプログラミング」とは傾向が違うモノだったんだ。
ちなみに、どうも色々と見る限り、性質的にもWindowsプログラマとUNIXプログラマは違う。Windowsプログラマの方がドキュメントを書くのが上手く、また物静かな人が多い印象。騒がない。
一方、UNIX系プログラマが、大体、Web上での「炎上」が大好きな奴らで(つまり「意識高い系」で「声が大きい」)、そしてドキュメントを書くのがヘタクソだ(笑)。
Windows系プログラマはだから、マニュアルなんかを書くのが上手い。個人的に作ったフリーソフトでもキレイなマニュアルを書く。これはやっぱクライアントに「売る」前提で、会社でキチンと訓練された人が多いんだろう。
一方、UNIX系プログラマはマニュアル書くのがヘタクソで、彼らが作ったソフトのマニュアル読んでも意味不明、ってのが多い(笑)。Webなら「いつでも執筆途中(つまり「完成しない」・笑)」で「バージョンアップ」が可能だからな(笑)。
なお、ポール・グレアムなんかはUNIX系プログラマが文章を書くのが上手い、って持ち上げてたが、上に書いた通り、個人的には印象が真逆だ。Windows系プログラマの方が、色々と格上だと思う(笑)。実際、過去、Linux系のとあるソフトの使い方を検索してたが、Windows版があったので、Windowsプログラマがそのソフトに付いて書いてたヤツの方が「UNIX系プログラマが書いた散文的な(笑)」ブツより良くまとまってて、俄然読みやすくて分かりやすかった。と言う経験を経て、「UNIX系プログラマが書いたドキュメンテーションとは言えないドキュメンテーションは基本クソ」と言う信条になった。
Windows文化を舐めちゃいけないんだよ。

※4: ここではフツーの、パソコンの事。

※5: ここに挙げた名称は必ずしも同レベルに存在しない。ただし、それぞれのOSでグラフィック関係をプログラミングする際に「よく出てくる」一番最低レベルのGUIを構成するグラフィカルツールの名前だとは思う。

※6: MS-DOSでもパイプコマンドは"|"、PowerShellでもパイプコマンドは"|"だ。試してみてほしい。

※7: クラスとは本来何なのか、に付いてはここを参照。

※8: ここでの「ハングマン」は、BSD UNIXに含まれていたハングマンをモデルにしている。

BSD UNIX版ハングマン。Debian/Ubuntu等ではBSDGamesパッケージとして提供されてるゲームのうちの一つになる。

余力があったら是非ともソースコードを読んで欲しい。同じような事をしてもPythonの方がソースコードは圧倒的に短く、それが意味する事はC言語は「速い」けど、結果「短く物事を成し遂げられる」Pythonの方がパワフルだ、と言う意味になる。

※9: ntはWindows NTから来ている。現在のWindowsは全てWindows NTの後継。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事