Current Sprint: 2. 實作遊戲開始
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/
參照 卡表 可以知道
基本版共有 110 張卡片,其中
工廠建築: 5 種 共 42 張
城市建築:24 種 共 68 張
先實作「染坊」卡片,並填上「空白牌」組成牌庫
卡片的屬性有:
{
"id": "01",
"name": "Indigo Plant",
"price": 1,
"type": "Production",
"score": 1,
"amount": 10
}
# app/models/cards/indigo_plant.rb
class Cards::IndigoPlant < Cards::BaseCard
class << self
def id
"01"
end
end
def name
"Indigo Plant"
end
def price
1
end
def type
"Production"
end
def score
1
end
def amount
10
end
end
# app/models/cards/blank_card.rb
class Cards::BlankCard < Cards::BaseCard
class << self
def id
"00"
end
end
def name
"Blank Card"
end
def price
1
end
def type
"Production"
end
def score
1
end
def amount
-1
end
end
# app/models/cards/base_card.rb
class Cards::BaseCard
# declare DECK_SIZE to fill the deck with blank cards
DECK_SIZE = 110.freeze
@@card_class = {}
class << self
def card_class
@@card_class
end
def <<(klass)
@@card_class[klass.id] = klass
end
def id
raise NotImplementedError
end
end
def name
raise NotImplementedError
end
def price
raise NotImplementedError
end
def type
raise NotImplementedError
end
def score
raise NotImplementedError
end
def amount
raise NotImplementedError
end
def id
self.class.id
end
def to_h
{
id: id,
name: name,
price: price,
type: type,
score: score,
amount: amount
}
end
end
class String
def to_card
if self.in? Cards::BaseCard.card_class.keys
Cards::BaseCard.card_class[self].new
else
raise "#{self} is not a valid card id"
end
end
end
Cards::BaseCard << Cards::BlankCard
Cards::BaseCard << Cards::IndigoPlant
# Cards::BaseCard.card_class["02"] = Cards::SugarMill
# Cards::BaseCard.card_class["03"] = Cards::TobaccoStorage
# Cards::BaseCard.card_class["04"] = Cards::CoffeeRoaster
# Cards::BaseCard.card_class["05"] = Cards::SilverSmelter
# Cards::BaseCard.card_class["06"] = Cards::Smithy
# Cards::BaseCard.card_class["07"] = Cards::GoldMine
# Cards::BaseCard.card_class["08"] = Cards::Archive
# Cards::BaseCard.card_class["09"] = Cards::PoorHouse
# Cards::BaseCard.card_class["10"] = Cards::BlackMarket
# Cards::BaseCard.card_class["11"] = Cards::TradingPost
# Cards::BaseCard.card_class["12"] = Cards::Well
# Cards::BaseCard.card_class["13"] = Cards::MarketStand
# Cards::BaseCard.card_class["14"] = Cards::Crane
# Cards::BaseCard.card_class["15"] = Cards::Chapel
# Cards::BaseCard.card_class["16"] = Cards::Tower
# Cards::BaseCard.card_class["17"] = Cards::Aqueduct
# Cards::BaseCard.card_class["18"] = Cards::Carpenter
# Cards::BaseCard.card_class["19"] = Cards::Prefecture
# Cards::BaseCard.card_class["20"] = Cards::MarketHall
# Cards::BaseCard.card_class["21"] = Cards::Quarry
# Cards::BaseCard.card_class["22"] = Cards::Library
# Cards::BaseCard.card_class["23"] = Cards::Statue
# Cards::BaseCard.card_class["24"] = Cards::VictoryColumn
# Cards::BaseCard.card_class["25"] = Cards::Hero
# Cards::BaseCard.card_class["26"] = Cards::GuildHall
# Cards::BaseCard.card_class["27"] = Cards::CityHall
# Cards::BaseCard.card_class["28"] = Cards::TriumphalArch
# Cards::BaseCard.card_class["29"] = Cards::Palace
# # Expansion: "The New Buildings"
# Cards::BaseCard.card_class["30"] = Cards::GuardRoom
# Cards::BaseCard.card_class["31"] = Cards::OfficeBuilding
# Cards::BaseCard.card_class["32"] = Cards::Hut
# Cards::BaseCard.card_class["33"] = Cards::Tavern
# Cards::BaseCard.card_class["34"] = Cards::Park
# Cards::BaseCard.card_class["35"] = Cards::CustomsOffice
# Cards::BaseCard.card_class["36"] = Cards::Bank
# Cards::BaseCard.card_class["37"] = Cards::Harbor
# Cards::BaseCard.card_class["38"] = Cards::Goldsmith
# Cards::BaseCard.card_class["39"] = Cards::Residence
# Cards::BaseCard.card_class["40"] = Cards::Cathedral
這個 class 負責了:
- 定義卡片的共同介面
- 宣告共用的常數
- 收集了所有卡片的 class,方便由
BaseCard
去產生各種卡片物件
並擴充了 String class
,可以透過 to_card
方法產生物件
rails run 'puts "01".to_card.to_h.to_json' | jq
{
"id": "01",
"name": "Indigo Plant",
"price": 1,
"type": "Production",
"score": 1,
"amount": 10
}
# spec/requests/api/v1/game_spec.rb
RSpec.describe "Api::V1::Games", type: :request do
# ...
post 'Create a game' do
tags 'Games'
consumes 'application/json'
produces 'application/json'
parameter name: :payload, in: :body, schema: {
type: :object,
properties: {
seed: { type: :string, example: '1234567890abcdef', description: 'Optional' }
}
}
let(:payload) { { seed: '1234567890abcdef' } }
response '200', 'Game created' do
schema type: :object,
properties: {
id: { type: :integer },
status: { type: :string }
},
required: [ 'id', 'status' ]
run_test! do
json = JSON.parse(response.body)
expect(json['status']).to eq('playing')
expect(json['game_config']['seed']).to eq('1234567890abcdef')
expect(json['game_data']['current_price']).to match_array([ 1, 2, 2, 2, 3 ])
expect(json['game_data']['supply_pile'].size).to eq(110 - 4)
expect(json['game_data']['supply_pile'][8]).to eq("01")
end
end
end
end
# ...
end
seed
# app/controllers/api/v1/games_controller.rb
class Api::V1::GamesController < ApplicationController
# ...
def create
@game = Game.start_new_game(seed: params[:seed])
render json: { error: @game.errors.full_messages } unless @game.present?
end
# ...
end
# app/views/api/v1/games/_game.json.jbuilder
# ...
json.game_data do
json.current_price game.current_price
json.supply_pile game.game_data["supply_pile"]
end
# ...
start_new_game
可以接受 seed
參數,並新增 generate_deck
產生 Array<id: string>
在遊戲中只要有 card id
,就能產生卡片物件並進行互動
# app/models/game.rb
class Game < ApplicationRecord
enum :status, {
unknown: 0,
playing: 1,
finished: 2
}, prefix: true
def play
return errors.add(:status, "can't be blank") unless status_playing?
self.status_finished!
end
class << self
def generate_seed = SecureRandom.hex(16)
def start_new_game(seed: nil)
game = new(status: :playing)
# 1. generate a random seed
game.seed = seed || SecureRandom.hex(16)
# 2. shuffle the 5 trading house tiles
game.game_data[:trading_house_order] = TradingHouse.new(seed: game.seed).order
# 3. remove 1 indigo plant from the deck for each player
# shuffle the remaining cards, forming a supply pile
# 3.1. generate the deck
deck = generate_deck
# 3.2. remove 1 indigo plant from the deck for each player
deck.shift(game.players.size)
# 3.3. shuffle the remaining cards to form a supply pile
deck.shuffle!
game.game_data[:supply_pile] = deck
# 4. deal 4 cards to each player as their initial hand, hidden from other players
# 5. choose first player
game.save
game
end
private
def generate_deck
# 3.1. initialize the deck
# 3.1.1. determine how many cards in the deck
deck_size = Cards::BaseCard::DECK_SIZE
# 3.1.2. generate a deck in order
deck = Cards::BaseCard.card_class.keys.map do |id|
next if id == Cards::BlankCard.id
Cards::BaseCard.card_class[id].new.amount.times.map { id }
end
deck = deck.flatten.compact
# 3.1.3. fill the deck with blank cards if deck size is not enough
blank_card_id = Cards::BlankCard.id
deck += (deck_size - deck.size).times.map { blank_card_id }
end
end
def current_price
TradingHouse.new(game_data["trading_house_order"]).current_price
end
# FIXME: hardcode 4 players for now
def players
Struct.new(:size).new(
size: 4
)
end
end
initializers
preload cards/ class,避免找不到定義# config/initializers/preload_game_model.rb
# https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#option-3-preload-a-regular-directory
unless Rails.application.config.eager_load
Rails.application.config.to_prepare do
Rails.autoloaders.main.eager_load_dir(Rails.root.join("app/models/cards"))
end
end
收工.
也可以直接看 diff b794589
Sprint 2: 實作遊戲開始
以上不代表明天會做,如有雷同純屬巧合
SPT (Side Project Taiwan) 的宗旨是藉由Side Project開發來成就自我,透過持續學習和合作,共同推動技術和專業的發展。我們相信每一個參與者,無論是什麼專業,都能在這個社群中找到屬於自己的成長空間。
歡迎所有對Side Project開發有興趣的人加入我們,可以是有點子來找夥伴,也可以是來尋找有興趣的Side Project加入,邀請大家一同打造一個充滿活力且有意義的技術社群!
Discord頻道連結:https://sideproj.tw/dc