Mattias Bodlund 2 weeks ago
parent
commit
26d672af18
12 changed files with 283 additions and 3 deletions
  1. +1
    -0
      app/assets/images/ico-marketplace.svg
  2. +1
    -0
      app/assets/images/ico-storage.svg
  3. +1
    -0
      app/assets/images/ico-transport.svg
  4. +20
    -0
      app/assets/stylesheets/application.css
  5. +38
    -1
      app/controllers/game_controller.rb
  6. +18
    -0
      app/models/player.rb
  7. +16
    -1
      app/views/layouts/application.html.erb
  8. +3
    -0
      config/locales/en.yml
  9. +168
    -0
      config/question_scores.json
  10. +5
    -0
      db/migrate/20260605000000_add_scores_to_players.rb
  11. +7
    -0
      db/migrate/20260605000001_add_is_done_to_players.rb
  12. +5
    -1
      db/schema.rb

+ 1
- 0
app/assets/images/ico-marketplace.svg View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="21" viewBox="0 0 22 21"><path d="M21.75,6.8l-2-3.5V.25H2.25v3.05L.25,6.8v.2c0,.98.38,1.86,1,2.53v10.47c0,.41.34.75.75.75h18c.41,0,.75-.34.75-.75v-10.47c.62-.67,1-1.55,1-2.53v-.2ZM3.75,3.7v-1.95h14.5v1.95l1.99,3.49c-.09,1.15-1.06,2.06-2.24,2.06-1.24,0-2.25-1.01-2.25-2.25h-1.5c0,1.64-1.99,2.25-3.25,2.25s-3.25-.61-3.25-2.25h-1.5c0,1.24-1.01,2.25-2.25,2.25s-2.15-.91-2.24-2.06l1.99-3.49ZM2.75,19.25v-8.73c.39.14.81.23,1.25.23,1.26,0,2.38-.63,3.06-1.59,1.82,2.08,6.06,2.08,7.88,0,.68.96,1.8,1.59,3.06,1.59.44,0,.86-.09,1.25-.23v8.73H2.75ZM5,15.75h5v1.5h-5v-1.5Z"/></svg>

+ 1
- 0
app/assets/images/ico-storage.svg View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22"><path d="M21.91,6.57h0s-3.28-6.57-3.28-6.57H3.36L.08,6.56h0c-.05.11-.09.22-.09.34v14.33c0,.42.34.77.77.77h20.47c.42,0,.77-.34.77-.77V6.91c0-.12-.04-.24-.09-.34ZM19.99,6.14h-8.22V1.53h5.92l2.3,4.6ZM4.31,1.53h5.92v4.6H2.01L4.31,1.53ZM20.47,20.47H1.53V7.67h18.93v12.79ZM14.07,11.77h-6.14v-1.53h6.14v1.53Z"/></svg>

+ 1
- 0
app/assets/images/ico-transport.svg View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="18" viewBox="0 0 22 18"><path d="M21.6,7.58h.01s-3.5-5.01-3.5-5.01c-.14-.2-.37-.32-.61-.32h-3.25v-1.25c0-.41-.34-.75-.75-.75H1c-.2,0-.39.08-.53.22s-.22.33-.22.53v14c0,.41.34.75.75.75h2.37c.33,1.15,1.38,2,2.63,2s2.3-.85,2.63-2h4.73c.33,1.15,1.38,2,2.63,2s2.3-.85,2.63-2h2.37c.41,0,.75-.34.75-.75v-7c0-.16-.06-.3-.15-.42ZM17.11,3.75l2.45,3.5h-5.31v-3.5h2.86ZM6,16.25c-.69,0-1.25-.56-1.25-1.25s.56-1.25,1.25-1.25,1.25.56,1.25,1.25-.56,1.25-1.25,1.25ZM16,16.25c-.69,0-1.25-.56-1.25-1.25s.56-1.25,1.25-1.25,1.25.56,1.25,1.25-.56,1.25-1.25,1.25ZM20.25,14.25h-1.62c-.33-1.15-1.38-2-2.63-2s-2.3.85-2.63,2h-4.73c-.33-1.15-1.38-2-2.63-2s-2.3.85-2.63,2h-1.62V1.75s11,0,11,0v6.25c0,.41.34.75.75.75h6.75v5.5ZM8.25,5h1.5v4h-1.5v-4ZM4.25,5h1.5v4h-1.5v-4Z"/></svg>

+ 20
- 0
app/assets/stylesheets/application.css View File

