iT邦幫忙

2024 iThome 鐵人賽

DAY 20
0

Current Sprint: 3. 遊戲基本流程完成
repo: https://github.com/side-project-at-SPT/ithome-ironman-2024-san-juan
swagger docs: https://side-project-at-spt.github.io/ithome-ironman-2024-san-juan/

前情提要

  • 建立 Games::PlayerCommand // 玩家行動
  • 建立 Games::ChooseRoleCommand < PlayerCommand // 選擇職業行動

sprint 3 遊戲基本流程完成

  • 建築卡片資料
  • 建築卡片功能實作
  • 選職業:開始職業階段
    • 🚧 選完職業,更新遊戲狀態
      • 更新 controller
      • 更新測試,產生 swagger
    • 判斷有沒有什麼動作可以由系統接續自動執行
    • 如果有,回傳 202
    • 如果沒有,回傳 204
  • 執行 礦工 階段行動
  • 執行 議員 階段行動
  • 執行 建築 階段行動
  • 執行 生產 階段行動
  • 執行 交易 階段行動
  • 行動結束,更換目前玩家
  • 回合開始
  • 遊戲結束

今天要做

接收到 assign 選擇職業 請求時

  • 建立 choose_role_command
  • 檢查是否建立成功 // 400 if fail
  • 執行 choose_role_command
  • 檢查是否執行成功 // 422 if fail
  • 執行 post action(s)
  • 通知下一位玩家
  • api 回傳目前遊戲狀態 // 200
  1. 建立 choose_role_command
# app/commands/games/choose_role_command.rb

module Games
  class ChooseRoleCommand < PlayerCommand
    attr_reader :role

    def initialize(params = {})
      super(params)
      @role = validate_role!(params[:role])
    end

    def call
      if errors.any?
        game.errors.add(:base, errors.full_messages.join(", "))
        return game
      end

      role_is_taken?(role, game.game_data["roles"]) do
        errors.add(:role, "#{role.demodulize} is being taken") and return self
      end

      game.game_data["roles"].delete(role)
      game.game_data["players"][game.game_data["current_player_index"]]["role"] = role
      game.save

      # 判斷下一個動作要做什麼
      case role
      when Roles::Prospector.to_s
        # 判斷玩家是否有金礦或金工坊
        # 如果有,則讓玩家選擇要先執行哪個動作
        # 如果沒有,則直接執行礦工的動作
        player_buildings = game.game_data["players"][game.game_data["current_player_index"]]["buildings"]

        # if player_buildings.any? { |building| building["id"] == Cards::GoldMine.id || building["id"] == Cards::GoldSmithy.id }
        if player_buildings.any? { |building| building["id"] == "07" || building["id"] == "38" }
          # TODO: implement this
          puts "TODO: implement this (choose action)"
        else
          # 從牌庫抽取一張卡片
          @post_action = [
            Games::DrawCommand,
            {
              player_id: player.id,
              number: 1,
              description: "選擇礦工玩家抽一張卡片"
            }
          ]
        end
      else
        pp "Unimplemented role: #{role}"
      end

      self
    end

    class InvalidRoleError < RuntimeError
      def initialize(invalid_role = nil)
        @invalid_role = invalid_role
      end

      def message
        "Invalid Role: #{@invalid_role}"
      end
    end

    private

    def role_is_taken?(role, roles, &block)
      unless role.in? roles
        if block_given?
          block.call
        else
          raise InvalidRoleError.new(role)
        end
      end
    end

    def validate_role!(params_role = nil)
      if (role = find_role(params_role))
        role
      else
        errors.add(:role, "#{params_role} is invalid")
        nil
      end
    end

    def find_role(value)
      return nil unless value

      case value.to_s.downcase
      when "1", "建築師", "builder"
        Roles::Builder.to_s
      when "2", "製造商", "producer"
        Roles::Producer.to_s
      when "3", "貿易商", "trader"
        Roles::Trader.to_s
      when "4", "礦工", "prospector"
        Roles::Prospector.to_s
      when "5", "議員", "councillor"
        Roles::Councillor.to_s
      else
        nil
      end
    end
  end
end

game 新增 build_assign_role_command 實體方法,產生 assign action 需要的 command

# app/models/game.rb
class Game < ApplicationRecord
# ...

  def build_assign_role_command(**params)
    command_builder(Games::ChooseRoleCommand, params.merge(description: "選擇職業"))
  end
  
  private
  
  # 產生任意 command
  # @param command [Class] command class
  # @param params [Hash] command parameters
  # @return [Games::PlayerCommand] command instance
  def command_builder(command, params)
    command.new(params.merge(game: self))
  end
end
  1. 檢查是否建立成功
# app/controllers/api/v1/games_controller.rb

class Api::V1::GamesController < ApplicationController
# ...
  def assign
    # ...
    command = @game.build_assign_role_command(role: params[:role], player: @current_player)
    if command.errors.any?
      return render status: :bad_request, json: { error: command.errors.full_messages }
    end
    # ...
  end
