【学習記録】PyhtonのTkinterで不思議なダンジョンゲーム作成

<p>
 pythonで動くものを作りたい！と思い、『<a href='http://www.sotechsha.jp/pc/html/1239.htm'>Pythonでつくるゲーム開発入門講座</a>』(<a href='http://www.sotechsha.co.jp/pc/html/1256.htm'>実践編</a>含む)で勉強中です。
</p>

<p>
 なぜpythonのtkinterなのか→私の最終目的はゲーム開発ではなく、スクレイピングなどのデータ収集や、データ分析、業務効率化など。よって、ゲーム開発に適したC++やUnity？でなく、pythonでゲーム作成。pygameライブラリを使わないのも同じ理由。
</p>

<p>
 スクレイピングやデータ分析、機械学習をピンポイントで勉強しても、今のところ応用方法が思いつかないので、遠回りになるけれど、ゲームを作成しながら、プログラミングを覚えようという魂胆。NCPを動かすのに、機械学習とかディープラーニングを組み込んでそのあたりの実践経験も積めたらなぁとか欲張ってます。
</p>

<p>
 ある程度学習出来たので、サンプルコードを参考（というか改造）にして、不思議なダンジョンゲームっぽいものを作ってみました。<br> 風来の試練やトルネコを思い浮かべる方が多いと思いますが、私は不思議なダンジョンと言えば、チョコボの不思議なダンジョンを思い浮かべます。（あとはテリーのワンダーランド？）最終的にはチョコボに近い'何か'を作りたいでうすね。<br> とりあえず、コードとキャラチップを<a href='https://github.com/T-Sakata125F/rpg_game
'>github</a>に保存してみました。<br>
キャラチップは<a href='https://pipoya.net/sozai/'>ぴぽや倉庫</a>さんより拝借いたしました。
マップを自動生成する、maze_maker.pyとゲーム本体のtkmaze.pyに分けてみました。
</p>



```python3:maze_maker.py

import random

class maze_maker:
    """
    ダンジョンを自動生成する
    """

    def __init__(self,MAZE_W,MAZE_H):
        self.MAZE_W = MAZE_W
        self.MAZE_H = MAZE_H
        self.maze = [[0]*self.MAZE_W for y in range(self.MAZE_H)]
        self.DUNGEON_W = MAZE_W*3
        self.DUNGEON_H = MAZE_H*3
        self.dungeon = [[0]*self.DUNGEON_W for y in range(self.DUNGEON_H)]

    def make_maze(self):
        """
        迷路を作る
        """
        XP = [ 0, 1, 0,-1]
        YP = [-1, 0, 1, 0]

        #周囲の柱
        for x in range(self.MAZE_W):
            self.maze[0][x] = 1
            self.maze[self.MAZE_H-1][x] = 1
        for y in range(self.MAZE_H):
            self.maze[y][0] = 1
            self.maze[y][self.MAZE_W-1] = 1

        #中を空っぽに
        for y in range(1,self.MAZE_H-1):
            for x in range(1,self.MAZE_W-1):
                self.maze[y][x] = 0

        #柱
        for y in range(2,self.MAZE_H-2,2):
            for x in range(2,self.MAZE_W-2,2):
                self.maze[y][x] = 1

        for y in range(2,self.MAZE_H-2,2):
            for x in range(2,self.MAZE_W-2,2):
                d = random.randint(0,3)
                if x > 2:
                    d = random.randint(0,2)
                self.maze[y+YP[d]][x+XP[d]] = 1

    def make_dungeon(self):
        """
        迷路からダンジョンを作る
        """
        self.make_maze()
        for y in range(self.DUNGEON_H):
            for x in range(self.DUNGEON_W):
                self.dungeon[y][x] = 9
        for y in range(1,self.MAZE_H-1):
            for x in range(1,self.MAZE_W-1):
                dx = x*3+1
                dy = y*3+1
                if self.maze[y][x] == 0:
                    if random.randint(0,99) < 20:
                        for ry in range(-1,2):
                            for rx in range(-1,2):
                                self.dungeon[dy+ry][dx+rx] = 0
                    else:
                        self.dungeon[dy][dx] = 0
                        if self.maze[y-1][x] == 0:
                            self.dungeon[dy-1][dx] = 0
                        if self.maze[y+1][x] == 0:
                            self.dungeon[dy+1][dx] = 0
                        if self.maze[y][x-1] == 0:
                            self.dungeon[dy][dx-1] = 0
                        if self.maze[y][x+1] == 0:
                            self.dungeon[dy][dx+1] = 0


    def put_event(self):
        while True:
            x = random.randint(3,self.DUNGEON_W-4)
            y = random.randint(3,self.DUNGEON_H-4)
            if(self.dungeon[y][x] == 0):
                for ry in range(-1,2):
                    for rx in range(-1,2):
                        self.dungeon[y+ry][x+rx] = 0
                self.dungeon[y][x] = 1
                break
        for i in range(60):
            x = random.randint(3,self.DUNGEON_W-4)
            y = random.randint(3,self.DUNGEON_H-4)
            if(self.dungeon[y][x] == 0):
                self.dungeon[y][x] = random.choice([2,3,3,3,4])


```



