タイニーなあやつで、簡単なゲームを…(後編)

前回の記事はこちらになります~ cacapon.hatenablog.com

前回の記事で足りていなかった情報を書いていきますね~
まず、今回のゲームはこちらの最初の課題を元にしています~

こちらの本は、C++を題材にしていますが、
今回はpythonで実装したので、
ヒントとして使ったのはこちらになります。

// ゲームは私の知る限り一つの例外もなくこの構造とのこと。
while ( true ){
    getInput();
    updateGame();
    draw();
} 

ステージデータ

########
# .. p #
# oo   #
#      #
########

これを元に、Tiny Core Linuxで動くゲーム
Mr.Carrierの作成に取り掛かりました。

そして完成したのがこちら。
矢印キーで移動して、oを.の所まで押していく感じですね~
CTRL+Cで終了、CTRL+Rで場面のリセットができるようになってます♪
f:id:cacapon:20190524211644g:plain

結局丸一日くらいで完成なので、
初心者レベルでしょうか…
(本によると新入社員さんでおおよそ1日~2日で完成だそうです)

コードはこんな感じで作りました~

フォルダ構造

mr_carrier
|--draw.py
|--getch.py
|--getinput.py
|--listfind.py
|--main.py
|--stage_data.txt
|--updategame.py

main.py 大まかな流れをこちらに書きました~

from getinput import GetInput as GetI
from updategame import UpdateGame as UpG
from draw import Draw as D


UpG.init_stage_data()
UpG.stage_error_ck()
D.draw()

while UpG.clear_flg():
    get_key = GetI.getinput()
    if get_key == 'exit':
        break
    UpG.update_game(get_key)
    D.draw()

print('Thank you for playing!')

draw.py CUIに表示する部分はここに実装しました~
ただ、クリアメッセージがここに書けなかったのが懸念になってます~

from subprocess import call
from updategame import UpdateGame as UpG


class Draw():
    @staticmethod
    def draw():
        call('clear')
        print('Hi! Mr.Carrier!')
        print('Could you carry my luggage up to . ?')
        print('')
        print('=====Rule=====')
        print('1.You can move with the direction key.')
        print('2.You can push luggage')
        print('  But only one luggage can be pushed.')
        print('3.You should carry all luggage to . ')
        print('====Command===')
        print('[CTRL+C : EXIT] [CTRL+R : RESET GAME]')

        print('')

        # struct stage data.
        for line in UpG.stage_data:
            print(''.join(line))

getch.py 矢印キーが入力したらすぐ反応できるようにするために
こちらの実装を入れました~
ただ、windowslinuxで使える関数が違うみたいです~

class GetCh():
    @staticmethod
    def getch():
        try:
            from msvcrt import getch
            return getch()
        except ImportError:
            import sys
            import tty
            import termios
            fd = sys.stdin.fileno()
            old = termios.tcgetattr(fd)
            try:
                tty.setraw(fd)
                return sys.stdin.read(1)
            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, old)

getinput.py 入力値が65番だったら↑、といった感じで割り振りをしています~
vscodeで確認したときと、linuxのtarminalで確認したときに番号が違ったので、
そのまま移植は出来なさそうです~

from getch import GetCh as G

key_list = {
    '65': 'up',  #vscode:72 
    '66': 'down',  #vscode:80
    '67': 'right',   #vscode:77
    '68': 'left',  #vscode:75
    '3': 'exit',
    '18': 'reset',
    # vector key is 27 + 91 + X.
    # 27 and 91 is ignore
    '27': 'ignore',
    '91': 'ignore'
}


class GetInput():
    @staticmethod
    def getinput():
        get_key = ord(G.getch())
        return key_list.get(str(get_key))

listfind.py pythonのlist.index(value)ってvalueがないとValueErrorになって止まってしまいます。
それを防ぐために、無い場合はNoneを返す、ListFindクラスを作成しました~

class ListFind():
    @staticmethod
    def listfind(list, words):
        # lower and upper measures.
        local_list = []
        for value in list:
            local_list.append(value.lower())
        try:
            num = local_list.index(words.lower())
            return num
        except ValueError:
            return None

updategame.py 今回一番大変だったところです~
もうちょっとスマートにできたらよかったなぁ…

import copy
from listfind import ListFind as Lfind

# game data update.
class UpdateGame():
    stage_data = []
    cnt_dot = 0
    cnt_obj = 0
    cnt_p = 0
    
    @classmethod
    def init_stage_data(cls):
        cls.stage_data = []
        cls.cnt_dot = 0
        cls.cnt_obj = 0
        cls.cnt_p = 0
        with open('stage_data.txt', mode='r') as f:
            for line in f:
                list_line = list(line.rstrip())
                cls.stage_data.append(list_line)
                cls.cnt_dot += list_line.count('.')
                cls.cnt_obj += list_line.count('o')
                cls.cnt_p += list_line.count('p')
    
    @classmethod
    def stage_error_ck(cls):
        if cls.cnt_dot != cls.cnt_obj:
            raise ValueError('goal and object is not same count.')
        if cls.cnt_p != 1:
            raise ValueError('Player is not one people. {}'.format(cls.cnt_p))
    
    @classmethod
    def clear_flg(cls):
        # not yet...
        luggage_on_goal =  0
        for row in cls.stage_data:
            luggage_on_goal += row.count('O')

        if cls.cnt_obj == luggage_on_goal:
            print('Game Clear!!')
            print('Thank you bery match. It was saved!')
            return False
        return True
        
    @staticmethod
    def update_game(key):
        # key -> move player
        if key == 'ignore':
            return
        if key == None: #wait.
            return
        if key == 'reset':
            UpdateGame.init_stage_data()
            return
        UpdateGame.player_action(key)
    
    @classmethod
    def player_action(cls, direction):    
        player = Player(direction)
        
        player.set_player_pos(cls.stage_data)
        player.set_move_pos(direction)
        
        player.move(cls.stage_data)
    
    @classmethod
    def luggage_action(cls, my_pos, direction):
        luggage = Luggage(my_pos)
        luggage.set_move_pos(direction)
        
        luggage.move(cls.stage_data)
                         

