神のゲーム、またはウルフ島が書いたように

むかしむかし、私がまだ大学にいたとき、私たちの大学の数学部でプログラマーに興味深い仕事を依頼されたと聞きました。いわゆる「オオカミの島」をモデル化することです。 その本質はおおよそ次のとおりです。







写真にあるもの
停止/開始 -世界を開始

ターン -世界を止める

再起動 -世界を作り直す

緑の細胞 -草のある細胞。 緑が多いほど、草が多くなります。

小さなウサギとオオカミ-子犬

大きなノウサギとオオカミ-大人

動物アイコンの赤と青のストライプ -現在の満腹感。 赤は男性、青は女性です。

各セルの左下隅の数字は、このセルのクリーチャーの数です

以下は、ノウサギとオオカミの総数と、最後の動きを処理するのにかかる時間です。



サイズN * Nのオオカミの島に 、野ウサギとオオカミ生息しています。 各種にはいくつかの代表者がいます。 同じ確率1/9の各瞬間のノウサギは、8つの隣接するマスの1つに移動します(海岸線で囲まれたエリアを除く)。 確率p(br)を持つ各うさぎは、1石の2羽の鳥になります。 確率p(bw)を持つ各オオカミは2つのオオカミに変わります。 各オオカミはランダムに(ウサギのように)移動し、次の8つのマスの1つに狩りをするウサギがいます。 オオカミとノウサギが同じ広場にいる場合、オオカミはノウサギを食べて、「ポイント」を獲得します。 そうでなければ、彼はdポイントを失います。 ゼロポイントのオオカミは死にます。 初期状態では、すべてのオオカミはH0ポイントを持っています。 問題のパラメーターはわずかに異なる可能性があり、多くの場合修正がありました。たとえば、少なくともオオカミの「正直な」有性生殖ですが、本質は同じままでした。



このモデルは私にとって非常に興味深いものであり、少なくとも何らかの形で実装したいと考えました。 しかし、たまたま手が届きました。 しかし、今では、オオカミとノウサギの両方の行動を「幻想化」するなど、もっと面白いことができました。 この記事では、複雑で生態系のバランスをとる、厄介だが非常に興味深い方法についてお話したいと思います。



初期パラメーターの選択



第一に、私は正方形のフィールドに限定されることを望まず、長方形のフィールドを基礎として取りました。 もちろん、六角形を作るというアイデアがありましたが、できるだけ早く画面に何かを生かしたかったので、古典的な長方形のグリッドと4つの可能な方向が最も成功した選択肢のようでした。



次に、ノウサギとオオカミの両方の人口がサポートされるために決定する必要がありました。 動物界のように、生物の繁殖はこの役割を担います。 最初のバージョンでは、選択はノウサギの無性生殖とオオカミの有性生殖にかかっていました。 最初のケースでは、ある程度の確率で、ウサギの各ターンがそのコピーを生成します(タスクで最初に説明したように)が、2番目のケースは実装においてはるかに興味深いものです。 オオカミは女性と男性の2つの性別であると想定されていましたが、他の違いはまったくありませんでした。 アルゴリズムは非常に単純でした。異性の2つのオオカミが同じセルで出会った場合、新しいランダムなオオカミが単純に生成されました。



ここで、制御されていない生物の繁殖を防ぐ方法を決定する必要があります。 ノウサギの個体群はオオカミの個体群によって制御されると想定されていましたが、そのために、元の問題では空腹モデルが提案されています。 このモデルによると、オオカミは動きごとに満腹感の特定の「ポイント」を失い、同じケージでノウサギを食べることでそれらを補充し、満腹感がゼロになったときに死にます。



生き物の行動のモデルを確立することだけが残っています。 最も簡単な方法は、最初の行動を取ることでした。うさぎは、空間内でランダムに移動する肉片であり、オオカミは、うさぎに気付くまで、狙いを定めずにさまよっています。



Java 8が実装言語として選択されたのは、最も馴染みのあるテクノロジーとオブジェクト指向言語がそのようなモデルに完全に適していたからです。 結論として、標準Javaツールのグラフィカルプリミティブを使用しました。それにもかかわらず、モデル自体はそのプレゼンテーションよりもはるかに興味がありました。



最初のパンケーキ



はい、彼はゴツゴツしていました。 当初、エコシステムは初めて機能するように見えましたが、機能しませんでした。 私が最初に遭遇した問題は、オオカミの生存率が低いことでした。オオカミには常に繁殖する時間がありませんでした。 はい、彼らはウサギを狩りましたが、異性への関心は彼らにありませんでした。 たまに、2匹のオオカミが隣のケージに衝突し、3匹目のオオカミが産卵し、森の中をあてもなく歩き続けました。 そこで、オオカミを地図に追加することにしました。 オオカミの大きな群れが森に最初に打ち上げられたとき、生態系がどれほど不安定であるかが示されました。 オオカミのように推論しましょう(明確さ-男性):私たちは森の中をさまよい、野ウサギに出会ったら食べます。 女性に会ったら、自分の小さなコピーを作成します。 そして、次の動きでは、別の小さなコピーがあります。女性は、おそらくどこにも逃げる時間がなかったからです。 さらに、ルールはパートナーへの排他的なアクセスを強制しませんでした。実際、同じオオカミまたはオオカミを使用して、1ターンに複数回、3番目のオオカミを生成できます。 最悪の事態は、新生児のオオカミがすぐに同じアルゴリズムに基づいて行動し始めたことです。 このような条件下では、オオカミの個体数の増加は雪崩のように起こりました。ウサギを食べる必要さえありませんでした。