```python3:tkmaze.py
"""
冥土で冥土探索。それは終わりなき旅。ゴールは無くて、敵を避けつつひたすら階段を降りていく。
1歩ごとにスコアカウント。階段を降りるとプラス。ハイスコアを目指そう！
"""
import tkinter
import maze_maker
from PIL import Image,ImageTk
import random

#キー入力
key = ''
koff = False
def key_down(e):
    global key,koff
    key = e.keysym
    koff = False

def key_up(e):
    global koff
    koff = True

CHIP_SIZE = 32
DIR_UP = 3
DIR_DOWN = 0
DIR_LEFT = 1
DIR_RIGHT = 2
chara_x = chara_y = 146
chara_d = chara_a = 0
obj_a = 0
emy_num = 4
emy_list_x = [0]*emy_num
emy_list_y = [0]*emy_num
emy_list_d = [0]*emy_num
emy_list_a = [0]*emy_num
ANIMATION = [1,0,1,2]
#素材は「ぴぽや http://blog.pipoya.net/」様より
imgplayer_pass = 'image/charachip01.png'
emy_img_pass = 'image/pipo-charachip019.png'
emy2_img_pass = 'image/hone.png'
emy3_img_pass = 'image/majo.png'
emy3_kageimg_pass = 'image/majo_kage.png'
takara_img_pass = 'image/pipoya_mcset1_obj01.png'
obj_pass = 'image/pipoya_mcset1_obj02.png'
obj2_pass = 'image/pipo-hikarimono005.png'
yuka_pass = 'image/pipoya_mcset1_at_gravel1.png'
kebe_pass = 'image/pipoya_mcset1_bridge01.png'

map_data = maze_maker.maze_maker(11,7)
map_data.make_dungeon()
map_data.put_event()

tmr = 0
idx = 1
floor_count = 0
item_count = 0
pl_life=150
pl_stamina = 150
pl_damage = 0

def move_player():
    global chara_x,chara_y,chara_a,chara_d,pl_stamina,pl_life
    if key == 'Up':
        chara_d = DIR_UP
        check_wall()
    if key == 'Down':
        chara_d = DIR_DOWN
        check_wall()
    if key == 'Left':
        chara_d = DIR_LEFT
        check_wall()
    if key == 'Right':
        chara_d = DIR_RIGHT
        check_wall()
    check_event()
    if tmr%4 == 0:
        if pl_stamina > 0:
            pl_stamina -= 1
        else:
            pl_life -= 1
            if pl_life <= 0:
                pl_life = 0
                idx = 2
    chara_a = chara_d*3 + ANIMATION[tmr%4]


def emy_set(emy_num):
    while True:
        x = random.randint(3,map_data.DUNGEON_W-4)
        y = random.randint(3,map_data.DUNGEON_H-4)
        if map_data.dungeon[y][x] == 0:
            emy_list_x[emy_num] = x*CHIP_SIZE+(CHIP_SIZE//2)
            emy_list_y[emy_num] = y*CHIP_SIZE+(CHIP_SIZE//2)
            break


def draw_text(txt):
    st_fnt = ('Times New Roman',60)
    canvas.create_text(200,200,text=txt,font=st_fnt,fill='red',tag='SCREEN')


def damage_cal(pl_damage):
    global pl_life,idx
    if pl_life <= pl_damage:
        pl_life = 0
        idx = 2
    else:
        pl_life -= pl_damage


def move_emy(emy_num):
    cy = int(emy_list_y[emy_num]//CHIP_SIZE)
    cx = int(emy_list_x[emy_num]//CHIP_SIZE)
    emy_list_d[emy_num] = random.randint(0,3)
    if emy_list_d[emy_num] == DIR_UP:
        if map_data.dungeon[cy-1][cx] != 9:
            emy_list_y[emy_num] -= 32
    if emy_list_d[emy_num] == DIR_DOWN:
        if map_data.dungeon[cy+1][cx] != 9:
            emy_list_y[emy_num] += 32
    if emy_list_d[emy_num] == DIR_LEFT:
        if map_data.dungeon[cy][cx-1] != 9:
            emy_list_x[emy_num] -= 32
    if emy_list_d[emy_num] == DIR_RIGHT:
        if map_data.dungeon[cy][cx+1] != 9:
            emy_list_x[emy_num] += 32
    emy_list_a[emy_num] = emy_list_d[emy_num]*3 + ANIMATION[tmr%4]
    if abs(emy_list_x[emy_num]-chara_x) <= 30 and abs(emy_list_y[emy_num]-chara_y) <= 30:
        pl_damage = 10*random.choice([1,2,2,3,3])
        damage_cal(pl_damage)


def obj_animation():
    global obj_a
    obj_a = ANIMATION[tmr%4]


def check_wall():
    global chara_x,chara_y,chara_a,chara_d
    cy = int(chara_y//CHIP_SIZE)
    cx = int(chara_x//CHIP_SIZE)
    if chara_d == DIR_UP:
        if map_data.dungeon[cy-1][cx] != 9:
            chara_y -= 32
    if chara_d == DIR_DOWN:
        if map_data.dungeon[cy+1][cx] != 9:
            chara_y += 32
    if chara_d == DIR_LEFT:
        if map_data.dungeon[cy][cx-1] != 9:
            chara_x -= 32
    if chara_d == DIR_RIGHT:
        if map_data.dungeon[cy][cx+1] != 9:
            chara_x += 32

def check_event():
    global chara_x,chara_y,chara_a,chara_d,idx,pl_damage
    global floor_count,emy_count,item_count,pl_life,pl_stamina
    cy = int(chara_y//CHIP_SIZE)
    cx = int(chara_x//CHIP_SIZE)
    if map_data.dungeon[cy][cx] == 1:
        #ワープの魔法陣にのった
        map_data.make_dungeon()
        map_data.put_event()
        emy_set(0)
        emy_set(1)
        emy_set(2)
        floor_count += 1
        chara_x = chara_y = 146
    if map_data.dungeon[cy][cx] == 2:
        #トラップの魔法陣に乗った
        if item_count > 0:
            item_count -= 1
            map_data.dungeon[cy][cx] = 0
        else:
            pl_damage = 5*random.choice([1,2,2,3,4,3])
            damage_cal(pl_damage)
            map_data.dungeon[cy][cx] = 0
    if map_data.dungeon[cy][cx] == 3:
        #アイテムに接触
        item_count += 1
        map_data.dungeon[cy][cx] = 0
    if map_data.dungeon[cy][cx] == 4:
        #食料に接触
        pl_stamina_recover = 5*random.choice([1,2,2,3,4,3])
        if pl_stamina + pl_stamina_recover > 150:
            pl_stamina = 150
        else:
            pl_stamina += pl_stamina_recover
        map_data.dungeon[cy][cx] = 0


def split_chip(chip_pass,chip_img_x,chip_img_y):
    '''
    複数のチップを１単位のチップに分割する
    '''
    chip_list = []
    for cy in range(0,chip_img_y,CHIP_SIZE):
        for cx in range(0,chip_img_x,CHIP_SIZE):
            chip_list.append(ImageTk.PhotoImage(Image.open(chip_pass).crop((cx,cy,cx+CHIP_SIZE,cy+CHIP_SIZE))))
    return chip_list


def draw_screen():
    st_fnt = ('Times New Roman',30)
    canvas.delete('SCREEN')
    for my in range(len(map_data.dungeon)):
        for mx in range(len(map_data.dungeon[0])):
            if map_data.dungeon[my][mx] != 9:
                canvas.create_image(mx*CHIP_SIZE+(CHIP_SIZE//2),my*CHIP_SIZE+(CHIP_SIZE//2),image=yuka_img[8],tag='SCREEN')
            if map_data.dungeon[my][mx] == 1:
                canvas.create_image(mx*CHIP_SIZE+(CHIP_SIZE//2),my*CHIP_SIZE+(CHIP_SIZE//2),image=obj2_img[obj_a],tag='SCREEN')
            if map_data.dungeon[my][mx] == 2:
                canvas.create_image(mx*CHIP_SIZE+(CHIP_SIZE//2),my*CHIP_SIZE+(CHIP_SIZE//2),image=obj2_img[6+obj_a],tag='SCREEN')
            if map_data.dungeon[my][mx] == 3:
                canvas.create_image(mx*CHIP_SIZE+(CHIP_SIZE//2),my*CHIP_SIZE+(CHIP_SIZE//2),image=takara_img[5],tag='SCREEN')
            if map_data.dungeon[my][mx] == 4:
                canvas.create_image(mx*CHIP_SIZE+(CHIP_SIZE//2),my*CHIP_SIZE+(CHIP_SIZE//2),image=obj_img[25],tag='SCREEN')
            if map_data.dungeon[my][mx] == 9:
                canvas.create_image(mx*CHIP_SIZE+(CHIP_SIZE//2),my*CHIP_SIZE+(CHIP_SIZE//2),image=kabe_img[28],tag='SCREEN')
    canvas.create_image(chara_x,chara_y,image=imgplayer[chara_a],tag='SCREEN')
    canvas.create_image(emy_list_x[0],emy_list_y[0],image=emy_img[emy_list_a[0]],tag='SCREEN')
    canvas.create_image(emy_list_x[1],emy_list_y[1],image=emy2_img[emy_list_a[1]],tag='SCREEN')
    canvas.create_image(emy_list_x[2],emy_list_y[2],image=emy3_img[emy_list_a[2]],tag='SCREEN')
    canvas.create_image(emy_list_x[2],emy_list_y[2],image=emy3_kageimg[emy_list_a[2]],tag='SCREEN')
    canvas.create_text(1110,50,text='{} 階'.format(floor_count),font=st_fnt,fill='black',tag='SCREEN')
    canvas.create_text(1110,100,text='{} 個'.format(item_count),font=st_fnt,fill='black',tag='SCREEN')
    canvas.create_rectangle(1060,135,1210,160,fill='black',tag='SCREEN')
    canvas.create_rectangle(1060,135,1060+pl_life,160,fill='limegreen',tag='SCREEN')
    canvas.create_text(1130,148,text='LIFE',font=('Times New Roman',15),fill='white',tag='SCREEN')
    canvas.create_rectangle(1060,165,1210,190,fill='black',tag='SCREEN')
    canvas.create_rectangle(1060,165,1060+pl_stamina,190,fill='blue',tag='SCREEN')
    canvas.create_text(1130,178,text='STAMINA',font=('Times New Roman',15),fill='white',tag='SCREEN')
    canvas.create_text(1155,210,text='{}のダメージを受けた！'.format(pl_damage),font=('Times New Roman',15),fill='black',tag='SCREEN')


def main():
    global tmr,koff,key,idx
    tmr += 1
    draw_screen()
    if idx == 1:
        if tmr == 1:
            for emy in range(0,emy_num):
                emy_set(emy)
        move_player()
        obj_animation()
        if tmr%2 == 0:
            for emy in range(0,emy_num):
                move_emy(emy)
        if pl_life == 0:
            idx = 2
    if idx == 2:
        draw_text('You Died')
        if tmr == 20:
            idx = 1
            print('hiu')

    if koff == True:
        key = ''
        koff = False

    root.after(130,main)



root = tkinter.Tk()
root.title('メイドで冥土探検！')
root.bind('<KeyPress>',key_down)
root.bind('<KeyRelease>',key_up)
canvas = tkinter.Canvas(width=1256,height=864)
imgplayer = split_chip(imgplayer_pass,96,128)
emy_img = split_chip(emy_img_pass,96,128)
emy2_img = split_chip(emy2_img_pass,96,128)
emy3_img = split_chip(emy3_img_pass,96,128)
emy3_kageimg = split_chip(emy3_kageimg_pass,96,128)
takara_img = split_chip(takara_img_pass,256,64)
obj_img = split_chip(obj_pass,256,224)
obj2_img = split_chip(obj2_pass,96,128)
yuka_img = split_chip(yuka_pass,64,160)
kabe_img = split_chip(kebe_pass,256,192)
canvas.pack()
main()
root.mainloop()

```

<p> プレイ画面。</p>

![範囲を選択_008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606735/dac51698-8db5-cbc8-e2f5-54e54ec7d355.png)

<p>
 全く面白く無いですが、マップの生成、プレイヤーが足踏みしながら方向を変える、ワープゾーンがアニメーション、敵キャラの配置・移動、ダメージ計算・・・など、とりあえずダンジョンゲームに必要な基礎的な処理はまずまず揃ったのでは？と思います。ちなみに、敵への攻撃はまだ実装できていません（汗
</p>

<h3>改良したい事</h3>
<ul>
    <li>敵がバカで適当にうろうろしているだけ。追いかけてくるとか、移動スピードを変えるとか組み込んでみたい</li>
    <li>攻撃できるようにして、敵を撃破できるようにする</li>
    <li>(かなり発展？)プレイヤーのレベルやらステータスとか作り込む</li>
    <li>【願望】敵に強化学習or機械学習を組み込む</li>
</ul>

<p>コードもまだまだ整理出来ていない気がするので、少しずつ改良したいです。</p>

<h2>こだわり？</h2>
<p>ピクセル数を指定してキャラチップを分解する関数。色々情報を集めながら頑張って考えました。