見出し画像

京大のPython教科書で勉強してみた(演習編)

京大のPython教科書で勉強してみた」の演習課題で実際に書いたソースコードを一部こちらに載せておきます(教科書のソースコードを一部いじるやつ以外を抜粋)。
基本的に教科書に載っているソースコードを真似て書いているので、たぶんイマイチな書き方とかあると思います。「もっといい書き方あるよ!」とかあったら教えてください。

8章 Turtleで遊ぶ

演習 正n角形を作る

正n角形を作る課題。
正確には正5角形と正7角形と正9角形を作る課題だったけど、定数に指定するとその図形を描いてくれるようにします。
そもそも「正多角形の1つの内角」の求め方がわからなかったので、ググるところから(昔算数とか数学とかでやったのかもしれないけど忘れた)。
180°(n-2)/ n だそうです。

こちらを踏まえて書いたプログラムがこちら。

from turtle import *
n = 5   # ここに描画したい正n角形のnを指定
kakudo = 180 - (180 * (n - 2) / n))
for i in range(n):
    forward(100)
    right(kakudo)
done()

これ、right()には外角を指定しないといけないから、角度を求める時に内角を求めて180から引いたんだけど、よく考えると

  • 最初から外角を計算する(外角の和は360°なので、360/nでいける)

  • rightではなくleftを使って内角を指定する

のどっちかでやった方がシンプルなので、そのように修正してみてどっちも期待通り動くことを確認。

演習 正n角形の星形を作る

正n角形の星形を作る課題。正5角形の星形なら☆になるのが期待値。
こっちも正確には正5角形、正7角形、正9角形の星形を作る課題だったけど、これも定数に指定できるようにしました。
で、例によって「正多角形の星形の内角」の求め方がわからなかったけど、よく考えると星形のツノの部分って二等辺三角形で、底辺の2つの角は正n角形の外角なので、そこから導き出すと 180 - (360 / n) * 2

それを踏まえて書いたプログラムがこちら(式がこれであってるかは不明)。

from turtle import *
n = 5  # ここに描画したい正n角形のnを指定
kakudo = 180 - (180 - (360 / n) * 2)
for i in range(n):
    forward(100)
    right(kakudo)
done()

星形を書く場合はright()にしてもleft()にしても外角を指定する必要があったので、内角求めた後に180°から引いてます。
こちらも期待通り動くことを確認。

課題 turtleの作品作成①

最後にturtleを使った作品を何か作れという課題があったので(本当に何作ってもいいらしくて特にテーマとかの指定はなかった)、私も適当に作ってみました。

それがこちら。とてもバカっぽいものができました。

ソースはこんな(人に見せるようなソースではないけど、ソースのバカっぽさも見て欲しいのであえて公開するスタイル)。ごりっごりのごり押しです。

from turtle import *

def draw(x, y):
    #左耳
    begin_fill()
    for i in range(30):
        forward(10)
        left(10)
    (x, y) = pos();
    penup()

    #右耳
    goto(x - 84, y)
    pendown()
    setheading(75)
    for i in range(30):
        forward(10)
        left(10)
    (x, y) = pos();
    penup()

    #顔
    goto(x + 131, y - 147)
    pendown()
    setheading(30)
    for i in range(38):
        forward(18)
        left(10)
    end_fill()

setup(600, 400)
onscreenclick(draw)

課題 turtleの作品作成②

さらにバカっぽさをパワーアップさせたのがこちら。
クリックした場所に小さいのを描画するようにしました。

ソースはこんな。さっきのよりはマシになってます。

from turtle import *

def draw(clx, cly):
    # 描画用情報
    infolist = ({"t":Turtle(), "forward":2, "left":10, "startx":0, "starty":0},
                {"t":Turtle(), "forward":2, "left":10, "startx":-40, "starty":0},
                {"t":Turtle(), "forward":3.5, "left":10, "startx":-21, "starty":-30})

    # turtle設定初期化
    for info in infolist:
        info["t"].hideturtle()
        info["t"].speed(0)
    
    # 描画開始位置へ移動
    for info in infolist:
        info["t"].penup()
        info["t"].goto(clx + info["startx"], cly + info["starty"])
        info["t"].pendown()
        info["t"].begin_fill()

    # 円を3つ描く
    for i in range(38):
        for info in infolist:
            info["t"].forward(info["forward"])
            info["t"].left(info["left"])

    # 塗りつぶす
    for info in infolist:
        info["t"].end_fill()

setup(800, 600)
onscreenclick(draw)

