🎲

衛星ビジネス戦略をゲームにして考えてみる その1

2025/01/22に公開

モチベーション

昨年は色々と、衛星軌道、撮像にかんする基本事項を勉強してきました。今年はビジネス面の理解を深めたいと考えています。

衛星をつくって運用する会社のプレーヤー数は国内、国外問わずどんどん増加しています。
そのなかで勝ち残っていくためには、ビジネスの基本要素を理解して戦略をもって進めることが重要だと思います。
そして、すぐれた戦略をつくるためには、試行錯誤が必須だと思います。

個人的に、カタンというボードゲームが好きで、年末年始などは家族で1日中ゲームをしています。
基本的なルールは、最初に土地を押さえて、そこから得られるあらたな資源で道や街をつくり、あらたな土地を獲得していくというゲームです。
運と戦略のバランスが丁度良く、よく考えて次の一手を打つことがもとめられます。

衛星ビジネスも同じように、資源を先に抑えて、そこから収集、提供できる価値を最大化していくというゲームといえるのではないかと考えています。
自分自身が衛星ビジネスを深く理解し、シミュレーションできるように衛星ビジネス戦略ゲームを考えてつくって動かしていきたいと思います。

まずシンプルに動くものをつくって、そこから複雑化させて、より現実のビジネスに近づけていこうと思います。

衛星ビジネス(ゲーム)v0.1 要素検討

ざっと書き出してみるといかのようなルールがあると最低限のゲームとして成立しそうです。

資源

  • お金(現金+負債)
  • 衛星の基数
  • 衛星データを買ってくれるポテンシャルカンパニー数

基本的な勝敗ルール

  • userの現金がマイナスの値になったときはゲームオーバー
  • 0 はまだ継続できる
  • 20アクションおこなったらゲーム終了
  • 現金から負債を引いた数が最も大きいユーザーが勝者となる

ユーザーの取れるアクション

  • 1:お金の借り入れ
  • 2:新規顧客営業
  • 3:衛星データ撮像
  • 4:新規衛星の打ち上げ

リワード

  • 1:お金の借り入れ を選択すると
    • Cash, Debtがある値(例:1-5) の範囲で同じ数だけ増える
    • Debtがおおすぎると借り入れに失敗して利子だけが加算してDebtだけが増えることもランダムに発生する、
  • 2:新規顧客営業 を選択すると
    • そのときのSatの数に比例して、DealCompanyがある値(例:1-9)の範囲で増える
    • その分potentialUserCompanyの数は減少する
  • 3:衛星データ撮像 を選択すると
    • DealCompanyの数におうじて Cashがある値 (例:1-20)の範囲でふえる
    • ごくまれに撮像が失敗する。
  • 4:新規衛星の打ち上げを選択すると
    • Cashがある値(例:5-15)の間でランダムにへり、Sat数が1増える。
    • Cashが足りないときは何も変化せず、Sat数も増えない。
    • 時々打ち上げに失敗してCashだけ減ってSatが増えない時がある

実装 (GraphAI)

さて、いままでだと上記をつくるのに自分でプログラムをかいてとなっていたと思うのですが、最初からGPT等の生成AIをゲームエンジンとして使うと決めていると、ちがった作り方になるということをSingularitySocietyのなかで学びました。 このゲームの実装ではそれを取り込んでいきたいと思います。

生成AIをくみ混む際に、モノリシックなシステムではなく、単目的に特化したAgent同士が連携して複雑な処理を実行するということがよく行われています。

それらを抽象度をあげてAgentのGraphという形で Yaml or Jsonであらわして、それをGraphAIというグラフ実行エンジンで動作させるということが可能になります。詳細はTutorialを参照。

参照グラフ

今回は上記Tutorialの中から一番データの流れが似ているChatをベースに作っていきたいと思います。

version: 0.5
loop:
  while: :continue
nodes:
  continue:
    value: true
    update: :checkInput
  messages:
    value: []
    update: :llm.messages
    isResult: true
  userInput:
    agent: textInputAgent
    params:
      message: "You:"
      required: true
  checkInput:
    agent: compareAgent
    inputs:
      array:
        - :userInput.text
        - "!="
        - /bye
  llm:
    agent: openAIAgent
    params:
      model: gpt-4o
    inputs:
      messages: :messages
      prompt: :userInput.text
  output:
    agent: stringTemplateAgent
    console:
      after: true
    inputs:
      text: "\e[32mAgent\e[0m: ${:llm.text}"

