LoginSignup
2
4

Pythonで狼と羊の生態系マルチエージェントシミュレーション(MAS)を作成した

Last updated at Posted at 2023-09-05

狼と羊の生態系マルチエージェントシミュレーションを作成しましたので公開します。

できる限り解説コメントを付けていますので、ご参考にどうぞ。

狼と羊の生態系マルチエージェントシミュレーション(MAS)とは何か

簡単に言うと弱肉強食(predator-prey)の生態系をプログラムで再現するものです。

 狼と羊はランダムにマップ上に置かれます。各ステップごとに狼と羊は移動し、体力が1減少します。
体力が0になると死んでしまうため、各エージェント(狼や羊)は羊や牧草を食べて体力を回復しなければなりません。
 また、人口規模を維持するために、各エージェント(狼や羊)はステップごとに数%の確率で繁殖します。

注:このモデルの原典は米国タフツ大STARLogoTで公開されている「オオカミとヒツジ」モデルです。

MASに採用している条件
・エージェント(狼や羊)がフィールドの壁に向かって移動すると、その壁と対になる壁から出現する。

コードの工夫

エージェント情報を辞書で管理し、MASの高速化を図る
->これは辞書に保存されたエージェントの探索がリストに比べて高速に行えるためです。(筆者のPC環境でリストだと35秒程度、辞書だと5秒程度かかりました)

エージェントにクラスメソッドを使用して、オブジェクト生成時に毎回特定の辞書へ格納するコードを記述する手間を省く
->筆者はエージェントのオブジェクト生成時に格納する辞書の名前をいちいち覚えていられない為、自動で格納されるようにしています。

エージェントの親クラスAgentを設ける
->コードが短くなりました

コード本文

コメントでできる限りの解説を行っています。是非お読みください。

コードに違いや無駄があった場合はコメントで教えて頂けると助かります。
注意:実行には数秒かかります。

追記:MASのプログラミングの仕方はたくさんあります。
・エージェントを2次元配列で管理するなど大域変数を設ける
・エージェントは他のエージェントの情報(メンバ変数など)をいじらない
など様々な設計思想がありますが、今回紹介するプログラムには上記の制約を一切反映しておりません。
まだまだ勉強中の身ですのでどうかご容赦をお願いします。

狼と羊のMAS.py
import random
import time

# エージェントが移動するエリアの縦、横の上限を設定
CELL_WIDTH = 50 
CELL_HEIGHT = 50

START_SHEEP_COUNT = 100 # 最初の羊の数
NOW_SHEEP_COUNT = 0   # 現在の羊の累積数(死んでいる羊もカウントしている)
START_WOLF_COUNT = 50   # 最初の狼の数
NOW_WOLF_COUNT = 0    # 現在の狼の累積数(死んでいる狼もカウントしている)

STEP = 200 # ステップ数

ETABLE_COUNT = 10 # 牧草が食べられる状態になるまでに必要なステップ数

SHEEP_RECOVERY_AMOUNT = 5 # 羊が牧草を食べたときのhp回復量
WOLF_RECOVERY_AMOUNT = 30 # 狼が羊を食べたときのhp回復量

SHEEP_BIRTH_RATE = 4 # 羊の出産率(単位:%)
WOLF_BIRTH_RATE = 2  # 狼の出産率(単位:%)



# 牧草
class Grass:

    # オブジェクト生成時自動格納する辞書を設定する関数
    @classmethod
    def set_dict(cls, dict):
        cls.dict = dict

    def __init__(self, pos:tuple, eat_step:int = random.randint(1, 9)):
        self.pos = pos # 位置座標
        self.dict[str(self.pos)] = self # 辞書に登録 Key: 位置座標, Value: オブジェクトそのもの

        # ステップごとにこの値(eat_step)が1増加し、10以上になると羊が食べる。
        #その後、また0からステップごとに1増加するを繰り返す。
        self.eat_step = eat_step 
    
    # 可食カウントを進める
    def add_eat_step(self):
        self.eat_step += 1
    