ちなみにこれ、描画中に別の場所をクリックすると、中断して新しいやつの描画を開始してしまうので、できれば描画を継続しつつ新しいのも描画できるようにしたい。ちょっとやってみてできなかったんだけど、そのうち暇があったらまた試してみようかと。できるかどうかはわからないけど。

11章 クラス

演習 複数のオブジェクトの生成と利用

Dentakuクラスのオブジェクトを複数利用して何か作る課題。
全く思いつかなかったので、例として挙げられていた検算用のオブジェクトを作って検算してみることにしました。
それがこちら。

class Dentaku():
    def __init__(self):
        self.first_term = 0
        self.second_term = 0
        self.result = 0
        self.operation = "+"

    def do_operation(self):
        if self.operation == "+":
            self.result = self.first_term + self.second_term
        elif self.operation == "-":
            print(self.first_term, self.second_term)
            self.result = self.first_term - self.second_term
        elif self.operation == "*":
            self.result = self.first_term * self.second_term
        elif self.operation == "/":
            self.result = self.first_term / self.second_term

dentaku = Dentaku()
kenzan = Dentaku()
while True:
    f = int(input("First term "))
    dentaku.first_term = f
    o = input("Operation ")
    dentaku.operation = o
    s = int(input("Second term "))
    dentaku.second_term = s
    dentaku.do_operation()
    r = dentaku.result
    print("Result ", r)
    # 検算
    kenzan.first_term = r
    kenzan.second_term = s
    if o == "+":
        kenzan.operation = "-"
    elif o == "-":
        kenzan.operation = "+"
    elif o == "*":
        kenzan.operation = "/"
    elif o == "/":
        kenzan.operation = "*"
    kenzan.do_operation()
    k = kenzan.result
    if k == dentaku.first_term:
        print("Kenzan OK")
    else:
        print("Kenzan NG")

検算用のメソッドをDentakuクラスに入れるか悩んだけど、入れちゃうとなんか意味合いが違ってきそうな気がして入れなかったらなんかイマイチになった。

演習 tkinter で作成した電卓プログラムでの Dentaku クラス利用

9章で作った電卓の完全版みたいなやつです。
教科書に記載されていた電卓のソースに以下の対応をしています。

  • 演習9-2(体裁の調整)の対応

  • 演習9-3(四則演算への拡張)の対応

  • 演習9-4(ウィジェットのリスト管理)の対応

  • Dentakuクラスを使う

見た目はイマイチもいいとこですがこんな感じにしました。
=とCが変なとこにあるのでよく押し間違えます。

電卓

ソースはこんな。数字キーのループの無理やり感が否めない。

import tkinter as tk

class Dentaku():
    def __init__(self):
        self.current_number = 0
        self.first_term = 0
        self.second_term = 0
        self.result = 0
        self.operation = "+"

    def do_eq(self):
        self.second_term = self.current_number
        if self.operation == "+":
            self.result = self.first_term + self.second_term
        elif self.operation == "-":
            self.result = self.first_term - self.second_term
        elif self.operation == "*":
            self.result = self.first_term * self.second_term
        elif self.operation == "/":
            self.result = self.first_term / self.second_term
        self.current_number = 0
        self.operation = 0

    def key(self, n):
        self.current_number = self.current_number * 10 + n
        self.show_number(self.current_number)

    def calc(self, op):
        self.operation = op
        self.first_term = self.current_number
        self.current_number = 0

    def clear(self):
        self.current_number = 0
        self.show_number(self.current_number)

    def eq(self):
        self.do_eq()
        self.show_number(self.result)

    def show_number(self, num):
        e.delete(0, tk.END)
        e.insert(0, str(num))

dentaku = Dentaku()
root = tk.Tk()
f = tk.Frame(root)
f.grid()
f.configure(bg='#ffffc0')

bns = list()
for i in range(0, 10):
    bns.append(tk.Button(f, text=i, command=lambda x=i: dentaku.key(x)))
             
bc = tk.Button(f, text='C', command=dentaku.clear)
bp = tk.Button(f, text='+', command=lambda x="+": dentaku.calc(x))
bm = tk.Button(f, text='-', command=lambda x="-": dentaku.calc(x))
bt = tk.Button(f, text='x', command=lambda x="*": dentaku.calc(x))
bd = tk.Button(f, text='÷', command=lambda x="/": dentaku.calc(x))
be = tk.Button(f, text='=', command=dentaku.eq)

