さらなる改良として、面選択の画面で既にクリアした面がわかればいいと思いました。そのためにはクリアした時点でその面の番号を保存したファイルを作成しなければなりません。また一度クリアした面に再挑戦する場合には、前回の歩数より少ない歩数でクリアすればやり甲斐が出るので、このファイルには前回の歩数も保存した方がいいですね。さらに、一度クリアした面でも再挑戦するとクリアできないこともあり、どうやってクリアできたのか知りたいこともあります。このような場合には、作成した履歴配列を保存しておくのがいいでしょう。
そこでクリア面データをR/Wするクラスを作成したいと思います。作成するファイルは一行に、クリアした面番号、クリア時の歩数、クリア時のplayerHis[]の3つが記されたファイルとします。保存するときにどう書き出すか悩むような煩雑な構造をしているデータとなるので、それをまるっと保存するときに非常に便利と言われるpickleライブラリを使うことにしました。
pickleを使うのは初めてなので、テストしてみます。pickleは標準ライブラリなので予めpythonに入っており、import文だけで使えます。下のコードでテストしました。面番号に対応する変数を「num」、クリア時の歩数に対応する変数を「counter」としました。荷物は二つの場合を想定し、その履歴配列に対応する変数を「bagsHis」としています。この三つの変数を要素とする配列「now」を作り、クリアした面が四つある場合を想定してnowを四つ含む配列「clearSt」を作成しました。このclearStをpickleでwriteしてclearSt.binに保存し、それをreadしてCMDに表示してみます。
001:import pickle 002: 003:num = 1 004:counter = 30 005:bagsHis = [[(1,1), (2,2)], [(2,1), (2,2)], [(2,1), (3,2)], [(2,1), (3,3)]] 006: 007:now = [num, counter, bagsHis] 008:clearSt = [] 009:clearSt.append(now) 010:clearSt.append(now) 011:clearSt.append(now) 012: 013:f = open('clearSt.bin', 'wb') 014:pickle.dump(clearSt, f) 015:f.close() 016: 017:with open('clearSt.bin', 'rb') as f: 018: test = pickle.load(f) 019:print(test) 020:
上を実行するとclearSt.binが自動作成され、CMDには[[1, 30, [[(1, 1), (2, 2)], [(2, 1), (2, 2)], [(2, 1), (3, 2)], [(2, 1), (3, 3)]]], [1, 30, [[(1, 1), (2, 2)], [(2, 1), (2, 2)], [(2, 1), (3, 2)], [(2, 1), (3, 3)]]], [1, 30, [[(1, 1), (2, 2)], [(2, 1), (2, 2)], [(2, 1), (3, 2)], [(2, 1), (3, 3)]]]]と表示されました。
pickleでは13-15行のようにしてファイルに保存し、17-18行のようにファイルから読み出します。13-15行のようにopen/closeを明示するのが基本ですが、pythonでは17-18行の書き方もでき、この書き方ならクローズ処理が不要になります。
面をクリアする度に配列clearSt[]にデータを追加することになりますが、pickleは追記に対応していないことがわかりました。そこでネットを探索し、色々とテストしてみましたがなかなか上手くいきません。どうしたものか悩んでいましたが、配列に追記するのでありファイルに追記するのではないことに気付きました。そうです、データを追加した配列を作り、それをファイルに書き出せばいいという当たり前のことにようやく気付いたのです。ずいぶん遠回りをしてしまいました。
ネットを検索しているときにある記述を見つけました。pickleを使うなら「pandas」が便利とのことです。pythonにpandasライブラリがあることは知っていましたが名前を知っているだけでした。調べてみるとpandasをimportするだけで、pickleのimportは不要であり、W/Rもそれぞれ1行書くだけですむそうです。やってみましょう。「pip」を使ってpandasをインストールし、test.pyを作ってテストしてみます。
ファイル名:test.py 001:import pandas as pd 002: 003:num_1 = 1 004:counter_1 = 30 005:playerHis_1 = [[(1,1), (2,2)], [(2,1), (2,2)], [(2,1), (3,2)], [(2,1), (3,3)]] 006:num_2 = 14 007:counter_2 = 41 008:playerHis_2 = [[(10,1), (1,2)], [(10,1), (2,2)]] 009: 010:stage1 = [num_1, counter_1, playerHis_1] 011:stage2 = [num_2, counter_2, playerHis_2] 012: 013:testStage = [] 014:testStage.append(stage1) 015:testStage.append(stage2) 016: 017:pd.to_pickle(testStage, 'test.pkl') # file save 018:data = pd.read_pickle('test.pkl') # file read 019: 020:print(data)
test.pyを実行するとCMDには以下のように表示されました。
[[1, 30, [[(1, 1), (2, 2)], [(2, 1), (2, 2)], [(2, 1), (3, 2)], [(2, 1), (3, 3)]]], [14, 41, [[(10, 1), (1, 2)], [(10, 1), (2, 2)]]]]
上手くいったみたいです。1行目でpandasをimportしていますが、pickleはimportしていません。17行目でpickleを使ってデータをファイルに書き込み、18行目でpickleを使ってファイルからデータを読み出しています。ずいぶん短いコードでファイルをR/Wできることがわかりました。それでは、MapクラスにファイルのR/Wメソッドを作りましょう。
Rメソッド 001: def getCleared(self): # clearedList[]の作成 002: self.clearedList.clear() # 初期化 003: filename = 'cleared.pkl' 004: if os.path.exists(filename): # fileがあれば。無ければ何もしない 005: self.clearedList = pd.read_pickle(filename) Wメソッド 006: def save(self, data): 007: pd.to_pickle(data, 'cleared.pkl')
クリア情報を入れる配列名を「clearedList」とし、ファイル名は「cleared.pkl」としました。4行目はファイルが無い場合のエラー防止です。Rメソッドでは、pickleファイルを読んでclearedList[]に格納しています。Wメソッドでは、引数dataをcleared.pklに書き込んでいます。それではこのメソッドを使って、実際にclearedList[]を作成し、それを読んで表示してみましょう。
import pygame from pygame.locals import *import os import sys sys.path.append("C:\myPython\module") import loadimage as LI import math from button import Buttons # Buttons classを importimport pandas as pd WIDTH = 800 HEIGHT = 480 SCR_RECT = Rect(0, 0, WIDTH, HEIGHT) class Map: pygame.init() screen = pygame.display.set_mode(SCR_RECT.size) blockImg = LI.load_image("../images/block.png", -1) bagImg = LI.load_image("../images/bag.png", -1) placeImg = LI.load_image("../images/place.png", -1) storedImg = LI.load_image("../images/stored.png", -1) playerImg = LI.load_image("../images/player.png", -1) def __init__(self): self.mapdata = [] # mapdataを作成 self.filename = 'microban.txt' self.blocks = [] # 壁座標(tuple)の List self.places = [] # 収納場所座標(tuple)の List self.bags = [] # 荷物座標(tuple)の List self.stored = [] # 格納座標(tuple)の List self.player = [] # プレーヤー座標(tuple)の List、要素数は1 self.bagsHis = [] # 荷物座標(tuple)の履歴 List self.storedHis = [] # 格納座標(tuple)の履歴 List self.playerHis = [] # プレーヤー座標(tuple)の履歴 List self.stage = [] # 選択された面のデータ配列self.clearedList = [] # clear情報の配列 # txtファイルを読んでself.mapdata[]を作成するメソッド def getData(self): flag = [] # '#'がある行は'0' mapdataに不要な行は'1' f = open(self.filename, 'r') while(True): # 一行ずつ読んで listに格納 line = f.readline() if '#' in line: flag.append('0') else: flag.append('1') if not line: break f.close() max = 0 # 面数を取得して初期化 for i in range(len(flag)): if flag[i] == '0' and flag[i+1] == '1': # 不要行の次に'#'を含む行があれば max += 1 self.mapdata = [[] for _ in range(max)] # mapdata[][]の初期化 f = open(self.filename, 'r') lines = f.readlines() # 全行の list f.close n = 0 for i in range(len(lines)): # 全行を調べる if '#' in lines[i]: self.mapdata[n].append(lines[i].rstrip('\n')) # 改行を削除して配列に追加 if flag[i] == '0' and flag[i+1] == '1': # 不要行の次に'#'を含む行があれば n += 1 # mapdata[n++] # num個の buttonを表示してマウスで選択するメソッド def getStage(self, num):self.getCleared() # ファイルを読んでクリア面の配列を作成 btS = round(math.sqrt(num)) # 画面短辺方向のボタンの数 if num == 2: # 2個の場合は2とする btS = 2 btPx = int(HEIGHT/btS) # ボタン一辺のピクセル数 buttons = [] for i in range(num): # numの数だけ x = i // btS # 横方向のボタン数 y = i % btS # 縦方向のボタン数 buttons.append(Buttons(i, x*btPx, y*btPx)) # Buttonsのインスタンスを配列に入れる for button in buttons: button.draw(btPx, self.screen) # num個のbuttonを緑の正方形で描画 button.writeNum(self.screen) # 番号を全部黄色で表示 pygame.display.update() while(1): for event in pygame.event.get(): # 終了用のイベント処理 if event.type == QUIT: pygame.quit() sys.exit() if event.type == KEYDOWN and event.key == K_ESCAPE: pygame.quit() sys.exit() if event.type == MOUSEBUTTONDOWN and event.button == 1: for button in buttons: if button.getRect(btPx).collidepoint(event.pos): self.index = button.num stage = self.mapdata[self.index] return stage def setChars(self, stage): self.blocks.clear() self.places.clear() self.bags.clear() self.stored.clear() self.player.clear() for i in range(len(stage)): # 行数 for j in range(len(stage[i])): # 文字数 if stage[i][j] =='#': self.blocks.append((j, i)) # 壁(x,y) if stage[i][j] =='.': self.places.append((j, i)) # 置き場(x,y) if stage[i][j] =='$': self.bags.append((j, i)) # 荷物(x,y) if stage[i][j] =='@': self.player.append((j, i)) # 荷物(x,y) if stage[i][j] =='*': self.stored.append((j, i)) # 収納済(x,y) self.bags.append((j, i)) # 壁(x,y) self.places.append((j, i)) # 置き場(x,y) def getPlayerPos(self, stage): for i in range(len(stage)): # 行数 for j in range(len(stage[i])): # 文字数 if stage[i][j] =='@': # Player.posを get return(j, i) breakdef getCleared(self): # clearedList[]の作成 self.clearedList.clear() # 初期化 filename = 'cleared.pkl' if os.path.exists(filename): # fileがあれば。無ければ何もしない self.clearedList = pd.read_pickle(filename) print(self.clearedList) # CMDに表示 def save(self, data): pd.to_pickle(data, 'cleared.pkl')
import pygame from pygame.locals import*import os import sys sys.path.append("C:\myPython\module") import loadImage as LI # moduleの利用 from mapStage import Map # Mapクラスのインポート from players import Player DOWN, LEFT, RIGHT, UP = 0, 1, 2, 3 SCR_RECT = Rect(0, 0, 1200, 480) sysfont = pygame.font.SysFont(None, 30) sysfont1 = pygame.font.SysFont(None, 50) pygame.init() screen = pygame.display.set_mode(SCR_RECT.size) class Souko: def __init__(self): pass def main(self): clearFlag = 0 map = Map() # Mapクラスのインスタンス作成 map.getData() # mapdata[][]の作成 map.stage = map.getStage(len(map.mapdata)) # 面の選択 map.setChars(map.stage) # 選択した面に画像をセット pos = map.getPlayerPos(map.stage) player = Player(pos, DOWN) player.makeHis(map) # 履歴Listに最初の座標をセットmap.getCleared() clock = pygame.time.Clock() while (1): clock.tick(60) player.animate() # イベント処理 for event in pygame.event.get(): # 終了用のイベント処理 if event.type == QUIT: pygame.quit() sys.exit() if event.type == KEYDOWN and event.key == K_ESCAPE: pygame.quit() sys.exit() # その他のキーイベント処理 if event.type == KEYDOWN and event.key == K_BACKSPACE: # 一歩戻る player.playBack(map) if event.type == KEYDOWN and event.key == K_SPACE: # stage選択画面に戻るmap.clearedList.append([map.index, player.counter, map.playerHis]) # append map.save(map.clearedList) # cleared.pklに save map.stage.clear() # stage[]クリア self.main() # mainメソッドを実行 if event.type == KEYDOWN and event.key == K_r: # reset player.resetStage(map) # プレイヤーの移動処理 if event.type == KEYDOWN and event.key == K_DOWN: player.move(DOWN, map) if event.type == KEYDOWN and event.key == K_LEFT: player.move(LEFT, map) if event.type == KEYDOWN and event.key == K_RIGHT: player.move(RIGHT, map) if event.type == KEYDOWN and event.key == K_UP: player.move(UP, map) if map.bags == map.stored: # 収納完了時の表示 clearFlag = 1 end = sysfont1.render('Stage : '+ str(map.index) + ' Cleared!!', True, (0,255,255)) screen.blit(end, (0, 400)) player.drawChar(map, screen) # player以外を描画 player.draw(screen) # playerを描画 now = sysfont.render('Stage : '+ str(map.index), True, (255,255,255)) # stage No 表示 select = sysfont.render('Select : space key', True, (255,255,255)) # space Key 表示 count = sysfont.render('Counter : '+ str(player.counter), True, (255,255,255))# Counter No 表示 back = sysfont.render('Back : "Back space" key', True, (255,255,255)) # back key 表示 reset = sysfont.render('Reset : "R" key', True, (255,255,255)) # reset key 表示 screen.blit(now, (0, 10)) screen.blit(select, (0, 40)) screen.blit(count, (0, 70)) screen.blit(back, (0, 100)) screen.blit(reset, (0, 130)) pygame.display.update() screen.fill((0,0,0)) if __name__ == "__main__": souko = Souko() souko.main()
例えば43面を選んで一歩でクリアすると、CMDには[[43, 1, [(0, (1, 1)), (2, (1, 1))]]]と表示されます正しく動いているようです。しかし今のままではspaceキーを押すと、クリアしていなくてもその面の情報が記録されてしまいます。これを解決するために、souko.pyで定義しているclearFlagを使うことになります。