class Common():
    def __init__(self):
        self.bef_pos = {'y': None, 'x': None}
        self.aft_pos = {'y': None, 'x': None}
        
        self.dict_direction = {
            'up' : {'y': -1, 'x': 0},
            'down' : {'y': 1, 'x': 0},
            'left' : {'y': 0, 'x': -1},
            'right' : {'y': 0, 'x': 1}
        }
        
    def set_move_pos(self, direct):
        # {'x':?, 'y':?} + {'x':?, 'y':?}
        self.aft_pos = {'x' : None, 'y': None}
        self.aft_pos['x'] = self.bef_pos['x'] + self.dict_direction[direct]['x']
        self.aft_pos['y'] = self.bef_pos['y'] + self.dict_direction[direct]['y']
    
    def move(self, stage_data, obj):
        if stage_data[self.aft_pos['y']][self.aft_pos['x']] == '#':
            # not move
            return 
        if (stage_data[self.aft_pos['y']][self.aft_pos['x']] == ' ' or
            stage_data[self.aft_pos['y']][self.aft_pos['x']] == '.'):
            # move
            self.bef_check(stage_data)
            self.aft_check(stage_data, obj)
    
    def bef_check(self, stage_data):
        if (stage_data[self.bef_pos['y']][self.bef_pos['x']] == 'O' or 
            stage_data[self.bef_pos['y']][self.bef_pos['x']] == 'P'):
            stage_data[self.bef_pos['y']][self.bef_pos['x']] = '.'
        else:
           stage_data[self.bef_pos['y']][self.bef_pos['x']] = ' '
    
    def aft_check(self, stage_data, obj):
        if stage_data[self.aft_pos['y']][self.aft_pos['x']] == '.':
            stage_data[self.aft_pos['y']][self.aft_pos['x']] = obj.upper()
        else:
            stage_data[self.aft_pos['y']][self.aft_pos['x']] = obj
                                        

class Player(Common):
    def __init__(self, direction):
        super(Player, self).__init__()
        self.direction = direction
    
    def set_player_pos(self, stage_data):
        for r,row in enumerate(stage_data):
            if Lfind.listfind(row, 'p') is not None :
                self.bef_pos['y'] = r
                self.bef_pos['x'] = Lfind.listfind(row, 'p')
                break
    
    def move(self, stage_data):
        if (stage_data[self.aft_pos['y']][self.aft_pos['x']] == 'o' or
            stage_data[self.aft_pos['y']][self.aft_pos['x']] == 'O'):
            # put luggage
            UpdateGame().luggage_action(self.aft_pos, self.direction)    
            return
        super().move(stage_data, 'p')
                                                                                        

class Luggage(Common): 
    def __init__(self, my_pos):
        super().__init__()
        self.bef_pos = my_pos
    
    def move(self, stage_data):
        if (stage_data[self.aft_pos['y']][self.aft_pos['x']] == 'o' or
            stage_data[self.aft_pos['y']][self.aft_pos['x']] == 'O'):
            # not move
            return
        super().move(stage_data, 'o')

stage_data.txt ステージのデータです。ここを変えればステージを変えることができます。
updategeme.pyでステージのエラーチェックもしていますが、
①荷物とゴールの数が一致しているか?
②プレイヤーが一人になっているか?
のチェックしかしていないので、
囲いがちゃんとしていなかったり、
指定の文字列以外が入っていたりすると、
うまく動かなくなりそうです~

########
# .. p #
# oo   #
#      #
########

大変だったこと

  • drawやkey_inputは比較的すんなりできたのですが、
    updateの部分でどのように実装するか悩みました~
  • 一応遊べるところまでできたのは良かったですが、
    セクション内の記述にもある通り、ちょこちょこバグが起きそうなところとか、
    思ったより長くなってしまったところ、
    理想形が分からなく妥協しているところもあるので、
    そこは課題になりそうです~
  • Tiny Core Linuxを日本語対応させてなかったので、日本語が使えませんでした…
  • 新PCに戻したとき、動かなくなったこと。
    文字コードtarminalとwindows?で違ったみたいで矢印キーが認識されませんでした~
    こういうところで移植の問題があるんだなぁと痛感しました~

まとめ
思ったより大変なところもありましたが、
総じて楽しめてゲーム作りができたかなぁと思います♪
でも、これが一番最初の課題なんですよね…
そして、現在画像すら出てきてないレベルです~
ここから少しでもレベルアップをして
今出ているゲームに近い物が作れるようになりたいなぁ
と思うcacaponでした~