作者: 機械伯爵
日時: 2002/5/2(15:07)
「P は Python の P <実戦編>」
 〜やるぞ作るぞRPG!〜


NP-2 領土分割

 というわけで、前回作ったEndless Battleという簡単ゲームを、今回は改良
してみようと思います。

 とはいっても、実はゲーム内容は(表示部分を除けば)全く変わりません。

 何をするかというと、コードの整理を行うわけです。

 ゲーム開発は使い捨てコードではありませんから、そのまま無理やり拡張し
ていけば、そのうちに何がなんだかわからなくなる恐れがあります。

 ですから、まずは拡張しやすいように、モジュール分割を行おうというわけ
です。

 ユーザ・インターフェイス、データの改良などをいっぺんに行うと、そのう
ちに不具合の追跡に困難をきたすことになります。

 というわけで、勇者に魔法の剣を持たせたり、他のモンスターを加えたりし
たい気持ちを抑えて、まずはゲームを整理してみましょう。

#<BEGIN>---ebv2.py---
#---ENDLESS BATTLE ver.2---
from dice    import *
from command import *
from display import *
from unit    import *

# 初期化作業

hero = Hero()
hero_action = FormalCommand((
  ("ATTACK",hero.attack),
  ("HEAL",hero.heal)
))

# ゲーム本体

print 'WELCOME TO THE "ENDLESS BATTLE" FIELD!!'
# メインループ

try:
  numberOfSlay = 0
  while numberOfSlay < 10 :
    print "Encounter!!"
    dragon = Dragon()
    while 1 :
      print "HERO'S   HP = ",hero.hp
      print "HERO'S   MP = ",hero.mp
      print "DRAGON'S HP = ",dragon.hp
      # 勇者の行動
      dragon.hp -= hero_action()
      # ドラゴンの反撃
      print ""
      hero.hp -= dragon.attack()
      next()
      if hero.hp <= 0 : raise "HeroHPZero"
      elif dragon.hp <= 0 : break
    print "YOU WIN!"
    numberOfSlay += 1
    print "LEVEL UP!!"
    hero.levelUp()
    next()
except "HeroHPZero":
  print "--HERO IS DEAD!--"
  print "--THE GAME OVER--"
  next()
else:
  print "--HERO SLAY 10 DRAGONS--"
  print "----CONGRATULATION!!----"
  print "--------GAME END--------"
  next()

#<END>---ebv2.py---

 手続きやクラスの定義がすべて外部モジュールになり、メインモジュールは
実際の処理の内容だけになっています。

 この内容自体はほとんど変化がありませんので、分割されたモジュールの方
を詳しく見ていってみましょう。

#<BEGIN>---dice.py---
# ダイス処理をまとめたモジュール

from whrandom import randint as _rnd

# 順にダイス数、ダイス面数、数値修正、出目修正
def dice(num=1,side=6,mod=0,adj=0):
  n = 0
  while num:
    n   += _rnd(1,side) + adj
    num -= 1
  return n + mod

def   d4(num=1) : return dice(num,4)
def   d6(num=1) : return dice(num,6)
def   d8(num=1) : return dice(num,8)
def  d10(num=1) : return dice(num,10)
def  d12(num=1) : return dice(num,12)
def  d20(num=1) : return dice(num,20)
def  d30(num=1) : return dice(num,30)
def d100(num=1) : return dice(num,100)

# 新しいダイスを作るための関数
def newDice(num=1,side=6,mod=0,adj=0):
  nd = lambda : dice(num,side,mod,adj)
  return nd

#<END>---dice.py---

 ボードゲームなどに馴染みの無い方には、「ナニやってんだコレ?」な内容
だと思われます(笑)が、実は、実際にダイス(サイコロ)を転がしてゲームを行
うことを想定しながら作っております。

 というか…コンピュータを使わないRPGとゆーのは、コンピュータのソフトの
マニュアルより分厚いルールブックを読みながら行うゲームですので、実際コー
ディングしながら「こんなの、よくやってたな」と自分自身で感心してました。

 4面〜100面ダイスは実在します。

 私はほとんど持っていますが、ゴルフボールのように座りの悪い100面ダイス
は嫌いなので持ってません。代わりに、00-90の刻印がある10面ダイスを、普通
の10面ダイスとペアで使って代用しています。

 ちなみに、30面ダイスはマイナーなので、実際のゲームではあまり使われま
せん。

 このモジュールは、現在はごく一部しか使われていませんが、様々な拡張を
予想して、いろいろ便利につかえるように用意されています。

 まぁ、これだけあれば、多分乱数発生は問題無いでしょう(笑)