# ...
end
  1. 執行 choose_role_command,檢查是否執行成功
# app/controllers/api/v1/games_controller.rb

    result = command.call
    if result.errors.any?
      return render status: :unprocessable_entity, json: { error: result.errors.full_messages }
    end
  1. 執行 post action(s)

執行接下來不需要玩家選擇,而可以自動執行的動作

# app/controllers/api/v1/games_controller.rb

    # resolve the rest of the action can be done automatically
    result.resolve_post_action(game: @game)
  1. 通知下一位玩家 🚧
# app/controllers/api/v1/games_controller.rb

    # notify the next player to take action
    # TODO: implement this
    @game.notify_next_turn
  1. api 回傳目前遊戲狀態

新增 show action
新增 show.json.jbuilder

# app/controllers/api/v1/games_controller.rb

  def show; end

  def assign
    # ...
    @message = "你選擇了: #{params[:role]}"
    render :show
  end
# app/views/api/v1/games/show.json.jbuilder

json.ignore_nil!
json.partial! "api/v1/games/game", game: @game
json.message @message
  1. 測試看看
rails rswag # 更新 swagger

建立遊戲

curl -X 'POST' \
  'http://localhost:3000/api/v1/games' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "seed": "1234567890abcdef"
}'

回應

{
  "id": 7,
  "status": "playing",
  "game_config": {
    "seed": "1234567890abcdef"
  },
  "game_data": {
  // 略

選擇職業(礦工)

curl -X 'POST' \
  'http://localhost:3000/api/v1/games/7/roles/%E7%A4%A6%E5%B7%A5' \
  -H 'accept: application/json' \
  -d ''

回應

{
  "id": 7,
  "status": "playing",
  "game_config": {
    "seed": "1234567890abcdef"
  },
  "game_data": {
    "current_price": [
      1,
      2,
      2,
      2,
      3
    ],
    "supply_pile": ["00",...],
    "current_player_index": 0,
    "roles": [
      "Games::Roles::Builder",
      "Games::Roles::Producer",
      "Games::Roles::Trader",
      "Games::Roles::Councillor"
    ],
    "players": [
      {
        "id": 1,
        "hand": [
          "00",
          "00",
          "00",
          "00",
          "00"
        ],
        "buildings": [
          {
            "id": "01"
          }
        ]
      },
      {
        "id": 2,
        "hand": [
          "00",
          "00",
          "00",
          "00"
        ],
        "buildings": [
          {
            "id": "01"
          }
        ]
      },
      {
        "id": 4,
        "hand": [
          "01",
          "00",
          "00",
          "00"
        ],
        "buildings": [
          {
            "id": "01"
          }
        ]
      },
      {
        "id": 3,
        "hand": [
          "00",
          "00",
          "00",
          "00"
        ],
        "buildings": [
          {
            "id": "01"
          }
        ]
      }
    ]
  },
  "message": "你選擇了: 礦工"
}

收工

TODO

  • 建築卡片資料
    • 工廠建築卡片資料
    • 城市建築卡片資料
  • 建築卡片功能實作
  • 🆕 建立 step model 用來儲存遊戲(每一步)紀錄
  • 🆕 增加遊戲狀態 phase,用來描述目前是哪個職業階段
  • 選職業:開始職業階段
    • 🆕 更新目前玩家
    • 選完職業,更新遊戲狀態
      • 更新 controller
      • 更新測試,產生 swagger
    • 判斷有沒有什麼動作可以由系統接續自動執行
  • 執行 礦工 階段行動
  • 執行 議員 階段行動
  • 執行 建築 階段行動
  • 執行 生產 階段行動
  • 執行 交易 階段行動
  • 行動結束,更換目前玩家
  • 回合開始
  • 遊戲結束

明天要做什麼

  • 🆕 建立 step model 用來儲存遊戲(每一步)紀錄
  • 🆕 更新目前玩家
  • 🆕 增加遊戲狀態 phase,用來描述目前是哪個職業階段

以上不代表明天會做,如有雷同純屬巧合


工商服務

SPT (Side Project Taiwan) 的宗旨是藉由Side Project開發來成就自我,透過持續學習和合作,共同推動技術和專業的發展。我們相信每一個參與者,無論是什麼專業,都能在這個社群中找到屬於自己的成長空間。

歡迎所有對Side Project開發有興趣的人加入我們,可以是有點子來找夥伴,也可以是來尋找有興趣的Side Project加入,邀請大家一同打造一個充滿活力且有意義的技術社群!

Discord頻道連結: https://sideproj.tw/dc


上一篇
Day 19 - 選職業(2/n)
下一篇
Day 21 - 選職業(4/n)
系列文
透過實作網頁遊戲練習網站工程師的基本素養,以 San Juan(聖胡安) 為例。30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言