Skip to content

Instantly share code, notes, and snippets.

@afa
Created May 19, 2017 12:45
Show Gist options
  • Select an option

  • Save afa/9da4911e9ecfd93ae5f13aa95b9039e3 to your computer and use it in GitHub Desktop.

Select an option

Save afa/9da4911e9ecfd93ae5f13aa95b9039e3 to your computer and use it in GitHub Desktop.
celluloid game
class Game
include Celluloid
include Celluloid::IO
include Celluloid::Internals::Logger
include Celluloid::Notifications
extend Forwardable
finalizer :finalizer
def_delegators :int_state, :stage, :step, :total_steps, :step_status, :statements, :setting
attr_accessor :name, :online
def self.create params = {}
uuid = build(params)
args = { uuid: uuid }.merge(params[:server_setup] ? { server_setup: params[:server_setup] } : {}).merge(start: params[:start])
Center.current.async.to_supervise as: :"game_#{uuid}",
type: Game,
args: [args]
end
def self.build(params = {})
uuid = UUID.new.generate
time = if params[:start_at]
Time.at(params[:start_at].to_i).to_i
elsif params[:start]
Time.at(params[:start][:time].to_i).to_i
else
Time.now.to_i + 300
end
store = Store::Game.create(
mongo_id: params[:id],
name: params[:name],
uuid: uuid,
company: params[:company],
country: params[:country],
description: params[:description],
industry: params[:industry],
state: params[:state],
time_zone: params[:time_zone],
start_at: time
)
if params[:players]
params[:players].each_with_index do |pl, idx|
dat = pl.merge(game_uuid: uuid, order: idx + 1)
mid = dat.delete(:id)
dat[:mongo_id] = mid
Player.build dat
end
end
sett = Store::Setting.for_game(uuid)
sett.update data: sett.data.merge(params[:settings]) if params[:settings]
args = { uuid: uuid, start_at: time }
args.merge!(params[:server_setup]) if params[:server_setup].is_a?(Hash)
if store.start_at.to_i > Time.now.to_i
Center.current.to_supervise as: "game_#{uuid}", type: Game, args: [args]
end
uuid
end
def self.results_for(id)
store = Store::Game.find(uuid: id).first
players = Store::Player.find(game_uuid: id).all.to_a
setting = Store::Setting.find(game_uuid: id).first
statements = Store::Statement.find(game_uuid: id).all.to_a
{
game: { name: store.name, id: store.mongo_id, uuid: store.uuid },
players: players.map do |pl|
{
name: pl.name,
mangled_name: "Player_#{pl.position}",
pitcher_score: pl.pitcher_score,
catcher_score: pl.catcher_score,
uuid: pl.uuid,
position: pl.position
}
end,
settings: setting.data,
statements: statements.inject(s: [], w: [], o: [], t: []) { |rez, st| } # TODO: wtf?
}
end
def int_state
Actor[:"state_#{@uuid}"]
end
def initialize(params = {})
@online = false
@uuid = params[:uuid]
@server_setup = params[:server_setup]
sgame = Store::Game.find(uuid: @uuid).first
@start_at = sgame.start_at
centre = Center.current
centre.to_supervise as: :"admin_logger_#{@uuid}", type: AdminLogger, args: [{ game_uuid: @uuid }]
centre.to_supervise as: :"state_#{@uuid}", type: State, args: [{ game_uuid: @uuid }]
self.name = sgame.name
state = int_state
centre.to_supervise(as: :"players_#{@uuid}", type: Players, args: [{ game_uuid: @uuid }])
players = Actor[:"players_#{@uuid}"]
centre.to_supervise as: :"timers_#{@uuid}", type: Timings, args: [{ game_uuid: @uuid }]
Timings::Start.instance(@uuid).set_time @start_at
state.state = if Timings::Start.instance(@uuid).next_time
:waiting
elsif Timings::Start.instance(@uuid).at
:started
else
:waiting
end
Control.current.publish_control((params.has_key?(:players) ? { players: players.players.map { |p| { name: p.name, url: "#{@server_setup[:url]}/game/#{p.uuid}", uuid: p.uuid, email: p.email } } } : { }).merge(type: 'status', uuid: @uuid, replly_to: 'create'))
Control.current.add_game(@uuid)
state.add_game @uuid
subscribe :save_game_data, :save_game_data
info "done init for #{@uuid}"
async.run
end
def save_game_data(_topic, game_id)
return unless game_id == @uuid
sync_game
publish :game_data_saved, @uuid, :game
end
def sync_game
info 'syncing game'
end
def onconnect
# TODO: react to connection when web connected
# push_state reply: 'connect'
end
def run
# p @uuid
puts 'ok'
end
def start
state = int_state
state.clean_state
players = Actor[:"players_#{@uuid}"]
players.async.build_queue # TODO move to create
if %w(waiting started).map(&:to_sym).include? state.state
state.state = :started
if players.enough_players
publish :game_started, @uuid
start_stage
else
state.state = :terminated
async.terminate_timeout
end
end
end
def start_stage # whats?
players = Actor[:"players_#{@uuid}"]
statements = Actor[:"statements_#{@uuid}"]
Timings::Stage.instance(@uuid).start
statements.clean_current
players.async.push_start_stage
async.start_step
end
def start_step
state = int_state
state.clean_state
players = Actor[:"players_#{@uuid}"]
if %w(s w o t).include? state.stage.to_s
if state.step == 1
Timings::FirstPitch.instance(@uuid).start
else
Timings::Pitch.instance(@uuid).start
end
state.step_status = state.first_enum(State::STEP_STATUSES)
else
Timings::Ranging.instance(@uuid).start
end
players.async.push_start_step
players.async.push_messages
end
def stage_timeout
state = int_state
players = Actor[:"players_#{@uuid}"]
state.stage = state.next_enum(State::STAGES, state.stage)
Timings::Pitch.instance(@uuid).cancel
Timings::FirstPitch.instance(@uuid).cancel
Timings::BetweenStages.instance(@uuid).start
players.async.push_end_stage
end
def ranging(params = {})
# value index player
statements = Actor[:"statements_#{@uuid}"]
stage_swot = State::STAGES.fetch(params[:stage], swot: :end)[:swot]
stmnts = statements.visible_for_buf(statements.rebuild_visible_for(stage_swot))
st = stmnts[params[:index].to_i - 1]
impo = {
player: params[:player],
value: params[:value],
index: params[:index],
stage: stage_swot,
statement: st.value.inspect
}
statements.async.range_for(impo)
publish :importance_added, @uuid, impo
end
def pitch(params = {})
state = int_state
players = Actor[:"players_#{@uuid}"]
# alarms = Actor[:"alarms_#{@uuid}"]
queue = Actor[:"queue_#{@uuid}"]
return unless state.check_state :pitch, queue.pitcher.uuid
state.set_state :pitch, queue.pitcher.uuid
statements = Actor[:"statements_#{@uuid}"]
tm = Time.now.to_i + (state.setting[:voting_quorum_timeout] || 60)
rpl = (params[:to_replace] || []).map do |r|
if r.is_a? Hash
r[:index].to_i
else
r.to_i
end
end
statement = {value: params[:value], to_replace: rpl, author: queue.pitcher.uuid, stage: state.stage, step: state.step, game_uuid: @uuid}
errors = statements.add statement
if errors.empty?
publish :pitcher_pitch, queue.pitcher.uuid, state.stage
state.step_status = state.next_enum(State::STEP_STATUSES, state.step_status)
Timings::Pitch.instance(@uuid).cancel
Timings::FirstPitch.instance(@uuid).cancel
Timings::VotingQuorum.instance(@uuid).start
publish :statement_pitched, @uuid, statement: statement
players.push_pitch(
errors.merge(
value: params[:value],
to_replace: params[:to_replace] || [],
author: queue.pitcher.uglify_name(state.stage.to_s),
timer: Timings.instance(@uuid).next_stamp
)
)
else
state.clean_state
pit = queue.pitcher
pit.publish_msg errors
# unless errors.empty?
# end_step(errors)
# end
end
end
def pitch_timeout(_params = {})
queue = Actor[:"queue_#{@uuid}"]
state = int_state
pit = queue.pitcher
uuid = pit.uuid
state.set_state :pitch_end, uuid
Timings::Pitch.instance(@uuid).cancel
Timings::FirstPitch.instance(@uuid).cancel
publish :pitch_timeout, @uuid, uuid if pit && pit.alive?
publish :pitcher_timeout, uuid, state.stage
end_step(status: 'timeouted')
end
def pass(_params = {})
queue = Actor[:"queue_#{@uuid}"]
pit = queue.pitcher
uuid = pit.uuid
state = int_state
return unless state.check_state :pass, uuid
state.set_state :pass, uuid
Timings::Pitch.instance(@uuid).cancel
Timings::FirstPitch.instance(@uuid).cancel
publish :pitcher_passed, uuid, state.stage
publish :pitch_pass, @uuid, queue.pitcher_id
end_step(status: 'passed')
end
def vote(params = {})
state = int_state
players = Actor[:"players_#{@uuid}"]
# alarms = Actor[:"alarms_#{@uuid}"]
statements = Actor[:"statements_#{@uuid}"]
voting = statements.voting
return unless voting
voting.vote(player: params[:player], result: params[:result])
publish :vote_added, @uuid, vote: { player: params[:player], result: params[:result] }
if voting.quorum?
if Timings::VotingQuorum.instance(@uuid).next_time.to_f > state.setting[:voting_tail_timeout].to_f
Timings.instance(@uuid).cancel(%w(voting_quorum voting_tail))
Timings::VotingTail.instance(@uuid).start
players.async.push_quorum
end
end
if voting.voted_count == voting.votable.size
Timings.instance(@uuid).cancel(%w(voting_quorum voting_tail))
async.end_step(status: voting.calc_result)
end
end
def end_step(params = {})
state = int_state
players = Actor[:"players_#{@uuid}"]
if %w(rs rw ro rt).include? state.stage.to_s
# msg = {type: 'event', subtype: 'end_step', timer: Timings.instance(@uuid).next_stamp}
# async.publish_msg msg
publish :save_game_data, @uuid
players.async.push_end_step params
async.end_stage
else
queue = Actor[:"queue_#{@uuid}"]
statements = Actor[:"statements_#{@uuid}"]
Timings.instance(@uuid).cancel(%w(pitch first_pitch voting_quorum voting_tail))
stat = statements.voting
if stat
stat.calc_votes
stat.set_contribution if stat.status == 'accepted'
stat.vote_results!
publish :player_log_push, @uuid, stat.uuid
elsif %w(passed timeouted).include? params[:status]
# не таймаута/пасса нет
queue.pitcher.count_pitcher_score('pass')
end
statements.update_visible
players.async.push_messages
state.step_status = state.next_enum(State::STEP_STATUSES, state.step_status)
state.step_status = state.next_enum(State::STEP_STATUSES, state.step_status) unless state.step_status == :end
Timings::Results.instance(@uuid).start unless %w(passed timeouted).include?(params[:status])
lg = Actor[:"admin_logger_#{@uuid}"]
queue.next!
lg.step_results :step_results, @uuid
lg.statement_results :statement_results, @uuid, stat.uuid if stat
publish :send_score, @uuid
publish :save_game_data, @uuid
players.async.push_end_step params
async.results_timeout if %w(passed timeouted).include?(params[:status])
end
end
def voting_quorum_timeout(_params = {})
# state = int_state
# players = Actor[:"players_#{@uuid}"]
# queue = Actor[:"queue_#{@uuid}"]
statements = Actor[:"statements_#{@uuid}"]
voting = statements.voting
# calc rank results
Timings::VotingQuorum.instance(@uuid).cancel
Timings::VotingTail.instance(@uuid).cancel
return unless voting
publish :vote_timeouts, @uuid, statement: voting.uuid
end_step(status: voting.conclusion)
end
def voting_tail_timeout(_params = {})
statements = Actor[:"statements_#{@uuid}"]
Timings::VotingQuorum.instance(@uuid).cancel
Timings::VotingTail.instance(@uuid).cancel
voting = statements.voting
return unless voting
publish :vote_timeouts, @uuid, statement: voting.uuid
end_step(status: voting.conclusion)
end
def end_stage(_params = {})
state = int_state
players = Actor[:"players_#{@uuid}"]
state.stage = state.next_enum(State::STAGES, state.stage)
Timings::Terminate.instance(@uuid).cancel if state.stage == :tr
if %w(sw wo ot tr).include? state.stage.to_s
statements = Actor[:"statements_#{@uuid}"]
Timings::BetweenStages.instance(@uuid).start
players.async.push_end_stage
players.copy_half if state.stage == :sw
players.copy_before if state.stage == :tr
statements.copy_before if state.stage == :tr
if state.stage == :tr
statements.init_importances
publish :ranging, @uuid, stage: state.stage
else
publish :next_stage, @uuid, stage: state.stage
end
elsif %w(rs rw ro rt).include? state.stage.to_s
players.async.push_end_stage
async.start_stage
elsif state.stage == :end
players.async.push_end_stage
Timings::AfterGame.instance(@uuid).start
players.async.push_game_results
async.push_saved_game_results
end
end
def end_game(_params = {})
info 'TODO end game'
publish :game_done, @uuid
end
def results_timeout(_params = {})
state = int_state
statements = Actor[:"statements_#{@uuid}"]
Timings::Results.instance(@uuid).cancel
if statements.check_triple_decline
async.end_stage
elsif state.step < state.total_steps
state.step += 1
lg = Actor[:"admin_logger_#{@uuid}"]
lg.next_pitcher :next_pitcher, @uuid
async.start_step
else
async.end_stage
end
end
def between_stages_timeout(_params = {})
state = int_state
Timings::BetweenStages.instance(@uuid).cancel
state.stage = state.next_enum(State::STAGES, state.stage)
state.step = 1
state.step_status = state.first_enum(State::STEP_STATUSES)
async.start_stage
end
def ranging_timeout(_params = {})
Timings::Ranging.instance(@uuid).cancel
async.end_step
end
# def push_event event, params = {}
# publish_msg({type: 'event', subtype: event})
# end
def push_state(_params = {})
# TODO: remove?
end
def push_saved_game_results
sgame = Store::Game.find(uuid: @uuid).first
hsh = { game_id: sgame.mongo_id }
statements = Actor[:"statements_#{@uuid}"]
statements.update_importance_score
statements.rescore
state = int_state
players = Actor[:"players_#{@uuid}"]
stats = %w(s w o t).map(&:to_sym).inject({}) do |rez, sym|
rez[sym] = { statements: [] }
rez[sym][:statements] += statements.visible_for_buf(statements.rebuild_visible_for(sym)).map do |s|
{
author: s.author,
votes: s.votes.map(&:as_json),
importances: s.importances,
result: s.result,
body: s.value,
contribution: s.contribution
}
end
rez
end
vis = %w(s w o t).map(&:to_sym).inject({}) do |rez, sym|
rez[sym] = []
rez[sym] += statements.visible_for_buf(statements.rebuild_visible_for(sym)).map(&:uuid)
rez
end
statements.statements.each { |stmnt| stmnt.visible = vis[stmnt.stage.to_sym].include?(stmnt.uuid) }
sts = statements.statements.map(&:to_store)
pls = players.players
ps = pls.map do |plyer|
sh_name = plyer.uglify_name(:s)
{
sh_name => {
name: plyer.name,
pitcher_score: plyer.pitcher_score,
pitcher_score_before_ranging: plyer.pitcher_score_before_ranging,
catcher_score_before_ranging: plyer.catcher_score_before_ranging,
uglify_name: sh_name,
pitcher_score_first_half: plyer.pitcher_score_first_half,
catcher_score_first_half: plyer.catcher_score_first_half,
uuid: plyer.uuid,
catcher_score: plyer.catcher_score
}
}
end
hsh.merge!(data: stats, players: ps, statements: sts)
al = Actor[:"admin_logger_#{@uuid}"]
logs = al.as_json
hsh[:logs] = logs.sort_by { |log| log.has_key?(:created_at) ? log[:created_at] : log['created_at'] }
uri = URI(state.setting[:game_results_callback])
req = Net::HTTP::Post.new uri.request_uri
req.body = "q='#{hsh.to_json}'"
Net::HTTP.start(uri.hostname, uri.port) do |http|
http.request(req)
end
end
def after_game_timeout
async.stop_timers
async.end_game
end
def terminate_timeout
state = int_state
players = Actor[:"players_#{@uuid}"]
state.state = :terminated
publish :game_terminated, @uuid
players.async.push_terminated
async.stop_timers
async.end_game
end
def stop_timers
timings = Timings.instance(@uuid)
timings.async.stop_timers
end
def online!
@online = true
info "#{@uuid} online"
end
def offline!
@online = false
info "#{@uuid} offline"
end
def publish_msg(msg)
return unless @online
ch = Actor[:"gm_chnl_#{@uuid}"]
if ch && ch.alive?
ch.publish_msg msg.to_json
else
offline!
end
end
def finalizer; end
end
require 'scores'
# игрок. внутренний класс, привязан к реестру
class Player
include Celluloid
include Celluloid::IO
include Celluloid::Notifications
include Celluloid::Internals::Logger
include Scores
finalizer :finalizer
attr_accessor :name, :email, :channel, :game_uuid, :uuid, :redis, :order, :score, :online, :was_online
attr_accessor :pitcher_score, :pitcher_rank, :catcher_score, :delta
attr_accessor :pitcher_score_first_half, :catcher_score_first_half
attr_accessor :pitcher_score_before_ranging, :catcher_score_before_ranging
def self.build(params = {})
Store::Player.create(
name: params[:name],
email: params[:email],
state: params[:state],
mongo_id: params[:mongo_id],
uuid: UUID.new.generate,
game_uuid: params[:game_uuid],
order: params[:order]
)
end
def initialize(params = {})
@online = false
@was_online = false
# @game_uuid = params[:game_uuid]
if params[:uuid]
store = Store::Player.find(uuid: params[:uuid]).first
warn "player #{params.inspect} invalid" unless store
@uuid = store.uuid
@game_uuid = store.game_uuid
@name = store.name
@email = store.email
@score = store.score
@pitcher_rank = 1.0
@catcher_score = 0.0
@delta = 0.0
end
queue = Actor[:"queue_#{@game_uuid}"]
queue.add @uuid
Center.current.async.to_supervise as: "player_logger_#{@uuid}", type: PlayerLogger, args: [{ player_uuid: @uuid }]
subscribe :send_score, :send_players_score
end
def current_stamp
Time.now.to_f.round(3)
end
def send_players_score(_topic, guid)
return unless @game_uuid == guid
players = Actor[:"players_#{@game_uuid}"]
dat_sort = players.players.sort do |sa, sb|
id = sa.uuid
if id == sb.uuid
0
elsif id == @uuid
-1
else
id <=> sb.uuid
end
end
dat = dat_sort.map { |pl| { pitcher: pl.pitcher_rank, catcher: pl.catcher_score } }
msg = {
type: 'event',
subtype: 'ranks',
value: dat
}
publish_msg msg
end
def pitch(params = {})
queue = Actor[:"queue_#{@game_uuid}"]
return unless queue.pitcher_id == @uuid
Timings::Pitch.instance(@game_uuid).cancel
Timings::FirstPitch.instance(@game_uuid).cancel
game = Actor[:"game_#{@game_uuid}"]
game.pitch(params) # TODO: params for game on pitch done (move code to game.pitch)
end
def pass(_params = {})
# info 'start player.pass'
queue = Actor[:"queue_#{@game_uuid}"]
return unless queue.pitcher_id == @uuid
game = Actor[:"game_#{@game_uuid}"]
Timings::Pitch.instance(@game_uuid).cancel
Timings::FirstPitch.instance(@game_uuid).cancel
game.async.pass
end
def count_pitcher_score(typ)
# TODO: step_score.count_pitcher_rank, count_pitcher_score(statements)
state = Actor[:"state_#{@game_uuid}"]
cfg = state.setting
mult = cfg[:"pitcher_rank_multiplier_#{typ}"].to_f
rank = pitcher_rank
rank *= mult
self.pitcher_rank = [rank, cfg[:pitcher_minimum_rank].to_f].max
statements = Actor[:"statements_#{@game_uuid}"]
statements.count_pitchers_score
end
def vote(params = {})
val = params[:value]
game = Actor[:"game_#{@game_uuid}"]
send_vote(value: val)
game.async.vote(result: val, player: @uuid)
# TODO: params for game on pitch done (move code to game.pitch)
end
def ranging(params = {})
index = params[:index]
val = params[:val]
game = Actor[:"game_#{@game_uuid}"]
state = Actor[:"state_#{@game_uuid}"]
send_ranging(value: val, index: index)
game.async.ranging(value: val, player: @uuid, index: index, stage: state.stage)
end
def online!
@online = true
@was_online = true
info "#{@uuid} online"
state_obj = Actor[:"state_#{@game_uuid}"]
state = state_obj.state.to_s
players = Actor[:"players_#{@game_uuid}"]
players.check_min_players
async.send_ready reply_to: 'connect' if state == 'waiting'
async.send_state reply_to: 'connect' if state == 'started'
async.send_terminated if state == 'terminated'
async.send_result reply_to: 'connect' unless %w(waiting started).include?(state)
publish :player_online, @game_uuid, uuid: @uuid
end
def offline!
@online = false
players = Actor[:"players_#{@game_uuid}"]
players.check_min_players
info "#{@uuid} offline"
publish :player_offline, @game_uuid, uuid: @uuid
end
def publish_msg(msg)
if @online
ch = Actor[:"chnl_#{@uuid}"]
if ch && ch.alive?
ch.publish_msg msg.merge(time: current_stamp, rel: SWOT_REL, version: SWOT_VERSION).to_json
else
info 'chnl down'
offline!
end
else
info "player #{@uuid} offline"
end
end
def send_result(params = {})
intervals = params.fetch(:intervals, []).map(&:to_sym)
msg = { type: 'event',
subtype: 'result',
timeout_at: Timings.instance(@game_uuid).stamps(intervals),
time: current_stamp }
publish_msg msg
end
def send_game_results(params = {})
statements = Actor[:"statements_#{@game_uuid}"]
players = Actor[:"players_#{@game_uuid}"]
stats = %w(s w o t).map(&:to_sym).inject({}) do |r, sym|
r[sym] = { statements: [] }
# TODO: check contrib
r[sym][:statements] += statements.visible_for_buf(statements.rebuild_visible_for(sym)).map do |s|
{ body: s.value, contribution: format('%d%', (100.0 * s.contribution_for(@uuid))) }
end
r
end
pls = players.players.sort do |sa, sb|
id = sa.uuid
sbid = sb.uuid
if id == sbid
0
elsif id == @uuid
-1
else
id <=> sbid
end
end
cur = pls.shift
ps = [{
cur.name => {
pitcher_score: format('%.03f', cur.pitcher_score),
catcher_score: format('%.03f', cur.catcher_score)
}
}] + pls.map do |pl|
{
pl.uglify_name(:s) => {
pitcher_score: format('%.03f', pl.pitcher_score),
catcher_score: format('%.03f', pl.catcher_score)
}
}
end
msg = { type: 'results', value: { data: stats, players: ps } }
publish_msg msg
end
def send_ready(_params = {})
state = Actor[:"state_#{@game_uuid}"]
queue = Actor[:"queue_#{@game_uuid}"]
pit = queue.pitcher.uuid == @uuid
msg = {
type: 'event', subtype: 'ready', name: @name,
start_at: Timings::Start.instance(@game_uuid).at, pitcher: pit,
timeout_at: Timings.instance(@game_uuid).stamps(%w(start).map(&:to_sym)),
version: SWOT_VERSION, time: current_stamp, max_steps: state.total_steps
}
publish_msg msg
end
def send_pitch(params = {})
state = Actor[:"state_#{@game_uuid}"]
queue = Actor[:"queue_#{@game_uuid}"]
msg = {
type: 'event', subtype: 'pitched', value: params[:value], to_replace: (params[:to_replace] || []),
author: queue.pitcher.uglify_name(state.stage),
timeout_at: Timings.instance(@game_uuid).stamps(%w(stage voting_quorum voting_tail).map(&:to_sym)),
time: current_stamp, step: { status: state.step_status }
}
publish_msg msg
end
def send_pass
msg = { type: 'event', subtype: 'passed', time: current_stamp }
publish_msg msg
end
def send_vote(params = {})
msg = {
type: 'event', subtype: 'voted',
timeout_at: Timings.instance(@game_uuid).stamps(%w(voting_quorum voting_tail stage).map(&:to_sym)),
time: current_stamp
}.merge(params)
publish_msg msg
end
def send_quorum
msg = {
type: 'event', subtype: 'quorum',
timeout_at: Timings.instance(@game_uuid).stamps(%w(voting_quorum voting_tail stage).map(&:to_sym)),
continue: true, time: current_stamp
}
publish_msg msg
end
def send_ranging(params = {})
msg = {
type: 'event', subtype: 'ranging',
timeout_at: Timings.instance(@game_uuid).stamps([:ranging]),
time: current_stamp
}.merge(params)
publish_msg msg
end
def send_messages(_params = {})
state = Actor[:"state_#{@game_uuid}"]
stage = state.stage.to_s
stage_swot = State::STAGES.fetch(state.stage, swot: :end)[:swot]
statements = Actor[:"statements_#{@game_uuid}"]
stmnts = if %w(rs rw ro rt).include? stage
statements.visible_for_buf(statements.rebuild_visible_for(stage_swot)).map { |st| st.as_json(@uuid) }
elsif %w(s w o t sw wo ot tr).include?(stage)
statements.active_js(@uuid)
else
[]
end
msg = { type: 'event', subtype: 'statements', value: stmnts }
publish_msg msg
end
def send_start_step
state = Actor[:"state_#{@game_uuid}"]
queue = Actor[:"queue_#{@game_uuid}"]
msg = {
type: 'event', subtype: 'start_step', turn_in: queue.index(@uuid),
pitcher_name: queue.pitcher.uglify_name(state.stage),
timeout_at: Timings.instance(@game_uuid).stamps(%w(ranging stage first_pitch pitch).map(&:to_sym)),
step: { current: state.step, total: state.total_steps, status: state.step_status }, time: current_stamp
}
publish_msg msg
end
def send_end_step(params = {})
queue = Actor[:"queue_#{@game_uuid}"]
statements = Actor[:"statements_#{@game_uuid}"]
stat = statements.last_stat
if stat
quorum = stat.quorum?
status = stat.status
per = 100 * stat.result.to_f
per = 100 - per unless status == 'accepted'
per = quorum ? per.round(1) : 'no_quorum'
rnk = if stat.author == @uuid
{ score: @pitcher_rank }
else
{ score: @catcher_score, delta: format('%+.1f', @delta) }
end
msg = {
type: 'event', subtype: 'end_step',
result: {
status: (quorum ? status : 'no_quorum'),
players_voted: per
}.merge(rnk),
timeout_at: Timings.instance(@game_uuid).stamps(%w(stage results between_stages).map(&:to_sym)),
time: current_stamp
}
else
rnk = if queue.prev_pitcher == @uuid
{ rank: @pitcher_rank }
else
{ score: @catcher_score, delta: format('%+.1f', @delta) }
end
msg = {
type: 'event', subtype: 'end_step',
result: {
status: params[:status]
}.merge(rnk),
timeout_at: Timings.instance(@game_uuid).stamps(%w(stage results between_stages).map(&:to_sym)),
time: current_stamp
}
end
publish_msg msg
end
def send_start_stage
queue = Actor[:"queue_#{@game_uuid}"]
state = Actor[:"state_#{@game_uuid}"]
msg = { type: 'event', subtype: 'start_stage', value: state.stage, turn_in: (queue.index(@uuid) || 3) }
publish_msg msg
end
def send_end_stage
state = Actor[:"state_#{@game_uuid}"]
queue = Actor[:"queue_#{@game_uuid}"]
msg = {
type: 'event', subtype: 'end_stage', value: state.stage,
pitcher: %w(s w o t sw wo ot).include?(state.stage.to_s) && queue.pitcher == @uuid,
turn_in: (queue.index(@uuid) || 3), player: { turn_in: (queue.index(@uuid) || 3) },
time: current_stamp,
timeout_at: Timings.instance(@game_uuid).stamps([:between_stages])
}
publish_msg msg
end
def copy_half
@catcher_score_first_half = @catcher_score
@pitcher_score_first_half = @pitcher_score
end
def copy_before
@catcher_score_before_ranging = @catcher_score
@pitcher_score_before_ranging = @pitcher_score
end
def send_terminated
publish_msg(type: 'event', subtype: 'terminated')
msg = gen_state
publish_msg msg
end
def uglify_name(stage)
%w(s sw t tr).map(&:to_sym).include?(stage) ? "Player #{order}" : name
end
def gen_conclusion
statements = Actor[:"statements_#{@game_uuid}"]
queue = Actor[:"queue_#{@game_uuid}"]
state = Actor[:"state_#{@game_uuid}"]
pitcher = queue.pitcher.uglify_name(state.stage)
conclusion = { author: pitcher }
if statements.voting
vot = statements.voting
per = 100 * vot.result.to_f
per = 100 - per unless vot.status == 'accepted'
per = per.round(1)
conclusion.merge!(
value: vot.value,
author: Actor[:"player_#{vot.author}"].uglify_name(state.stage),
to_replace: vot.replaces.map { |r| statements.find(r) }.compact.map(&:value),
status: vot.status,
player_score: 0.0, # TODO: fix scores
players_voted: per
)
end
conclusion
end
def gen_state(_params = {})
queue = Actor[:"queue_#{@game_uuid}"]
state = Actor[:"state_#{@game_uuid}"]
stage_swot = State::STAGES.fetch(state.stage, swot: :end)[:swot]
statements = Actor[:"statements_#{@game_uuid}"]
if %w(rs rw ro rt).include? state.stage.to_s
stmnts = statements.visible_for_buf(statements.rebuild_visible_for(stage_swot)).map { |s| s.as_json(@uuid) }
elsif %w(s w o t sw wo ot tr).include?(state.stage.to_s)
stmnts = statements.active_js(@uuid)
else
stmnts = []
end
{
type: 'status',
version: SWOT_VERSION,
state: state.state,
name: @name,
game: {
time: current_stamp,
step: {
current: state.step,
total: state.total_steps,
status: state.step_status
},
current_stage: state.stage, # one of stages
conclusion: gen_conclusion,
replaces: [],
statements: stmnts,
player: {
turn_in: (queue.index(@uuid) || 3)
},
started_at: Timings::Start.instance(@game_uuid).at,
timeout_at: Timings.instance(@game_uuid).stamps(
%w(stage pitch first_pitch voting_quorum voting_tail ranging between_stages results start).map(&:to_sym)
)
}
}
end
def send_state(params = {})
publish_msg gen_state(params)
end
def catcher_apply_delta(delta)
@catcher_score += delta
@delta = delta
end
def finalizer
end
end
module Message
class Base
attr_accessor :at
include Celluloid::Internals::Logger
def self.try_load(ch, hsh)
new ch, hsh
end
def initialize ch, hash = {}
self.at = ('%10.6f' % Time.now).to_f
end
def process
end
end
end
require "game"
module Message
class Create < Base
include Celluloid::Internals::Logger
def self.try_load(ch, hsh)
return nil unless hsh[:type] == 'create'
return nil unless ch == '/swot/control'
# return nil unless ch == Control::CONTROL_CHANNEL
Celluloid::Internals::Logger.info "create #{ch.inspect} #{hsh.inspect}"
super
end
def initialize ch, hash = {}
info "init create"
super
@name = hash[:name] if hash[:name]
@players = hash[:players] if hash[:players] && hash[:players].is_a?(Array)
@start = hash[:start]
@set = hash[:settings]
end
def process
info "process create"
super
::Game.create(name: @name, players: @players, start: @start, reply: 'create', server_setup: Center.current.server_config.dup, settings: @set)
end
end
end
require 'hashing'
require 'players/player'
require 'players/queue'
# require 'forwardable'
class Players
include Hashing
# extend Forwardable
include Celluloid
include Celluloid::IO
include Celluloid::Notifications
include Celluloid::Internals::Logger
attr :players
def initialize(params = {})
@players = []
guid = params[:game_uuid]
if guid
@game_uuid = guid
queue_wait = future.mk_queue
async.mk_players if queue_wait.value == :ok
end
subscribe :save_game_data, :save_game_data
end
def mk_queue
Center.current.to_supervise as: :"queue_#{@game_uuid}", type: Queue, args: [{ game_uuid: @game_uuid }]
:ok
end
def mk_players
splayers = Store::Player.find(game_uuid: @game_uuid).to_a.sort_by(&:order)
splayers.each do |pl|
p_id = pl.uuid
Center.current.to_supervise(as: :"player_#{p_id}", type: Player, args: [uuid: p_id])
async.add p_id
end
end
def save_game_data(_topic, game_id)
return unless game_id == @game_uuid
sync_players
publish :game_data_saved, @game_uuid, :players
end
def sync_players
info 'syncing players'
end
def push_event(event, params = {})
players.each { |pl| pl.send_event event, params }
end
def push_state(params = {})
players.each { |pl| pl.send_state params }
end
def push_messages(params = {})
players.each { |pl| pl.send_messages params }
end
def players
@players.map { |id| Actor[:"player_#{id}"] }.select { |pl| pl && pl.alive? }
end
def player_ids
@players
end
def add(player)
ord = players.map { |pl| pl.order.to_i }.inject(0) { |rez, pl| rez >= pl ? rez : pl }
pl_id = player.is_a?(String) ? player : player.uuid
Actor[:"player_#{pl_id}"].order = ord + 1
@players << pl_id
queue.add pl_id
Control.current.add_player(@game_uuid, pl_id)
end
def copy_half
players.each do |pl|
pl.async.copy_half
end
end
def copy_before
players.each do |pl|
pl.async.copy_before
end
end
def push_start_stage
players.each(&:send_start_stage)
end
def push_start_step
players.each do |pl|
pl.catcher_apply_delta(0.0)
pl.send_start_step
pl.send_state
end
end
def push_end_step(params = {})
players.each do |pl|
pl.async.send_end_step params
end
end
def push_end_stage
players.each do |pl|
pl.async.send_end_stage
end
end
def push_pitch(params = {})
players.each do |pl|
pl.send_pitch params
end
end
def push_pass
players.each(&:send_pass)
end
def push_quorum
players.each(&:send_quorum)
end
def push_vote
players.each(&:send_vote)
end
def push_terminated
players.each(&:send_terminated)
end
def push_game_results
players.each(&:send_game_results)
end
def enough_players
state = Actor[:"state_#{@game_uuid}"]
cfg = state.setting
online.size >= cfg[:min_players].to_i
end
def online
players.select(&:online)
end
def was_online
players.select(&:was_online)
end
def check_min_players
state = Actor[:"state_#{@game_uuid}"]
return unless state.state == :started && %w(s sw w wo o ot t).include?(state.stage.to_s)
timer = Timings::Terminate.instance(@game_uuid)
if enough_players && timer.next_time
timer.cancel
else
timer.start
end
end
def find(pl_id)
return Actor[:"player_#{pl_id}"] if @players.include? pl_id
nil
end
def build_queue
queue.rebuild_tail
queue.fill_current
end
def current_pitcher
Actor[:"player_#{queue.first}"]
end
def queue
Actor[:"queue_#{@game_uuid}"]
end
end
require 'scores'
# игрок. внутренний класс, привязан к реестру
class Player
include Celluloid
include Celluloid::IO
include Celluloid::Notifications
include Celluloid::Internals::Logger
include Scores
finalizer :finalizer
attr_accessor :name, :email, :channel, :game_uuid, :uuid, :redis, :order, :score, :online, :was_online
attr_accessor :pitcher_score, :pitcher_rank, :catcher_score, :delta
attr_accessor :pitcher_score_first_half, :catcher_score_first_half
attr_accessor :pitcher_score_before_ranging, :catcher_score_before_ranging
def self.build(params = {})
Store::Player.create(
name: params[:name],
email: params[:email],
state: params[:state],
mongo_id: params[:mongo_id],
uuid: UUID.new.generate,
game_uuid: params[:game_uuid],
order: params[:order]
)
end
def initialize(params = {})
@online = false
@was_online = false
# @game_uuid = params[:game_uuid]
if params[:uuid]
store = Store::Player.find(uuid: params[:uuid]).first
warn "player #{params.inspect} invalid" unless store
@uuid = store.uuid
@game_uuid = store.game_uuid
@name = store.name
@email = store.email
@score = store.score
@pitcher_rank = 1.0
@catcher_score = 0.0
@delta = 0.0
end
queue = Actor[:"queue_#{@game_uuid}"]
queue.add @uuid
Center.current.async.to_supervise as: "player_logger_#{@uuid}", type: PlayerLogger, args: [{ player_uuid: @uuid }]
subscribe :send_score, :send_players_score
end
def current_stamp
Time.now.to_f.round(3)
end
def send_players_score(_topic, guid)
return unless @game_uuid == guid
players = Actor[:"players_#{@game_uuid}"]
dat_sort = players.players.sort do |sa, sb|
id = sa.uuid
if id == sb.uuid
0
elsif id == @uuid
-1
else
id <=> sb.uuid
end
end
dat = dat_sort.map { |pl| { pitcher: pl.pitcher_rank, catcher: pl.catcher_score } }
msg = {
type: 'event',
subtype: 'ranks',
value: dat
}
publish_msg msg
end
def pitch(params = {})
queue = Actor[:"queue_#{@game_uuid}"]
return unless queue.pitcher_id == @uuid
Timings::Pitch.instance(@game_uuid).cancel
Timings::FirstPitch.instance(@game_uuid).cancel
game = Actor[:"game_#{@game_uuid}"]
game.pitch(params) # TODO: params for game on pitch done (move code to game.pitch)
end
def pass(_params = {})
# info 'start player.pass'
queue = Actor[:"queue_#{@game_uuid}"]
return unless queue.pitcher_id == @uuid
game = Actor[:"game_#{@game_uuid}"]
Timings::Pitch.instance(@game_uuid).cancel
Timings::FirstPitch.instance(@game_uuid).cancel
game.async.pass
end
def count_pitcher_score(typ)
# TODO: step_score.count_pitcher_rank, count_pitcher_score(statements)
state = Actor[:"state_#{@game_uuid}"]
cfg = state.setting
mult = cfg[:"pitcher_rank_multiplier_#{typ}"].to_f
rank = pitcher_rank
rank *= mult
self.pitcher_rank = [rank, cfg[:pitcher_minimum_rank].to_f].max
statements = Actor[:"statements_#{@game_uuid}"]
statements.count_pitchers_score
end
def vote(params = {})
val = params[:value]
game = Actor[:"game_#{@game_uuid}"]
send_vote(value: val)
game.async.vote(result: val, player: @uuid)
# TODO: params for game on pitch done (move code to game.pitch)
end
def ranging(params = {})
index = params[:index]
val = params[:val]
game = Actor[:"game_#{@game_uuid}"]
state = Actor[:"state_#{@game_uuid}"]
send_ranging(value: val, index: index)
game.async.ranging(value: val, player: @uuid, index: index, stage: state.stage)
end
def online!
@online = true
@was_online = true
info "#{@uuid} online"
state_obj = Actor[:"state_#{@game_uuid}"]
state = state_obj.state.to_s
players = Actor[:"players_#{@game_uuid}"]
players.check_min_players
async.send_ready reply_to: 'connect' if state == 'waiting'
async.send_state reply_to: 'connect' if state == 'started'
async.send_terminated if state == 'terminated'
async.send_result reply_to: 'connect' unless %w(waiting started).include?(state)
publish :player_online, @game_uuid, uuid: @uuid
end
def offline!
@online = false
players = Actor[:"players_#{@game_uuid}"]
players.check_min_players
info "#{@uuid} offline"
publish :player_offline, @game_uuid, uuid: @uuid
end
def publish_msg(msg)
if @online
ch = Actor[:"chnl_#{@uuid}"]
if ch && ch.alive?
ch.publish_msg msg.merge(time: current_stamp, rel: SWOT_REL, version: SWOT_VERSION).to_json
else
info 'chnl down'
offline!
end
else
info "player #{@uuid} offline"
end
end
def send_result(params = {})
intervals = params.fetch(:intervals, []).map(&:to_sym)
msg = { type: 'event',
subtype: 'result',
timeout_at: Timings.instance(@game_uuid).stamps(intervals),
time: current_stamp }
publish_msg msg
end
def send_game_results(params = {})
statements = Actor[:"statements_#{@game_uuid}"]
players = Actor[:"players_#{@game_uuid}"]
stats = %w(s w o t).map(&:to_sym).inject({}) do |r, sym|
r[sym] = { statements: [] }
# TODO: check contrib
r[sym][:statements] += statements.visible_for_buf(statements.rebuild_visible_for(sym)).map do |s|
{ body: s.value, contribution: format('%d%', (100.0 * s.contribution_for(@uuid))) }
end
r
end
pls = players.players.sort do |sa, sb|
id = sa.uuid
sbid = sb.uuid
if id == sbid
0
elsif id == @uuid
-1
else
id <=> sbid
end
end
cur = pls.shift
ps = [{
cur.name => {
pitcher_score: format('%.03f', cur.pitcher_score),
catcher_score: format('%.03f', cur.catcher_score)
}
}] + pls.map do |pl|
{
pl.uglify_name(:s) => {
pitcher_score: format('%.03f', pl.pitcher_score),
catcher_score: format('%.03f', pl.catcher_score)
}
}
end
msg = { type: 'results', value: { data: stats, players: ps } }
publish_msg msg
end
def send_ready(_params = {})
state = Actor[:"state_#{@game_uuid}"]
queue = Actor[:"queue_#{@game_uuid}"]
pit = queue.pitcher.uuid == @uuid
msg = {
type: 'event', subtype: 'ready', name: @name,
start_at: Timings::Start.instance(@game_uuid).at, pitcher: pit,
timeout_at: Timings.instance(@game_uuid).stamps(%w(start).map(&:to_sym)),
version: SWOT_VERSION, time: current_stamp, max_steps: state.total_steps
}
publish_msg msg
end
def send_pitch(params = {})
state = Actor[:"state_#{@game_uuid}"]
queue = Actor[:"queue_#{@game_uuid}"]
msg = {
type: 'event', subtype: 'pitched', value: params[:value], to_replace: (params[:to_replace] || []),
author: queue.pitcher.uglify_name(state.stage),
timeout_at: Timings.instance(@game_uuid).stamps(%w(stage voting_quorum voting_tail).map(&:to_sym)),
time: current_stamp, step: { status: state.step_status }
}
publish_msg msg
end
def send_pass
msg = { type: 'event', subtype: 'passed', time: current_stamp }
publish_msg msg
end
def send_vote(params = {})
msg = {
type: 'event', subtype: 'voted',
timeout_at: Timings.instance(@game_uuid).stamps(%w(voting_quorum voting_tail stage).map(&:to_sym)),
time: current_stamp
}.merge(params)
publish_msg msg
end
def send_quorum
msg = {
type: 'event', subtype: 'quorum',
timeout_at: Timings.instance(@game_uuid).stamps(%w(voting_quorum voting_tail stage).map(&:to_sym)),
continue: true, time: current_stamp
}
publish_msg msg
end
def send_ranging(params = {})
msg = {
type: 'event', subtype: 'ranging',
timeout_at: Timings.instance(@game_uuid).stamps([:ranging]),
time: current_stamp
}.merge(params)
publish_msg msg
end
def send_messages(_params = {})
state = Actor[:"state_#{@game_uuid}"]
stage = state.stage.to_s
stage_swot = State::STAGES.fetch(state.stage, swot: :end)[:swot]
statements = Actor[:"statements_#{@game_uuid}"]
stmnts = if %w(rs rw ro rt).include? stage
statements.visible_for_buf(statements.rebuild_visible_for(stage_swot)).map { |st| st.as_json(@uuid) }
elsif %w(s w o t sw wo ot tr).include?(stage)
statements.active_js(@uuid)
else
[]
end
msg = { type: 'event', subtype: 'statements', value: stmnts }
publish_msg msg
end
def send_start_step
state = Actor[:"state_#{@game_uuid}"]
queue = Actor[:"queue_#{@game_uuid}"]
msg = {
type: 'event', subtype: 'start_step', turn_in: queue.index(@uuid),
pitcher_name: queue.pitcher.uglify_name(state.stage),
timeout_at: Timings.instance(@game_uuid).stamps(%w(ranging stage first_pitch pitch).map(&:to_sym)),
step: { current: state.step, total: state.total_steps, status: state.step_status }, time: current_stamp
}
publish_msg msg
end
def send_end_step(params = {})
queue = Actor[:"queue_#{@game_uuid}"]
statements = Actor[:"statements_#{@game_uuid}"]
stat = statements.last_stat
if stat
quorum = stat.quorum?
status = stat.status
per = 100 * stat.result.to_f
per = 100 - per unless status == 'accepted'
per = quorum ? per.round(1) : 'no_quorum'
rnk = if stat.author == @uuid
{ score: @pitcher_rank }
else
{ score: @catcher_score, delta: format('%+.1f', @delta) }
end
msg = {
type: 'event', subtype: 'end_step',
result: {
status: (quorum ? status : 'no_quorum'),
players_voted: per
}.merge(rnk),
timeout_at: Timings.instance(@game_uuid).stamps(%w(stage results between_stages).map(&:to_sym)),
time: current_stamp
}
else
rnk = if queue.prev_pitcher == @uuid
{ rank: @pitcher_rank }
else
{ score: @catcher_score, delta: format('%+.1f', @delta) }
end
msg = {
type: 'event', subtype: 'end_step',
result: {
status: params[:status]
}.merge(rnk),
timeout_at: Timings.instance(@game_uuid).stamps(%w(stage results between_stages).map(&:to_sym)),
time: current_stamp
}
end
publish_msg msg
end
def send_start_stage
queue = Actor[:"queue_#{@game_uuid}"]
state = Actor[:"state_#{@game_uuid}"]
msg = { type: 'event', subtype: 'start_stage', value: state.stage, turn_in: (queue.index(@uuid) || 3) }
publish_msg msg
end
def send_end_stage
state = Actor[:"state_#{@game_uuid}"]
queue = Actor[:"queue_#{@game_uuid}"]
msg = {
type: 'event', subtype: 'end_stage', value: state.stage,
pitcher: %w(s w o t sw wo ot).include?(state.stage.to_s) && queue.pitcher == @uuid,
turn_in: (queue.index(@uuid) || 3), player: { turn_in: (queue.index(@uuid) || 3) },
time: current_stamp,
timeout_at: Timings.instance(@game_uuid).stamps([:between_stages])
}
publish_msg msg
end
def copy_half
@catcher_score_first_half = @catcher_score
@pitcher_score_first_half = @pitcher_score
end
def copy_before
@catcher_score_before_ranging = @catcher_score
@pitcher_score_before_ranging = @pitcher_score
end
def send_terminated
publish_msg(type: 'event', subtype: 'terminated')
msg = gen_state
publish_msg msg
end
def uglify_name(stage)
%w(s sw t tr).map(&:to_sym).include?(stage) ? "Player #{order}" : name
end
def gen_conclusion
statements = Actor[:"statements_#{@game_uuid}"]
queue = Actor[:"queue_#{@game_uuid}"]
state = Actor[:"state_#{@game_uuid}"]
pitcher = queue.pitcher.uglify_name(state.stage)
conclusion = { author: pitcher }
if statements.voting
vot = statements.voting
per = 100 * vot.result.to_f
per = 100 - per unless vot.status == 'accepted'
per = per.round(1)
conclusion.merge!(
value: vot.value,
author: Actor[:"player_#{vot.author}"].uglify_name(state.stage),
to_replace: vot.replaces.map { |r| statements.find(r) }.compact.map(&:value),
status: vot.status,
player_score: 0.0, # TODO: fix scores
players_voted: per
)
end
conclusion
end
def gen_state(_params = {})
queue = Actor[:"queue_#{@game_uuid}"]
state = Actor[:"state_#{@game_uuid}"]
stage_swot = State::STAGES.fetch(state.stage, swot: :end)[:swot]
statements = Actor[:"statements_#{@game_uuid}"]
if %w(rs rw ro rt).include? state.stage.to_s
stmnts = statements.visible_for_buf(statements.rebuild_visible_for(stage_swot)).map { |s| s.as_json(@uuid) }
elsif %w(s w o t sw wo ot tr).include?(state.stage.to_s)
stmnts = statements.active_js(@uuid)
else
stmnts = []
end
{
type: 'status',
version: SWOT_VERSION,
state: state.state,
name: @name,
game: {
time: current_stamp,
step: {
current: state.step,
total: state.total_steps,
status: state.step_status
},
current_stage: state.stage, # one of stages
conclusion: gen_conclusion,
replaces: [],
statements: stmnts,
player: {
turn_in: (queue.index(@uuid) || 3)
},
started_at: Timings::Start.instance(@game_uuid).at,
timeout_at: Timings.instance(@game_uuid).stamps(
%w(stage pitch first_pitch voting_quorum voting_tail ranging between_stages results start).map(&:to_sym)
)
}
}
end
def send_state(params = {})
publish_msg gen_state(params)
end
def catcher_apply_delta(delta)
@catcher_score += delta
@delta = delta
end
def finalizer
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment