You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

5.2 KiB

IKEA Foundation - Week 2026 Project

This project is a Ruby on Rails application developed for the IKEA Foundation. It is a multi-lingual, interactive game/educational tool where players progress through various stages, making choices or facing "chance" events.

Key Features

  • Multi-lingual Support: Uses the mobility gem for translating content into numerous languages. Supported locales include: en, zh, hr, cs, da, nl, fi, fr, fr-CA, de, hu, it, ja, ko, nb, pl, pt, ro, sr, sk, sl, es, sv, uk.
  • Hierarchical Content Management: A "Node" system (using ancestry) for managing pages, stages, and interactive elements.
  • Player Progression: Tracks player progress, scores, and decisions across different stages in the Player model's progress JSONB field.
  • Interactive Game Flow: Managed by GameController, featuring:
    • Stage: Presents a stage's answer nodes (good_answer, bad_answer, or chance).
    • Answer: Records the player's selection; for chance, an outcome child is sampled.
    • Stage result: Shows the outcome node and applies its score.
    • Last save / Results: Final compost-vs-landfill choice, then the scored results screen.
  • Scoring: Answer scores live in config/question_scores.json, not in the database. See below.
  • Admin Interface: A backend for managing nodes, assets (Active Storage), users, and translations.
  • Search: pg_search integration for content discovery.

Technical Stack

  • Framework: Ruby on Rails 8.1.2
  • Language: Ruby 3.4.9
  • Database: PostgreSQL
  • Asset Pipeline: Propshaft with Importmap-rails and Stimulus/Turbo.
  • Background Jobs: Sidekiq with Redis.

Core Models

  • Node: The central content model. Templates are hierarchy-dependent:
    • Root (Depth 0): start
    • Level 1: facts, intro, stage, last_save, results
    • Level 2 (answers under a stage): good_answer, bad_answer, chance
    • Level 3 (outcomes under a chance): good_answer, bad_answer
  • Player: Tracks session state, progress (per-stage answer_id / result_id), the cumulative score, and the scores hash keyed by food_waste, emissions, income.
  • Asset & Attachment: Handles media and its contextual content (body text, styling) associated with nodes.
  • User: Admin authentication and roles.

Scoring

Scores are not stored on nodes. config/question_scores.json holds them, and GameController#score_entry_for maps a node to its entry by position:

  • stages[stage_index - 1].answers[answer.position - 1] for a normal answer.
  • For a chance outcome, the parent chance node's entry is looked up the same way and the outcome is read from its outcomes[child.position - 1].

Each entry has an overall value (feeds player.score and the result band) and an impact hash of food_waste / emissions / income deltas (feed the per-category tones in GameHelper). type and early_exit are documentation only — nothing reads them.

Gotcha: because the mapping is positional, the JSON must mirror the node tree exactly. A chance answer needs a {"type": "chance", "outcomes": [...]} entry at its position; if it is flattened into sibling entries instead, the outcome lookup silently returns nil and that branch scores nothing. Verify with a bin/rails runner walk of the tree after editing.

Result band consistency guard

The two axes can diverge: a good chance outcome gives only overall: 1 where a safe good answer gives 2, so a player can accumulate green impact but a low overall — and land a "close call" headline above three positive impact texts.

GameHelper#result_state therefore raises the band from overall to at least GameHelper#tone_floor, computed from the same impact_tone values the impact texts use: all three positive floors at :best, none negative floors at :balanced. The guard only lifts, never lowers — a high overall still reaches :best, and any negative metric still allows :worst. The early-exit / last-save branch returns before the guard and is unaffected.

Consequence: the headline and the three impact texts are no longer independent. Retuning impact values in question_scores.json can move the headline even when overall is untouched, and changing IMPACT_TONE_BANDS changes both the texts and the band floor.

Results copy

Each impact category needs three distinct tone strings — positive, neutral, negative under game.results.<category> — in all 24 locale files. A missing tone is easy to miss because nothing raises: neutral was a verbatim copy of positive in every locale, so an income: 0 player was told they "earned some money". When touching these, sweep all locales and check the three strings actually differ, not just that the key exists.

Project Structure

  • app/controllers/admin/: Admin backend logic.
  • app/controllers/game_controller.rb: Main game loop (stage -> answer -> result -> results).
  • app/controllers/api/: JSON endpoints (see docs/leaderboard_api.md).
  • app/helpers/game_helper.rb: Result bands and per-impact tone thresholds.
  • app/models/concerns/: Shared logic for ancestry, attachments, and tags.
  • config/locales/: YAML translation files.
  • config/question_scores.json: Answer scoring table.