r = 4
c = 0
for i, bn in enumerate(bns):
    bn.grid(row=r, column=c)
    c += 1
    if c > 2 or i == 0:
        c = 0
        r -= 1
    
bc.grid(row=4, column=1)
be.grid(row=4, column=2)
bp.grid(row=1, column=3)
bm.grid(row=2, column=3)
bt.grid(row=3, column=3)
bd.grid(row=4, column=3)

for bn in bns:
    bn.configure(bg='#ffffff', width=2, font=('Helvetica', 14))

bc.configure(bg='#ff0000', width=2, font=('Helvetica', 14))
be.configure(bg='#00ff00', width=2, font=('Helvetica', 14))
bp.configure(bg='#00ff00', width=2, font=('Helvetica', 14))
bm.configure(bg='#00ff00', width=2, font=('Helvetica', 14))
bt.configure(bg='#00ff00', width=2, font=('Helvetica', 14))
bd.configure(bg='#00ff00', width=2, font=('Helvetica', 14))

e = tk.Entry(f)
e.grid(row=0, column=0,columnspan=4)
e.configure(font=('Helvetica', 14))
dentaku.clear()

root.mainloop()

13章 三目並べで学ぶプログラム開発

演習 棋譜の採取

ほぼ写経だけど、ログへの記録のみ自分で書けってことだったので書いて完成させてみました。あと、そのまま書くとエラーになったりイマイチだったりしたところをちょっとだけ修正したりアレンジしたりしています。
ソースは省略するけど、動かすとこんな感じになります。たぶんこれで合ってるはず。

三目並べ
>>> play()
 :0 1 2
---------
0:       
1:       
2:       

先手 の番です
行を入力してください: 1
列を入力してください: 1
OK
 :0 1 2
---------
0:       
1:   o   
2:       

後手 の番です
行を入力してください: 0
列を入力してください: 0
OK
 :0 1 2
---------
0: x     
1:   o   
2:       

先手 の番です
行を入力してください: 1
列を入力してください: 0
OK
 :0 1 2
---------
0: x     
1: o o   
2:       

後手 の番です
行を入力してください: 0
列を入力してください: 1
OK
 :0 1 2
---------
0: x x   
1: o o   
2:       

先手 の番です
行を入力してください: 1
列を入力してください: 2
OK
 :0 1 2
---------
0: x x   
1: o o o 
2:       

先手 の勝ちです
 :0 1 2
---------
0:       
1:       
2:       

先手 の番です
OK
 :0 1 2
---------
0:       
1:   o   
2:       

IS WIN 先手 :  False
後手 の番です
OK
 :0 1 2
---------
0: x     
1:   o   
2:       

IS WIN 後手 :  False
先手 の番です
OK
 :0 1 2
---------
0: x     
1: o o   
2:       

IS WIN 先手 :  False
後手 の番です
OK
 :0 1 2
---------
0: x x   
1: o o   
2:       

IS WIN 後手 :  False
先手 の番です
OK
 :0 1 2
---------
0: x x   
1: o o o 
2:       

IS WIN 先手 :  True
RESULT IN LOG:  1
IS WIN FIRST:  True
IS WIN SECOND:  False
IS DRAW:  False

力試し

写経したソースを元に、以下にチャレンジします。

  • 盤面のGUI化

  • クラス化

  • 対戦相手のNPC化

  • オセロ化

このうちオセロ化はちょっと後回しにして、他の3つの改修をしてみました。
ソースは割愛しますが、以下のように実装しています。

・盤面のGUI化
3×3にボタンを配置。
下に自分が先手と後手のどっちなのか、勝敗結果を記載するメッセージボックスを追加(先手と後手は最初にランダムに決める感じにしました)。

・クラス化
手番関連のクラス、棋譜関連のクラス、盤面関連のクラスに分けました。

・対戦相手のNPC化
ロジックは以下のような感じです。

  1. 自分のリーチがかかっている場所(勝てる場所)があればそこに置く。リーチかかっている場所が複数あればランダムでいずれかに置く。

  2. 相手のリーチがかかっている場所(負ける場所)があればそこに置く。リーチかかっている場所が複数あればランダムでいずれかに置く(この場合は負け確)。

  3. 2つ並べられそうな場所かつ、並び上に相手の駒がない場所があればそこに置く。そのような場所が複数あればランダムでいずれかに置く

  4. 上記いずれにも該当しない場合(初回とか)はランダムで置く。

割といい感じにできた気がするので、プレイ動画のっけときます。

勝ちパターン
負けパターン
引き分けパターン