class GameController < ApplicationController
|
|
include QuizHelperMethods
|
|
include UtmTracking
|
|
include DevJump
|
|
skip_before_action :require_player!, only: [ :index, :start ]
|
|
|
|
# The whole game lives in one turbo-frame declared in the application layout,
|
|
# so the URL never changes. Frame requests must render that layout too:
|
|
# turbo-rails otherwise swaps in its bare "turbo_rails/frame" layout, and the
|
|
# response comes back without the <turbo-frame id="game"> Turbo is looking for.
|
|
layout "application"
|
|
|
|
before_action :set_node, only: [ :stage, :answer, :stage_result ]
|
|
after_action :track_step, only: [ :facts, :intro, :stage, :stage_result, :last_save, :done, :results ]
|
|
|
|
helper_method :root_node,
|
|
:stage_index,
|
|
:n_stages,
|
|
:last_save_node
|
|
|
|
def index
|
|
@node = root_node
|
|
not_found and return unless @node
|
|
|
|
render action: @node.template
|
|
end
|
|
|
|
# POST
|
|
def start
|
|
player = Player.create(utm_attributes.merge(
|
|
locale: I18n.locale.to_s,
|
|
country: country_from_param,
|
|
device: Player.device_from_user_agent(request.user_agent)))
|
|
ResolvePlayerCountryJob.perform_later(player.id, request.remote_ip) if player.country.blank?
|
|
session[:player_id] = player.id
|
|
|
|
if params[:skip_facts]
|
|
redirect_to action: "intro"
|
|
return
|
|
end
|
|
|
|
@node = root_node&.children&.facts&.first
|
|
respond_to do |format|
|
|
format.turbo_stream
|
|
format.html { redirect_to action: "facts" }
|
|
end
|
|
end
|
|
|
|
|
|
def facts
|
|
@node = root_node&.children&.facts&.first
|
|
end
|
|
|
|
|
|
def intro
|
|
@node = root_node&.children&.intro&.first
|
|
end
|
|
|
|
|
|
def stage
|
|
end
|
|
|
|
|
|
# POST
|
|
def answer
|
|
@answer = @node.children.find_by(id: params[:value])
|
|
@answer = @answer.children.sample if @answer&.chance?
|
|
|
|
current_player.record_answer(stage_index, @answer.id)
|
|
apply_score(score_entry_for(stage_index, @answer))
|
|
|
|
bad_chance_outcome = @answer.parent&.chance? && @answer.bad_answer?
|
|
|
|
if stage_index >= n_stages && !bad_chance_outcome
|
|
redirect_to action: :done
|
|
else
|
|
redirect_to action: :stage_result, id: params[:id]
|
|
end
|
|
end
|
|
|
|
|
|
|
|
def stage_result
|
|
@result_node = @node.descendants.find_by(id: current_player.answer_id_for(stage_index))
|
|
not_found and return unless @result_node
|
|
end
|
|
|
|
|
|
def last_save
|
|
@node = last_save_node
|
|
end
|
|
|
|
|
|
# POST
|
|
def answer_last_save
|
|
@node = last_save_node
|
|
not_found and return unless @node
|
|
|
|
@answer = @node.children.find_by(id: params[:value])
|
|
current_player.record_last_save_answer(@answer.id)
|
|
apply_score(last_save_score_entry_for(@answer))
|
|
|
|
redirect_to action: :done
|
|
end
|
|
|
|
|
|
def done
|
|
current_player.update(is_done: true) unless current_player.is_done?
|
|
|
|
if current_player.last_save_answer_id
|
|
@node = last_save_node
|
|
@result_node = @node.descendants.find_by(id: current_player.last_save_answer_id)
|
|
else
|
|
@node = stages.last
|
|
@result_node = @node.descendants.find_by(id: current_player.answer_id_for(stages.size))
|
|
end
|
|
end
|
|
|
|
|
|
def results
|
|
@node = root_node.children.results.first
|
|
end
|
|
|
|
|
|
# POST -- thumbs up / down on the results screen. One rating per player;
|
|
# tapping the other thumb (or the same one again) just overwrites it.
|
|
def rate
|
|
current_player.rate!(params[:direction])
|
|
|
|
head :no_content
|
|
end
|
|
|
|
|
|
private
|
|
|
|
|
|
# Records the furthest screen this player reached, so the admin funnel can
|
|
# show drop-off for people who never answered anything.
|
|
def track_step
|
|
return unless response.successful?
|
|
|
|
current_player.record_step(step_name)
|
|
end
|
|
|
|
|
|
def step_name
|
|
case action_name
|
|
when "stage" then "stage_#{stage_index}"
|
|
when "stage_result" then "stage_#{stage_index}_result"
|
|
else action_name
|
|
end
|
|
end
|
|
|
|
|
|
def root_node
|
|
@root_node ||= Node.roots.viewable.first
|
|
end
|
|
|
|
|
|
def last_save_node
|
|
@last_save_node ||= root_node.children.last_save.first
|
|
end
|
|
|
|
|
|
def stages
|
|
@stages ||= root_node.children.ordered.stage
|
|
end
|
|
|
|
|
|
def set_node
|
|
@node = stages[params[:id].to_i - 1]
|
|
not_found and return unless @node
|
|
end
|
|
|
|
|
|
|
|
def stage_index
|
|
@stage_index ||= stages.index(@node) + 1
|
|
end
|
|
|
|
|
|
def n_stages
|
|
@n_stages ||= stages.size
|
|
end
|
|
|
|
|
|
|
|
# An explicit ?country= override (handy for testing or a fixed-country kiosk).
|
|
# When absent, IP-based detection runs asynchronously in
|
|
# ResolvePlayerCountryJob so it never blocks the start of the game.
|
|
def country_from_param
|
|
Player.valid_country_code(params[:country])
|
|
end
|
|
|
|
|
|
def url_from_param
|
|
return "" unless params[:url]
|
|
|
|
# return File.join('', params[:url]) if I18n.default_locale == I18n.locale
|
|
File.join("", I18n.locale.to_s, params[:url])
|
|
end
|
|
|
|
|
|
def question_scores
|
|
@@question_scores ||= JSON.parse(Rails.root.join("config", "question_scores.json").read)
|
|
end
|
|
|
|
|
|
def score_entry_for(stage_idx, answer_node)
|
|
stage_data = question_scores["stages"][stage_idx - 1]
|
|
return nil unless stage_data
|
|
|
|
if answer_node.parent&.chance?
|
|
chance_entry = stage_data["answers"][answer_node.parent.position - 1]
|
|
chance_entry&.dig("outcomes", answer_node.position - 1)
|
|
else
|
|
stage_data["answers"][answer_node.position - 1]
|
|
end
|
|
end
|
|
|
|
|
|
def last_save_score_entry_for(answer_node)
|
|
question_scores.dig("last_save", "answers", answer_node.position - 1)
|
|
end
|
|
|
|
|
|
def apply_score(entry)
|
|
return unless entry
|
|
|
|
current_player.add_to_score(
|
|
overall: entry["overall"].to_i,
|
|
impact: entry["impact"] || {}
|
|
)
|
|
end
|
|
end
|