上記をテキストファイル"chat.yaml"にコピーして
カレントフォルダの.envにOPENAI_API_KEY=XXX で自分のAPI_KEYを設定し

npm i -g  @receptron/graphai_cli
graphai chat.yaml

で実行できます。

ゲームの要素をAgent化する

Static Node

プロパティは、いかのようにValueの値だけをもつStaticNodeとして定義します

  • gameState
        "gameState": {
        "value": {
          "userSat": 0,
          "userCash": 0,
          "userDebt": 0,
          "userDealCompany": 0,
          "npcSat": 0,
          "npcCash": 0,
          "npcDebt": 0,
          "npcDealCompany": 0,
          "potentialUserCompany":100,
          "turn":0
        },
    

リワード Agent

ゲームの面白さのコアの部分です。かきのようにStaticとComputeを組み合わせて定義します

 "actions": {
      "value": "1:お金の借り入れ 2:新規顧客営業 3:衛星データ撮像 4:新規衛星の打ち上げ"
    },
    "actionsBasicRule": {
      "value": "1:お金の借り入れ を選択するとCash, Debtが1-5 の範囲で同じ数だけ増える、Debtがおおすぎると借り入れに失敗して利子だけが加算してDebtだけが増えることもランダムに発生する、 2:新規顧客営業 を選択するとそのときのSatの数に比例して、DealCompanyが1-9の範囲で増える その分potentialUserCompanyの数は減少する 3:衛星データ撮像 を選択すると DealCompanyの数におうじて Cashが 1-20の範囲でふえる ごくまれに撮像が失敗する。  4:新規衛星の打ち上げ は Cashが5-15の間でランダムにへり、Sat数が1増える。Cashが足りないときは何も変化せず、Sat数も増えない。時々打ち上げに失敗してCashだけ減ってSatが増えない時がある" 
    },
    "actionAgentPrompt": {
      "agent": "stringTemplateAgent",
      "inputs": {
        "actions": ":actions",
        "userInput": ":userInput.text",
        "basicRule": ":actionsBasicRule",
        "gameState": ":gameStateStr",
        "schema": ":stateSchema"
      },
      "params": {
        "template": "${actions}  user selected ${userInput} におうじてポイントの増減を考えてください。基本原則は ${basicRule} のとおりです。現在の状態は ${gameState} です。 下記のスキーマでpointの増減をよく考えて正しいJsonのフォーマットでそれぞれの増減する値をJSON部分のみ応答してください。そのポイントにした理由はDescriptionにゲームユーザーがワクワクするような表現でまとめて記載してください。 ${schema}"      
      }
    },

GameJudge Agent

ゲームの終了、勝敗を判定するAgentです

    "gameBasicRule": {
      "value": "userCashがマイナスの値になったときはゲームオーバー 0 はまだ継続できる npcCashがマイナスになったとしてもゲームは継続する,  turn が 20になったらゲーム終了 CashからDebtを引いた数が最も大きいユーザーが勝者となる" 
    },
    "gameJudgePrompt": {
      "agent": "stringTemplateAgent",
      "inputs": {
        "gameBasicRule": ":gameBasicRule",
        "gameState": ":newGameState"
      },
      "params": {
        "template": "${gameBasicRule}にそって判断する。 現在の状態は ${gameState} です。 ゲームオーバーもしくはゲームエンドの場合は end:true, 続行の場合はend:false を下記のスキーマでJSON部分のみ応答してください。終了状況をDescriptionにゲームを総括する冷静なビジネス運営分析評論家として厳しくまとめて記載してください。{\"end\":true, \"description\":string}"      
      }
    },  
    "gameJudgeAgent": {
      "agent": "openAIAgent",
      "params": {
        "model": "gpt-4o"
      },
      "inputs": {
        "messages": [],
        "result": ":actionCalc",
        "prompt": ":gameJudgePrompt"
      }
    },

間をむすぶAgent

ChatGPTの応答はこのままだとTextなので、それをJsonに変更して
もとのgameState Jsonのポイントと計算するためのAgentが下記です。
gameState に update:actionCalc と設定することでactionCalcの内容で更新されます。

    "actionsResult": {
      "agent": "jsonParserAgent",
      "inputs": {
        "text": ":actionAgent.text"
      }
    },
    "actionCalc": {
      "agent": "totalAgent",
      "console": {
        "after": true
      },
      "inputs": {
        "array": [
          ":actionsResult",
          ":npcResult",
          ":gameState"
        ]
      }
    },
//再掲
    "gameState": {
      "value": {
        "userSat": 0,
        "userCash": 0,
        "userDebt": 0,
        "userDealCompany": 0,
        "npcSat": 0,
        "npcCash": 0,
        "npcDebt": 0,
        "npcDealCompany": 0,
        "potentialUserCompany":100,
        "turn":0
      },
      "update": ":actionCalc"
    },

ユーザーからのアクション入力を得る部分はChatと同じです。

    "userInput": {
      "agent": "textInputAgent",
      "params": {
        "message": "You: Please select your action from ${:actions}",
        "required": true
      }
    },

実際の動作確認

% graphai graphai/sample.json
✔ You: Please select your action from 1:お金の借り入れ 2:新規顧客営業 3:衛星データ撮像 4:新規衛星の打ち上げ

ここでいきなり新規衛星をうちあげると、、 はなばなしく終了してしまうので、
まずは無難に1を選択します。

✔ You: Please select your action from 1:お金の借り入れ 2:新規顧客営業 3:衛星データ撮像 4:新規衛星の打ち上げ 1
{
  "userSat": 0,
  "userCash": 3,
  "userDebt": 3,
  "userDealCompany": 0,
  "npcSat": 0,
  "npcCash": 3,
  "npcDebt": 3,
  "npcDealCompany": 0,
  "potentialUserCompany": 100,
  "turn": 2,
  "description": "お金の借り入れに成功しました!キャッシュが増え、未来への投資の準備が整いました。しかし、借り入れた分だけの借金も背負うことに。次の一手を考え、賢く進めていきましょう!NPC chose to borrow money, increasing both Cash and Debt by 3."
}

中略、お金をためてから衛星の打ち上げを選択

✔ You: Please select your action from 1:お金の借り入れ 2:新規顧客営業 3:衛星データ撮像 4:新規衛星の打ち上げ 4
{
  "userSat": 1,
  "userCash": 13,
  "userDebt": 25,
  "userDealCompany": 0,
  "npcSat": 0,
  "npcCash": 9,
  "npcDebt": 33,
  "npcDealCompany": 0,
  "potentialUserCompany": 100,
  "turn": 12,
  "description": "あなたは新しい衛星の打ち上げに成功しました!空に舞い上がる衛星は、未知の可能性への扉を開きます。キャッシュは少し減りましたが、それ以上に得られるものは大きいでしょう。今、あなたの未来は星々と共に輝いています!この成功を糧に、さらに多くの夢を追いかけましょう!お金の借り入れに成功し、あなたのキャッシュが膨らみました!新たな資金は、宇宙の可能性を手に入れるための強力な武器となります。星々の間を自由に駆け巡る夢が、ますます現実味を帯びてきました!一方で、NPCは再び借り入れに失敗し、利子によって借金が増えてしまいました。あなたの優位性が際立ち、次の冒険が待ち遠しくなります!この挑戦を乗り越え、未知の世界を探検する準備を整えましょう!あなたはお金の借り入れに成功し、キャッシュと共に新たな可能性を手にしました!キャッシュが増え、未来への投資の準備が整いました。しかし、借り入れた分だけの借金も背負うことに。次の一手を考え、賢く進めていきましょう!NPCは高い借金のため借り入れに失敗し、利子により借金が増加しました。

やったー 無事打ち上げられた。
さっそく営業だ

✔ You: Please select your action from 1:お金の借り入れ 2:新規顧客営業 3:衛星データ撮像 4:新規衛星の打ち上げ 2
{
  "userSat": 1,
  "userCash": 13,
  "userDebt": 25,
  "userDealCompany": 2,
  "npcSat": 0,
  "npcCash": 9,
  "npcDebt": 36,
  "npcDealCompany": 0,
  "potentialUserCompany": 98,
  "turn": 13,
  "description": "新規顧客営業に成功し、あなたの衛星が持つ可能性を顧客に伝えることができました!その結果、2社の新たな取引先を獲得し、あなたのビジネスはさらに拡大しています。これにより、未知の宇宙市場に大きな一歩を踏み出しました。今こそ新たな夢を追いかけ、さらに多くの星々への道筋を描きましょう!

よし、ついに販売だ!

✔ You: Please select your action from 1:お金の借り入れ 2:新規顧客営業 3:衛星データ撮像 4:新規衛星の打ち上げ 3
{
  "userSat": 1,
  "userCash": 28,
  "userDebt": 25,
  "userDealCompany": 2,
  "npcSat": 1,
  "npcCash": -2,
  "npcDebt": 36,
  "npcDealCompany": 0,
  "potentialUserCompany": 98,
  "turn": 14,
  "description": "衛星データ撮像に成功しました!あなたの取引先が増えたおかげで、キャッシュが大幅に増えました。この成功は、あなたの事業をさらに拡大するための大きな一歩です。今こそ、宇宙市場での影響力をさらに高め、夢を追いかける時が来ました。星々の間を駆け巡る未来が、あなたの手の中にあります!

ついにCashが、Debtを上回りました! 胸が熱いです。
20 turn 終了時のメッセージ

{
  "gameResult": {
    "end": true,
    "description": "ゲームは終了しました。最終ターンである20ターン目に達し、ゲームの結果が確定しました。ユーザーは衛星データ撮像の成功によりキャッシュを増加させ、事業の拡大に成功しましたが、負債も増加し、慎重な資金管理が求められる状況です。一方、NPCは負債の増加に悩まされ、キャッシュフローの改善に苦しむ姿が見受けられました。総じて、宇宙市場における事業の成功には、攻めの姿勢と共に、財務の安定性を確保するための堅実な戦略が不可欠であることを示しています。ユーザーの未来は明るいものの、次なる挑戦に向けた計画が必要です。"
  }
}

まとめ

こんなに短い定義ファイルだけで、ゲームがなんとか成立するということは原理検証できました。(gpt+graphai すごい!)
これからビジネス要素を追加していくのと、他のユーザーとハイスコアを競えるような仕組みとして公開したいです。 そのためにはLLMの部分をやすいものに置き換えないと大赤字ですが。

以下、Graph全体です。GPTだと動かし続けるとお金がかかるのでご注意を。

{
  "version": 0.5,
  "loop": {
    "while": ":continue"
  },
  "nodes": {
    "continue": {
      "value": true,
      "update": ":gameEnd"
    },
    "gameBasicRule": {
      "value": "userCashがマイナスの値になったときはゲームオーバー 0 はまだ継続できる npcCashがマイナスになったとしてもゲームは継続する,  turn が 20になったらゲーム終了 CashからDebtを引いた数が最も大きいユーザーが勝者となる" 
    },
    "gameState": {
      "value": {
        "userSat": 0,
        "userCash": 0,
        "userDebt": 0,
        "userDealCompany": 0,
        "npcSat": 0,
        "npcCash": 0,
        "npcDebt": 0,
        "npcDealCompany": 0,
        "potentialUserCompany":100,
        "turn":0
      },
      "update": ":actionCalc"
    },
    "stateSchema": {
      "value":  "{\"userSat\": 0, \"userCash\": 0, \"userDebt\": 0, \"userDealCompany\": 0,  \"npcSat\": 0, \"npcCash\": 0, \"npcDebt\": 0, \"npcDealCompany\": 0, \"potentialUserCompany\": 0, \"turn\": 1, \"description\":string}"
    },
    "gameStateStr": {
      "agent":"jsonParserAgent",
      "inputs": {
        "data": ":gameState"
      }
    },
    "actions": {
      "value": "1:お金の借り入れ 2:新規顧客営業 3:衛星データ撮像 4:新規衛星の打ち上げ"
    },
    "actionsBasicRule": {
      "value": "1:お金の借り入れ を選択するとCash, Debtが1-5 の範囲で同じ数だけ増える、Debtがおおすぎると借り入れに失敗して利子だけが加算してDebtだけが増えることもランダムに発生する、 2:新規顧客営業 を選択するとそのときのSatの数に比例して、DealCompanyが1-9の範囲で増える その分potentialUserCompanyの数は減少する 3:衛星データ撮像 を選択すると DealCompanyの数におうじて Cashが 1-20の範囲でふえる ごくまれに撮像が失敗する。  4:新規衛星の打ち上げ は Cashが5-15の間でランダムにへり、Sat数が1増える。Cashが足りないときは何も変化せず、Sat数も増えない。時々打ち上げに失敗してCashだけ減ってSatが増えない時がある" 
    },
    "userInput": {
      "agent": "textInputAgent",
      "params": {
        "message": "You: Please select your action from ${:actions}",
        "required": true
      }
    },
    "messages": {
      "value": [],
      "update": ":actionAgent.messages"
    },
    "actionAgentPrompt": {
      "agent": "stringTemplateAgent",
      "inputs": {
        "actions": ":actions",
        "userInput": ":userInput.text",
        "basicRule": ":actionsBasicRule",
        "gameState": ":gameStateStr",
        "schema": ":stateSchema"
      },
      "params": {
        "template": "${actions}  user selected ${userInput} におうじてポイントの増減を考えてください。基本原則は ${basicRule} のとおりです。現在の状態は ${gameState} です。 下記のスキーマでpointの増減をよく考えて正しいJsonのフォーマットでそれぞれの増減する値をJSON部分のみ応答してください。そのポイントにした理由はDescriptionにゲームユーザーがワクワクするような表現でまとめて記載してください。 ${schema}"      
      }
    },
    "actionAgent": {
      "agent": "openAIAgent",
      "params": {
        "model": "gpt-4o"
      },
      "inputs": {
        "messages": [],
        "prompt": ":actionAgentPrompt"
      }
    },
    "actionsResult": {
      "agent": "jsonParserAgent",
      "inputs": {
        "text": ":actionAgent.text"
      }
    },
    "npcAgentPrompt": {
      "agent": "stringTemplateAgent",
      "inputs": {
        "actions": ":actions",
        "basicRule": ":actionsBasicRule",
        "gameState": ":gameStateStr",
        "schema": ":stateSchema"
      },
      "params": {
        "template": "${actions}  npc は 現在のGame状況におうじて最適な選択を選択を一つするとします。  npcのポイントの増減を考えてください。基本原則は ${basicRule} のとおりです。現在の状態は ${gameState} です。 Cashがマイナスのときは何もできずにターンを終了して、ポイントの増減はありません。 下記のスキーマでpointの増減をよく考えて正しいJsonのフォーマットでそれぞれの増減する値をJSON部分のみ応答してください、npcの場合はturn:0 (turnは増減しない)とします。${schema}"      
      }
    },
    "npcAgent": {
      "agent": "openAIAgent",
      "params": {
        "model": "gpt-4o"
      },
      "inputs": {
        "messages": [],
        "prompt": ":npcAgentPrompt"
      }
    },
    "npcResult": {
      "agent": "jsonParserAgent",
      "inputs": {
        "text": ":npcAgent.text"
      }
    },
    "actionCalc": {
      "agent": "totalAgent",
      "console": {
        "after": true
      },
      "inputs": {
        "array": [
          ":actionsResult",
          ":npcResult",
          ":gameState"
        ]
      }
    },
    "newGameState": {
      "agent": "jsonParserAgent",
      "inputs": {
        "data": ":actionCalc"
      }
    },

    "gameJudgePrompt": {
      "agent": "stringTemplateAgent",
      "inputs": {
        "gameBasicRule": ":gameBasicRule",
        "gameState": ":newGameState"
      },
      "params": {
        "template": "${gameBasicRule}にそって判断する。 現在の状態は ${gameState} です。 ゲームオーバーもしくはゲームエンドの場合は end:true, 続行の場合はend:false を下記のスキーマでJSON部分のみ応答してください。終了状況をDescriptionにゲームを総括する冷静なビジネス運営分析評論家として厳しくまとめて記載してください。{\"end\":true, \"description\":string}"      
      }
    },  
    "gameJudgeAgent": {
      "agent": "openAIAgent",
      "params": {
        "model": "gpt-4o"
      },
      "inputs": {
        "messages": [],
        "result": ":actionCalc",
        "prompt": ":gameJudgePrompt"
      }
    },
    "gameResult": {
      "agent": "jsonParserAgent",
      "isResult": true,
      "inputs": {
        "text": ":gameJudgeAgent.text"
      }
    },      
    "gameEnd": {
      "agent": "compareAgent",
      "console": {
        "after": true
      },
      "inputs": {
        "array": [
          ":gameResult.end",
          "!=",
          true
        ]
      }
    },    
    "output": {
      "agent": "copyAgent",
      "params": {
        "namedKey": "text"
      },
      "inputs": {
        "text": "${:gameJudgeAgent.text}"
      }
    }
  }
}
シンギュラリティ・ソサエティ

Discussion