# エージェントの親クラス(羊、狼)
class Agent:

    # オブジェクト生成時自動格納する辞書を設定する関数
    @classmethod
    def set_dict(cls, dict, grasses):
        cls.dict = dict # Agent(Sheep, Wolf)のオブジェクトを保存するための辞書。Key: クラス名 + 個体識別番号, Value: オブジェクトそのもの
        
        # Sheepが牧草を食べる際、参照が必要になるため
        #あらかじめ牧草オブジェクトが保存されている辞書(Key: 位置座標(?, ?), Value: オブジェクトそのもの)を保存しておく
        cls.grasses = grasses 

    def __init__(self, id:int, pos:tuple, hp:int, maxhp:int):
        self.id = id   # 個体識別番号
        self.pos = pos # 位置座標
        self.hp = hp   # ヒットポイント(毎ターン減少し続け0になると死亡、何かを食べることでこの数値が増える)
        self.maxhp = maxhp # ヒットポイントの上限(エージェント(狼や羊)が食い溜めできる量には限界がある)
    
    # Agentクラスの辞書にこのSheep, Wolfオブジェクトを格納する予定であるため、
    # 各クラスのオブジェクトの見分けがつくようにするための関数。
    # 後に記述するSheep, Wolfそれぞれでオーバーライドしている
    def isSheep(self):
        return False

    def isWolf(self):
        return False
    
    # 移動してhpを1減らす
    def move_random(self):
        # 1:"上", 2:"下", 3:"左", 4:"右"
        # 左上が(1,1), 右下が(CELL_WIDTH CELL_HEIGHT)
        dir = random.randint(1, 4)
        if(dir == 1):# 上
            # print("上に移動")
            if(self.pos[1] - 1 >= 1):# 壁にぶつからないならば
                self.pos = (self.pos[0], self.pos[1] - 1)
            else:# 壁にぶつかったならその壁と対になる壁から出現する
                self.pos = (self.pos[0], CELL_HEIGHT)

        elif(dir == 2):# 下
            # print("下に移動")
            if(self.pos[1] + 1 <= CELL_HEIGHT):# 壁にぶつからないならば
                self.pos = (self.pos[0], self.pos[1] + 1)
            else:# 壁にぶつかったならその壁と対になる壁から出現する
                self.pos = (self.pos[0], 1)

        elif(dir == 3):# 左
            # print("左に移動")
            if(self.pos[0] - 1 >= 1):# 壁にぶつからないならば
                self.pos = (self.pos[0] - 1, self.pos[1])
            else:# 壁にぶつかったならその壁と対になる壁から出現する
                self.pos = (CELL_WIDTH, self.pos[0])

        else:# 右
            # print("右に移動")
            if(self.pos[0] + 1 <= CELL_WIDTH):# 壁にぶつからないならば
                self.pos = (self.pos[0] + 1, self.pos[1])
            else: # 壁にぶつかったならその壁と対になる壁から出現する
                self.pos = (1, self.pos[0])
        
        # お腹が減る
        self.hp -= 1

# 羊
class Sheep(Agent):
    def __init__(self, id:int, pos:tuple, hp: int, maxhp: int):
        super().__init__(id, pos, hp, maxhp) # 親クラス(Agent)のコンストラクタ(__init__)関数を起動

        # 自身のオブジェクトを辞書(親クラスのset_dict関数で設定)に保存する
        self.dict[str(self.__class__.__name__) + str(self.id)] = self 
    
    # Agentクラスの辞書にこのSheepオブジェクトを格納する予定であるため、
    # Wolfクラスのオブジェクトと見分けがつくようにするための関数
    def isSheep(self):
        return True

    # 牧草を食べてhp回復
    def eat(self):
        if(self.hp > 0):
            grass = self.grasses[str(self.pos)] # 直下の牧草オブジェクトを取得
            # 食べれる牧草が自身の座標と同じ座標にあるなら食べる
            if(grass.eat_step < ETABLE_COUNT):
                pass
                # print("羊%s:草がない:hp = %s"%(self.id, self.hp))
            else:
                if(self.hp + SHEEP_RECOVERY_AMOUNT > self.maxhp):
                    self.hp = self.maxhp
                else:
                    self.hp += SHEEP_RECOVERY_AMOUNT # 体力回復
                #print("羊%s:草を食べる:hp = %s"%(self.id, self.hp))
                grass.eat_step = 0
                
            
    # 4%の確率で出産する  
    def born(self):
        # 繁殖
        if(random.randint(1,100) <= SHEEP_BIRTH_RATE):
            #print("羊%sが繁殖した"%(self.id))
            self.hp = int(self.hp/2)
            BORN_SHEEP.append(self.pos)
                           
# 狼
class Wolf(Agent):
    def __init__(self, id:int, pos:tuple, hp:int, maxhp:int):
        super().__init__(id, pos, hp, maxhp) # 親クラス(Agent)のコンストラクタ(__init__)関数を起動

        # 自身のオブジェクトを辞書(親クラスのset_dict関数で設定)に保存する
        self.dict[str(self.__class__.__name__) + str(self.id)] = self
    
    # Agentクラスの辞書にこのSheepオブジェクトを格納する予定であるため、
    # Wolfクラスのオブジェクトと見分けがつくようにするための関数
    def isWolf(self):
        return True
    
    # 羊を食べてhp回復
    def eat(self):
        if(self.hp > 0):
            # 自身の座標と同じ座標に羊がいたなら食べる
            for sheep in self.dict.values():
                if(sheep.pos == self.pos and sheep.isSheep()):
                    #print("狼%sが羊%sを食べた!"%(self.id, sheep.id))
                    if(self.hp + WOLF_RECOVERY_AMOUNT > self.maxhp):
                        self.hp = self.maxhp
                    else:
                        self.hp += WOLF_RECOVERY_AMOUNT # 体力回復
                    sheep.isalive = False
                    break
                
    # 2%の確率で出産する  
    def born(self):
        # 繁殖
        if(random.randint(1,100) <= WOLF_BIRTH_RATE):
            #print("狼%sが繁殖した"%(self.id))
            self.hp = int(self.hp/2)
            BORN_WOLF.append(self.pos)

