前の頁へ   次の頁へ

老い学の捧げ物

9.データのR/W

さらなる改良として、面選択の画面で既にクリアした面がわかればいいと思いました。そのためにはクリアした時点でその面の番号を保存したファイルを作成しなければなりません。また一度クリアした面に再挑戦する場合には、前回の歩数より少ない歩数でクリアすればやり甲斐が出るので、このファイルには前回の歩数も保存した方がいいですね。さらに、一度クリアした面でも再挑戦するとクリアできないこともあり、どうやってクリアできたのか知りたいこともあります。このような場合には、作成した履歴配列を保存しておくのがいいでしょう。

そこでクリア面データを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[]を作成し、それを読んで表示してみましょう。

ファイル名:mapStage.py
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を import 
import 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)
               break

   def 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')
ファイル名:souko.py
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を使うことになります。

前の頁へ   次の頁へ