protected void breed(Visibility visibility) { //    if (this.sex != Sex.FEMALE || !adult()) { return; } ... }
      
      





この調整はほとんど成功しないため、すぐにシステムを変更しようとしました。 次の制限が追加されました。



  1. 男性と会ったときに、ランダムな性別の小さなオオカミを生成したのは、彼女だけです(少なくとも2つの新しいオオカミが以前に現れていた)。
  2. 「年齢」と「大人」の概念が導入されました。オオカミは特定の年齢からしか繁殖できませんでした。 オオカミとノウサギの両方が死にました。 この制限は、人口の爆発を防ぐために導入されました。
  3. 「可視性」の概念が導入されました。オオカミは特定の半径内のオブジェクトのみを分析できました(これまではケージ上で)が、外部のオブジェクトについては何も知りませんでした。

  4. ノウサギは、人口を維持する手段として繁殖効率の低下を補うためにより多くの「飽和点」を与え始めました。


私は興味をそそりません-これは十分ではありませんでした。 ノウサギの誕生の可能性により、種は消滅せず、一定の値での年齢(ノウサギの発生確率に応じて)により、上からそれらを制限することが可能になりました。 しかし、オオカミの数が少なすぎたために、オオカミは絶滅し始めました。 システムは不安定で、初期パラメータに大きく依存していました。 実際のエコシステムをモデル化するには、間違いなく修正が必要でした。



バランス調整



オオカミに「妊娠」を追加することが決定されました。 メカニズムは、新しいオオカミがすぐにではなく、一定数の動きの後に現れたという事実にありました。 「妊娠」の間、彼女は再び妊娠することができませんでした。これは、人口爆発の影響を排除するはずでした。



しかしそれにもかかわらず、オオカミの生存は(他の生き物と同様に)行動のモデルに強く依存しています。 したがって、オオカミの動作の簡単な実装が追加されました。 このため、「可視性」は非常に有用であることが判明しました。分析のために世界のオブジェクトの一部を特定のクリーチャーに転送する場合です。



オオカミは野ウサギから草を保護し始めます


ブラウンスクエア -無性ノウサギ

青い円 -オオカミ

黄色の円 -オオカミ



クリーチャーの動きはより複雑になります。リファクタリングが必要です。 これで、クリーチャーのコースのフェーズが明確に区別されました。





オオカミの行動「合理的」



このシステムでのクリーチャーの動作は、選択を行う必要があるすべての基本的なアクションを担当する一連の機能です。 そのとき、オオカミの選択は、動きの方向(移動)によってのみ決定されます。



動物の選択に影響を与えず自動的に発生する動物の基本機能(更新)を、理想的にはアルゴリズムを開発する過程でより高度なバージョンに置き換える必要がある行動機能から分離するのは良い考えのようです。 したがって、「合理的な」オオカミの最初のバージョンが表示されます。 さらに議論するために、「関心のあるオブジェクト」の概念を紹介します。これは、潜在的に食べられる可能性がある、または生殖が可能な可視性の分野の生き物です。 「インテリジェントな」オオカミは次のように行動しました。



  1. 可能なすべての移動方向が差し引かれます。
  2. オオカミが食べたいかどうかを判断します(健康が半分以下の場合)。
  3. オオカミが食べたい場合は、目に見えるオブジェクトの中から最も近い関心のあるオブジェクトが選択され、それを食べることができます(複数ある場合は、最初のオブジェクト)。
  4. オオカミが食べたくない場合は、目に見えるオブジェクトの中から最も近い関心のあるオブジェクトが選択され、それを使用して繁殖することができます。
  5. 選択した対象オブジェクトへの移動方向を返します。
  6. 関心のあるオブジェクトがない場合は、可能なオブジェクトからランダムな移動方向が返されます。


このスキームにより、オオカミは自分の満腹感を維持するために、最も近い鳥をすべて1石でかなり素早く捕まえることができ、迷いながら異性にすぐにアクセスできるようになりました。 しかし、そのような世界は縮退しました。オオカミはパックの中に残り、時々通り過ぎるウサギを食べてゆっくりと死にましたが、ウサギは世界の反対側で免責され、その後捕獲されました。 制御されていない繁殖を防ぐために、ウサギに複雑なルールを導入する必要があることが明らかになりました。



そして、そのような繁殖の結果




ウサギの行動は「合理的」



次のバージョンでは、ノウサギは食べる欲求と生きる欲求を持ち、オオカミのようにセックスをします。 まず、「食べたい」という言葉を追加してみることにしました。

さて、うさぎが存在するためには、独自の健康スケールを維持する必要があります。 野ウサギは草を食べますが、草はゆっくりと、しかし確実に足元で成長します(太陽エネルギーを食べます)。 当初、このアイデアは、エリア内の食料ユニットの数が上から野ウサギの人口を制限するというものでした。 したがって、草の成長のパラメータを使用し、ノウサギの満腹感を減らすと、島での最大数のかなり正確な値を取得することが可能になります。



「食べることの欲望」を実現するために、最も近い捕食者からの方向を計算するための簡単なアルゴリズムを追加します。 しかし、これらのルールの下では、オオカミは決して単一のノウサギに追いつくことはありません;したがって、「スピード」の概念が導入されました-「移動」と「食べる」の完全なライフサイクルの数。 したがって、速度が2のかなり空腹のオオカミは、その可視領域に現れると、ほとんどの場合、速度1でノウサギに追いつきます。 オオカミが互いに干渉するのを防ぐために、同性のオオカミに向かって動きたくないというわずかな不本意がターゲット計算アルゴリズムに追加されました。



そして、ここで、現在のモデルを安定させる過程で、(すでに壊れやすい生態学的バランスをさらに打ち砕くために)匂いを加えるのに頭がかかりました。 臭いは、ノウサギがいるセルに残っています( Sの初期値)。この値を移動すると、この値は特定のdSだけゼロに減少します。 このようなメカニズムにより、オオカミの可視性を理論的に高めることができました。これは、もしそれがNの匂いのあるセル上にある場合、これは、 SN dS$ 細胞は彼を残したウサギです。



最初の10回の打ち上げでは、全体として何も変わっていないことが明らかになりました。野草の繁殖は食べる必要性と重なっています。 これは、うさぎの繁殖を異なる方法で複雑にする必要があることを意味します。これはオオカミと同じ繁殖ですが、わずかに異なるパラメータがあります:複数の子孫の可能性の増加とウサギの妊娠年齢の減少。 そのような変化は、野ウサギにとってより大きな「合理性」を必要とします。なぜなら、野ウサギは今、捕食者から逃げて繁殖する必要があるからです。 同様のアルゴリズムがすでにオオカミに使用されており、食物、捕食者、パートナー、および競合他社の体重を再定義する必要があります。



しかし、負の重みは、正の重みと一緒に期待どおりに動作することを望みません。 そのため、各セルの重みを降順で隣接セルに配分する必要があります。 これで、中心点がクリーチャーの可視領域に配置され、距離とともに減少する効果が広がります(現在の実装では各ステップで1)。 その結果、スケールのマップが作成されます。このマップに従って、クリーチャーは正の穴に「転がり」、目的を達成します。



 //    static { VALUE_MAP.put(RegularRabbitTargetAttitude.PREDATOR, -20); VALUE_MAP.put(RegularRabbitTargetAttitude.COMPETITOR, -5); VALUE_MAP.put(RegularRabbitTargetAttitude.MATE, 10); VALUE_MAP.put(RegularRabbitTargetAttitude.FOOD, 10); } public Optional<Position> move(Visibility visibility) { //     Map<Position, Set<RegularRabbitTargetAttitude>> positionValues = updateWithUnits(visibility.units()); //     HashMap<Position, Integer> positionValueMap = positionValues.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, e -> e.getValue().stream() .mapToInt(VALUE_MAP::get).sum(), Integer::sum, HashMap::new)); //     Map<Position, Integer> predatorValueMap = updateProliferatingValues(visibility, PREDATOR, positionValues); //        Map<Position, Integer> competitorValueMap = updateProliferatingValues(visibility, COMPETITOR, positionValues); //     () Integer foodCellValue = VALUE_MAP.get(FOOD); Map<Position, Integer> cellValues = visibility.cells() .collect(Collectors.toMap( Cell::getPosition, c -> c.getGrass().getFoodCurrent() >= c.getGrass().getFoodValue() ? foodCellValue : 0)); Map<Position, Integer> totalValueMap = CollectionUtils.mergeMaps( Integer::sum, predatorValueMap, competitorValueMap, positionValueMap, cellValues); return totalValueMap.entrySet().stream() .max(Comparator.comparingInt(Map.Entry::getValue)) .map(max -> { List<Direction> availableDirections = Direction.shuffledValues() .filter(dir -> !getPosition().by(dir) .adjustableIn( 0, 0, visibility.getWidth(), visibility.getHeight())) .collect(Collectors.toList()); return getPosition() .inDirectionTo(max.getKey(), availableDirections); }); } private Map<Position, Integer> updateProliferatingValues( Visibility visibility, RegularRabbitTargetAttitude key, Map<Position, Set<RegularRabbitTargetAttitude>> positionValues ) { List<Position> negativePositions = positionValues.entrySet().stream() .filter(e -> e.getValue().contains(key)) .map(Map.Entry::getKey) .collect(Collectors.toList()); Integer epicenterValue = VALUE_MAP.get(key); return negativePositions.stream() .map(position -> (Map<Position, Integer>) visibility.cells() .map(Cell::getPosition) .collect(Collectors.toMap( Function.identity(), pos -> epicenterValue + position.distance(pos), Integer::sum, HashMap::new))) .reduce((map1, map2) -> CollectionUtils.mergeMaps(Integer::sum, map1, map2)) .orElse(Collections.emptyMap()); } private Map<Position, Set<RegularRabbitTargetAttitude>> updateWithUnits( Stream<LivingUnit> units ) { Map<Position, Set<RegularRabbitTargetAttitude>> positionValues = new HashMap<>(); Map<Position, List<LivingUnit>> positionUnits = units.collect(Collectors.groupingBy(LivingUnit::getPosition)); positionUnits.forEach((key, value) -> value.stream() .map(InterestUnit::new) .forEach(iu -> { if (iu.asPredator != null) { positionValues.computeIfAbsent(key, (pos) -> new HashSet<>()).add(PREDATOR); } if (iu.asMate != null) { RegularRabbitTargetAttitude attitude = goodPartner(iu.asMate) ? MATE : COMPETITOR; positionValues.computeIfAbsent(key, (pos) -> new HashSet<>()).add(attitude); } })); return positionValues; }
      
      





そして、これがシステムが最終的に安定したまさにその時点でした。野ウサギの個体数はもはや制御不能に成長することはなく、オオカミは彼らの「愚かさ」のために死ぬことはありませんでした。 世界にはバランスがとれており、私はオブザーバーに追加のアメニティを最適化して実装することに着手しました。



ストーリーを「コンテキスト内」で表示したり、githubで実行および再生たりできます。



更新する GitHubのシンプルな設定既にアセンブルされたバージョン



All Articles