# このプログラムの実行時間を計測開始
start = time.time()

# 初期設定
# オブジェクト生成時に自動格納される辞書を設定
Agents = {}  # エージェント(Sheep, Wolf)のオブジェクト, Key:クラス名 + 個体識別番号, Value: Agent(Sheep, Wolf)オブジェクト
Grasses = {} # 牧草(Grass)オブジェクトを保存, Key:座標(?, ?), Value: Grassオブジェクト
Grass.set_dict(Grasses)
Agent.set_dict(Agents, Grasses)# 牧草オブジェクトの情報を使用するため第2引数が必要

# 牧草オブジェクト作成
for w in range(CELL_WIDTH):
    for h in range(CELL_HEIGHT):
        Grass((w+1,h+1)) # 引数:位置座標(1, 1) ~ (CELL_WIDTH, CELL_HEIGHT)

# ランダムに500個を食べられる牧草オブジェクトにする
eatable = 0
while(eatable < 500):
    random.seed()
    x = random.randint(1,CELL_WIDTH)
    random.seed()
    y = random.randint(1,CELL_HEIGHT)
    n = Grasses[str((x, y))]# ランダムに選ばれた牧草オブジェクト
    if(n.eat_step != 10):
        n.eat_step = 10
        eatable += 1

# 羊を初期生成
while(NOW_SHEEP_COUNT < START_SHEEP_COUNT):
    random.seed()
    x = random.randint(1,CELL_WIDTH)
    random.seed()
    y = random.randint(1,CELL_HEIGHT)
    NOW_SHEEP_COUNT += 1 

    # 個体識別番号, 位置座標, 体力, 最大体力
    Sheep(NOW_SHEEP_COUNT, (x, y), random.randint(1, 10), 100) 

# 狼を初期生成
while(NOW_WOLF_COUNT < START_WOLF_COUNT):
    random.seed()
    x = random.randint(1,CELL_WIDTH)
    random.seed()
    y = random.randint(1,CELL_HEIGHT)
    NOW_WOLF_COUNT += 1 

    # 個体識別番号, 位置座標, 体力, 最大体力
    Wolf(NOW_WOLF_COUNT, (x, y), random.randint(1, 60), 300)      

# (main文)ステップ数だけ回す
for step in range(STEP):

    # 死亡したエージェントをfor文の外で削除するために一時保存するリスト
    DEAD_AGENT = [] # 死亡したエージェントのオブジェクトを格納するリスト

    #生まれたエージェントをfor文の外で生成するために一時退避する場所
    BORN_SHEEP = [] # エージェントの座標(タプル型)を格納するリスト
    BORN_WOLF = []  # エージェントの座標(タプル型)を格納するリスト

    # 捕食
    for n in Agents.values():
        n.eat()
        # print(n.__class__.__name__,n.id, n.pos, "hp:",n.hp)
    
    # ランダムに移動
    for n in Agents.values():
        n.move_random()
    
    # エージェントの死亡判定
    for n in Agents.values():
        if(n.hp < 1): # hpが0以下で死亡
            DEAD_AGENT.append(n)

    # 死亡したエージェントを削除
    if(DEAD_AGENT):
        for dead in DEAD_AGENT:
            del Agents[str(dead.__class__.__name__) + str(dead.id)]
            #print("<"+str(str(dead.__class__.__name__) + str(dead.id))+"が死亡>")

    # エージェントの出産判定
    for n in Agents.values():
        n.born()

    # 出産されたエージェントの生成
    if(BORN_SHEEP):
        for pos in BORN_SHEEP:
            NOW_SHEEP_COUNT += 1 # 累積羊数の増加
            Sheep(NOW_SHEEP_COUNT, pos, random.randint(1, 10), 100)
            #print("羊%sが誕生"%(NOW_SHEEP_COUNT))
    
    if(BORN_WOLF):
        for pos in BORN_WOLF:
            NOW_WOLF_COUNT += 1 # 累積狼数の増加
            Wolf(NOW_WOLF_COUNT, pos, random.randint(1, 60), 300)
            #print("狼%sが誕生"%(NOW_WOLF_COUNT))
        

    # 牧草の更新
    for n in Grasses.values():
        n.add_eat_step()

end = time.time()

print("実行時間:%s"%(end- start))

# 現在生きているエージェント(羊、狼)の数を数える
live_sheep_count = 0
live_wolf_count = 0
for x in Agents.values():
    if(x.isSheep()):
        live_sheep_count += 1
    elif(x.isWolf()):
        live_wolf_count += 1

print("開始羊数",START_SHEEP_COUNT,"最終羊数:", live_sheep_count, "累積羊数:",NOW_SHEEP_COUNT)
print("開始狼数",START_WOLF_COUNT,"最終狼数:", live_wolf_count, "累積狼数:",NOW_WOLF_COUNT)


結果

実行時間:5.223088502883911
開始羊数 100 最終羊数: 865 累積羊数: 4340
開始狼数 50 最終狼数: 679 累積狼数: 789
2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4