@ -111,6 +111,7 @@
} }
body { body {
margin: 0; margin: 0;
display: flex; display: flex;
@ -124,6 +125,25 @@ body {
background-color: var(--clr-white); background-color: var(--clr-white);
} }
.debug-score {
position: fixed;
font-size: var(--fs-s);
right: 0;
top: 0;
padding: 1rem;
background-color: var(--clr-white);
& dl {
display: grid;
grid-template-columns: max-content max-content;
gap: 0 1ch;
& dd {
text-align: right;
}
}
}
button { button {
cursor: pointer; cursor: pointer;
} }


+ 38
- 1
app/controllers/game_controller.rb View File

@ -44,8 +44,11 @@ class GameController < ApplicationController
@answer = @answer.children.sample if @answer&.chance? @answer = @answer.children.sample if @answer&.chance?
current_player.record_answer(stage_index, @answer.id) current_player.record_answer(stage_index, @answer.id)
apply_score(score_entry_for(stage_index, @answer))
if stage_index >= n_stages
bad_chance_outcome = @answer.parent&.chance? && @answer.bad_answer?
if stage_index >= n_stages && !bad_chance_outcome
redirect_to action: :done redirect_to action: :done
else else
redirect_to action: :stage_result, id: params[:id] redirect_to action: :stage_result, id: params[:id]
@ -73,6 +76,7 @@ class GameController < ApplicationController
@answer = @node.children.find_by(id: params[:value]) @answer = @node.children.find_by(id: params[:value])
current_player.record_last_save_answer(@answer.id) current_player.record_last_save_answer(@answer.id)
apply_score(last_save_score_entry_for(@answer))
redirect_to action: :done redirect_to action: :done
end end
@ -134,4 +138,37 @@ private
# return File.join('', params[:url]) if I18n.default_locale == I18n.locale # return File.join('', params[:url]) if I18n.default_locale == I18n.locale
File.join("", I18n.locale.to_s, params[:url]) File.join("", I18n.locale.to_s, params[:url])
end 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 end

+ 18
- 0
app/models/player.rb View File

@ -5,6 +5,8 @@ class Player < ApplicationRecord
LAST_SAVE_KEY = "last_save".freeze LAST_SAVE_KEY = "last_save".freeze
SCORE_KEYS = %w[food_waste emissions income].freeze
def record_answer(stage, answer_id) def record_answer(stage, answer_id)
stage_data(stage)["answer_id"] = answer_id stage_data(stage)["answer_id"] = answer_id
@ -12,6 +14,22 @@ class Player < ApplicationRecord
end end
def add_to_score(overall:, impact: {})
self.score = score.to_i + overall.to_i
impact.each do |key, value|
key = key.to_s
next unless SCORE_KEYS.include?(key)
scores[key] = scores[key].to_i + value.to_i
end
save
end
def score_for(key)
scores[key.to_s].to_i
end
def record_result(stage, result_id) def record_result(stage, result_id)
stage_data(stage)["result_id"] = result_id stage_data(stage)["result_id"] = result_id
save save


+ 16
- 1
app/views/layouts/application.html.erb View File

@ -21,7 +21,22 @@
</head> </head>
<body> <body>
<%= content_for :header %> <%= content_for :header %>
<% if respond_to?(:current_player) && current_player %>
<aside class="debug-score">
<details open>
<summary>Debug score (<%= current_player.score %>)</summary>
<dl>
<dt>Overall</dt><dd><%= current_player.score %></dd>
<dt>Food Waste</dt><dd><%= current_player.score_for(:food_waste) %></dd>
<dt>Emissions</dt><dd><%= current_player.score_for(:emissions) %></dd>
<dt>Income</dt><dd><%= current_player.score_for(:income) %></dd>
<dt>Done</dt><dd><%= current_player.is_done %></dd>
</dl>
</details>
</aside>
<% end %>
<%= tag.main class: [@result_node, @node].compact.map { |n| n.template }.first, <%= tag.main class: [@result_node, @node].compact.map { |n| n.template }.first,
data: { data: {
controller: content_for(:main_controller) controller: content_for(:main_controller)


+ 3
- 0
config/locales/en.yml View File

@ -343,6 +343,9 @@ en:
- -
- ico-soil - ico-soil
- ico-water - ico-water
- ico-storage
- ico-transport
- ico-marketplace
sessions: sessions:
login: Log in login: Log in


+ 168
- 0
config/question_scores.json View File

@ -0,0 +1,168 @@
{
"stages": [
{
"id": "soil",
"answers": [
{
"type": "good_answer",
"overall": 2,
"impact": { "food_waste": 1, "emissions": 1, "income": 0 }
},
{
"type": "bad_answer",
"overall": 0,
"impact": { "food_waste": 0, "emissions": -2, "income": 0 }
},
{
"type": "chance",
"outcomes": [
{
"type": "good_answer",
"overall": 1,
"impact": { "food_waste": 1, "emissions": 2, "income": 0 }
},
{
"type": "good_answer",
"overall": 1,
"impact": { "food_waste": 1, "emissions": 1, "income": 0 }
}
]
}
]
},
{
"id": "water",
"answers": [
{
"type": "good_answer",
"overall": 2,
"impact": { "food_waste": 1, "emissions": 1, "income": 0 }
},
{
"type": "bad_answer",
"overall": 0,
"impact": { "food_waste": -1, "emissions": -1, "income": 0 }
},
{
"type": "chance",
"outcomes": [
{
"type": "good_answer",
"overall": 1,
"impact": { "food_waste": 1, "emissions": 2, "income": 0 }
},
{
"type": "bad_answer",
"overall": 0,
"impact": { "food_waste": -3, "emissions": -1, "income": 0 },
"early_exit": true
}
]
}
]
},
{
"id": "storage",
"answers": [
{
"type": "good_answer",
"overall": 2,
"impact": { "food_waste": 2, "emissions": 1, "income": 0 }
},
{
"type": "bad_answer",
"overall": 0,
"impact": { "food_waste": -2, "emissions": 0, "income": 0 }
},
{
"type": "chance",
"outcomes": [
{
"type": "good_answer",
"overall": 1,
"impact": { "food_waste": 2, "emissions": 0, "income": 0 }
},
{
"type": "bad_answer",
"overall": 0,
"impact": { "food_waste": -3, "emissions": -1, "income": 0 },
"early_exit": true
}
]
}
]
},
{
"id": "transport",
"answers": [
{
"type": "good_answer",
"overall": 2,
"impact": { "food_waste": 2, "emissions": -1, "income": 0 }
},
{
"type": "bad_answer",
"overall": 0,
"impact": { "food_waste": 0, "emissions": -2, "income": 0 }
},
{
"type": "chance",
"outcomes": [
{
"type": "good_answer",
"overall": 1,
"impact": { "food_waste": 1, "emissions": 2, "income": 0 }
},
{
"type": "bad_answer",
"overall": 0,
"impact": { "food_waste": -3, "emissions": -1, "income": 0 },
"early_exit": true
}
]
}
]
},
{
"id": "market",
"answers": [
{
"type": "good_answer",
"overall": 2,
"impact": { "food_waste": 1, "emissions": 2, "income": 2 }
},
{
"type": "bad_answer",
"overall": 2,
"impact": { "food_waste": 1, "emissions": 2, "income": 2 }
},
{
"type": "bad_answer",
"overall": 1,
"impact": { "food_waste": 1, "emissions": 1, "income": 1 }
},
{
"type": "bad_answer",
"overall": 0,
"impact": { "food_waste": 1, "emissions": 0, "income": -1 }
}
]
}
],
"last_save": {
"answers": [
{
"type": "good_answer",
"result_type": "compost",
"overall": 0,
"impact": { "food_waste": 1, "emissions": 1, "income": 0 }
},
{
"type": "bad_answer",
"result_type": "landfill",
"overall": 0,
"impact": { "food_waste": -2, "emissions": -3, "income": 0 }
}
]
}
}

+ 5
- 0
db/migrate/20260605000000_add_scores_to_players.rb View File

@ -0,0 +1,5 @@
class AddScoresToPlayers < ActiveRecord::Migration[8.1]
def change
add_column :players, :scores, :jsonb, default: {}, null: false
end
end

+ 7
- 0
db/migrate/20260605000001_add_is_done_to_players.rb View File

@ -0,0 +1,7 @@
class AddIsDoneToPlayers < ActiveRecord::Migration[8.1]
def change
add_column :players, :is_done, :boolean, default: false, null: false
add_index :players, :is_done
add_index :players, :locale
end
end

+ 5
- 1
db/schema.rb View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_06_03_141335) do
ActiveRecord::Schema[8.1].define(version: 2026_06_05_000001) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql" enable_extension "pg_catalog.plpgsql"
@ -116,10 +116,14 @@ ActiveRecord::Schema[8.1].define(version: 2026_06_03_141335) do
create_table "players", force: :cascade do |t| create_table "players", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.boolean "is_done", default: false, null: false
t.string "locale", null: false t.string "locale", null: false
t.jsonb "progress", default: {}, null: false t.jsonb "progress", default: {}, null: false
t.integer "score", default: 0, null: false t.integer "score", default: 0, null: false
t.jsonb "scores", default: {}, null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["is_done"], name: "index_players_on_is_done"
t.index ["locale"], name: "index_players_on_locale"
end end
create_table "quiz_results", force: :cascade do |t| create_table "quiz_results", force: :cascade do |t|


Loading…
Cancel
Save