#<BEGIN>---command.py---
# 定型式コマンドクラス
# コマンド内容と反応が決まっているコマンドを作るクラス
# システムコマンドなどに使用
# 要求されるのは、('コマンド文字列',コマンド)のペアのタプルかリスト
from sys import exit

class FormalCommand :
  def __init__(self,comList):
    self.comList = comList
    self.num = comList.__len__()
  def __call__(self):
    n = 0
    # コマンド内容の一覧
    for x in self.comList :
      print n , ': ' , x[0]
      n += 1
    print 'INPUT COMMAND NUMBER,AND HIT ENTER KEY'
    while 1:
      c = raw_input('COMMAND >> ')
      if c == "end":exit()
      elif c.isdigit() :
        if 0 <= int(c) < self.num : break
        else : print "OVER RANGE\nPREASE ONCE MORE..."
      else : print "BAD COMMAND\nPREASE ONECE MORE..."
    c = int(c)
    return self.comList[c][1]()

# 非定型式コマンドクラス
# コマンド内容が、ファジーに変化するクラス
# 魔法やアイテム管理などに使用予定
# class InformalCommand
#<END>---command.py

 コマンドモジュールは、現在使用されている定型式コマンドの他に、ファジー
に拡張可能な非定型式コマンドを予定していますが、現在はアイディアだけで
す(笑)

 最終的には、定型、非定型を組み合わせて階層構造をつくり、ゲーム全体で
様々な行動ができるように改良する予定です。

#<BEGIN>---display.py---
# 表示停止、更新プロンプト
# Windows2000やNTのコマンドプロンプトでは、どうやらエスケープシーケンシー
# が使えないらしいので、直接コマンド入力。Win9xもLinuxもOKのはず・・・

import os as _os

def next():
  raw_input("\n<<HIT ENTER KEY>>\n")
  if _os.name=='nt':
    _os.system("cls")
  elif (_os.name == 'dos') or (_os.name=='posix'):
    print "\033[2J\033[0;0H" # エスケープシーケンスによる画面制御

#<END>---display.py---

 MS-DOS〜Win9x時代のcommand.comに代わって新たにNT以降登場したcmd.exeは、
様々な点で変更がなされていて、使いやすく高性能になった点も多いのですが、
エスケープシーケンシーを扱っていたansi.sysが組み込まれないため、エスケー
プ文字出力による画面制御ができなくなったようです。

 ちなみに、以前に友人に頼んでテストしてもらった結果、DOSプロンプトでも
Linuxなどでも、同じエスケープコードが使える(同じような端末規格を使用し
ている)ようなので、それで制御してありますが、おかしな挙動を示すようなら、
エスケープ文字出力のところは、適当に書き直すか、あるいは消してください
(単に、画面の文字を消して、カーソルを左上に戻しているだけです)

#<BEGIN>---unit.py---
from dice import *
# 勇者クラスを作る
class Hero:
  def __init__(self):
    self.lv = 0
    self.levelUp()   # HP,MPについては、1レベルにアップさせて初期化する。
  def levelUp(self):
    self.lv += 1
    self.hp = 10 + self.lv
    self.mp = self.lv
  def attack(self):
    print "HERO'S ATTACK!"
    if d20() > 4 :
      print "HIT!"
      return 2
    else :
      print "MISS!"
      return 0
  def heal(self):
    print "USE MAGIC!"
    if self.mp > 0:
      self.hp = 10 + self.lv
      self.mp -= 1
    else:
      print "MP IS CONSUMED!"
    return 0

# ドラゴンクラスを作る
class Dragon:
  def __init__(self):
    self.hp = dice(1,6,9) # 10-15
  def attack(self):
    n = d20()
    if n > 10 : # ドラゴンブレス
      print "DRAGON BREATH!!!"
      d = self.hp / 2
      self.hp -= 1
      return d
    else :    # 通常攻撃
      print "CLAW AND BITE!!"
      if d20() > 10:
        print "HIT!"
        return 1
      else :
        print "MISS!"
        return 0

#<END>---unit.py---

 将来ゲームシステムが安定した後は、このようなクラス設定を書くのがメイ
ンになります。

 現在はユニット管理だけの簡単なものですが、職業・魔法・アイテム・モン
スターなどのデータも全てクラスで表現される予定です。

 まぁ、このゲーム自体は感触をつかむだけの試作品なので、どこまで拡張す
るかはわかりませんが、再利用できるパーツはできるだけ作っておきたいと思
います。

 では次回は、ゲームの内容自体に少し改良を加えてみたいと思います。

※ちなみに、前回は強制終了させるしか途中終了させられませんでしたが、コ
  マンドモジュールを独立させるにあたって、"end"と入力すると、途中でプロ
  グラムを終えるようにしてあります。

   機械伯爵