Skip to content
This repository has been archived by the owner on Jun 24, 2024. It is now read-only.

Commit

Permalink
Merge pull request #54 from technologiestiftung/feat/games-resource
Browse files Browse the repository at this point in the history
feat: creating games and dice in UI
  • Loading branch information
dnsos authored Sep 29, 2023
2 parents dac1569 + 99b7c28 commit 2961be6
Show file tree
Hide file tree
Showing 22 changed files with 233 additions and 30 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# (Semi-serious) Idea Machine

## Todo

- [x] introduce Games and make all dice _optionally_ belong to a game
- [x] associate existing dice to first Game
- setup resourceful routes e.g. /games/1 and /api/v2/games/1/rolls
- deprecate old API route
- build UI for creating new Games (only creation, no updates, no delete for now)
- create dice and sides together with Game creation

```ruby
Die.all.each do |die|
die.game = Game.first
die.save!
end
```

Web app that creates ChatGPT-generated ideas for the digitalization of Berlin. The ideas are explicitly semi-serious and are intended as a conversation starter. The ideas are generated from physical die rolls that emit their result side and POST it to the web app. Each side is associated with a term such as "Web-App", "Gesundheit", etc. that serves as input to the idea generation.

![Technical setup of the app](/public/idea-machine-setup-v2.png)
Expand Down
26 changes: 26 additions & 0 deletions app/controllers/games_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,30 @@ def show
@latest_rolls << latest_roll unless latest_roll.blank?
end
end

def new
@game = Game.new
end

def create
@game = Game.new(game_params)

respond_to do |format|
if @game.save
format.html do
redirect_to game_path(@game), notice: "Willkommen zum neuen Spiel! POSTe Würfelereignisse an den Pfad: #{api_v2_game_rolls_path(@game)}"
end
else
format.html do
render :new, status: :unprocessable_entity
end
end
end
end

private

def game_params
params.require(:game).permit(:title, dice_attributes: [:title, :shortcode, sides_attributes: [:title, :shortcode, :variations]])
end
end
9 changes: 9 additions & 0 deletions app/helpers/die_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module DieHelper
def valid_die_values
values = []
Die.titles.keys.each_with_index do |title, index|
values << {title: title, shortcode: Die::VALID_SHORTCODES[index]}
end
values
end
end
8 changes: 8 additions & 0 deletions app/javascript/controllers/remove_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
remove(event) {
event.preventDefault();
this.element.remove();
}
}
1 change: 1 addition & 0 deletions app/models/die.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class Die < ApplicationRecord

belongs_to :game, optional: true
has_many :sides, dependent: :destroy
accepts_nested_attributes_for :sides
has_many :rolls, through: :sides

enum :title, {"focus_group" => 0, "topic" => 1, "medium" => 2}
Expand Down
2 changes: 2 additions & 0 deletions app/models/game.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
class Game < ApplicationRecord
has_many :dice, dependent: :destroy
has_many :sides, through: :dice
accepts_nested_attributes_for :dice

validates :title, presence: true
end
15 changes: 12 additions & 3 deletions app/models/side.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
class Side < ApplicationRecord
serialize :variations
after_initialize do |side|
side.variations ||= [side.title] if side.variations.blank?
end

after_initialize :normalize_variations

after_commit :roll_side, on: [:create], if: proc { |side| side.die.rolls.empty? }

belongs_to :die
Expand All @@ -13,6 +13,15 @@ class Side < ApplicationRecord

private

def normalize_variations
case variations
when ->(v) { v.blank? }
self.variations = [title]
when String
self.variations = variations.split(";")
end
end

def roll_side
Roll.create side: self
end
Expand Down
54 changes: 54 additions & 0 deletions app/views/games/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<%= form_with(model: game, class: "grid gap-6 grid-cols-3 items-start") do |form| %>
<div class="col-span-3 grid gap-y-1">
<%= form.label :title, "Titel des Spiels" %>
<%= form.text_field :title, class: "w-full" %>
<% if game.errors.key?(:title) %>
<ul class="text-danger mt-1">
<% game.errors.full_messages_for(:title).each do |error_message| %>
<%= tag.li class: "text-warning" do %>
<%= error_message %>
<% end %>
<% end %>
</ul>
<% end %>
</div>

<% valid_die_values.each_with_index do |obj, index| %>
<%= tag.div class: "grid gap-y-6" do %>
<%= form.fields_for "dice_attributes[#{index + 1}]", Die.new do |die_form| %>
<%= die_form.hidden_field :title, value: obj[:title] %>
<%= die_form.hidden_field :shortcode, value: obj[:shortcode] %>

<%= tag.div class: "scale-75" do %>
<%= render DieComponent.new(theme: obj[:title]) do %>
<%= obj[:title] %>
<% end %>
<% end %>

<% 6.times do |index| %>
<%= die_form.fields_for "sides_attributes[#{index + 1}]", Side.new do |sides_form| %>
<%= tag.div class: "grid" do %>
<%= sides_form.label :title, "Sichtbares Label", class: class_names("sr-only", {"not-sr-only": index == 0}) %>
<%= sides_form.text_field :title, class: "w-full" %>
<%= sides_form.hidden_field :shortcode, value: index + 1 %>
<details class="pt-1 pb-3 px-3 open:bg-gray-100">
<summary class="text-gray-700">Abweichende Begriffe?</summary>
<div class="mt-3">
<%= sides_form.text_field :variations, class: "w-full", aria_describedby: "variationsHint" %>
<%= tag.span "Bitte separariert mit Semikolon (;)", id: "variationsHint", class: "text-sm" %>
<%= tag.p "Nur nötig, wenn du andere Begriffe als das sichtbare Label für die Ideengenerierung benutzen willst.", class: "mt-2 text-sm" %>
<%= tag.p "Beispiel 1: Das sichtbare Label ist \"?\", soll aber nicht zur Ideengenerierung benutzt werden. Dann trage unten bitte etwas ein wie z.B. \"Begriff A;Begriff B;Begriff C\" (ohne Anführungszeichen)", class: "mt-2 text-sm" %>
<%= tag.p "Beispiel 2: Das sichtbare Label ist \"Berliner:innen\", du möchtest aber zusätzlich \"Berliner\" und \"Berlinerinnen\" als mögliche Option. Dann trage unten bitte ein: \"Berliner:innen;Berliner;Berlinerinnen\" (ohne Anführungszeichen)", class: "mt-2 text-sm" %>
</div>
</details>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>

<div class="col-span-3 justify-self-end">
<%= form.submit "Spiel erstellen", class: "bg-gray-900 text-white p-3 uppercase font-bold cursor-pointer" %>
</div>
<% end %>
1 change: 1 addition & 0 deletions app/views/games/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
<% end %>
<% end %>
<% end %>
<%= link_to "Neues Spiel anlegen", new_game_path, class: "underline" %>
<% end %>
7 changes: 7 additions & 0 deletions app/views/games/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<%= tag.div class: class_names("w-full max-w-screen-lg mx-auto", "px-8 pt-0", "grid gap-y-10") do %>
<%= tag.div class: "flex gap-x-3 items-center" do %>
<%= tag.h1 "Neues Spiel anlegen", class: "text-5xl font-bold" %>
<%= tag.span "Beta", class: "h-min border-4 px-2 py-1 border-gray-900 font-bold uppercase" %>
<% end %>
<%= render "form", game: @game %>
<% end %>
3 changes: 2 additions & 1 deletion app/views/games/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@
<%= turbo_stream_from "idea-stream" %>
<%= render partial: "ideas/idea_placeholder" %>
<% end %>
<% end %>
<% end %>

10 changes: 9 additions & 1 deletion app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,16 @@
<%= javascript_importmap_tags %>
</head>

<body class="min-h-screen w-full grid grid-rows-[auto,1fr]">
<body class="min-h-screen w-full grid grid-rows-[auto,auto,1fr]">
<%= render "shared/header" %>
<% if notice %>
<div class="px-6">
<div data-controller="remove" class="container mx-auto mb-8 max-w-screen-lg py-3 px-4 bg-gray-900 text-white font-bold flex gap-2 justify-between">
<%= tag.p notice %>
<button type="button" data-action="click->remove#remove" aria-label="Schließen"></button>
</div>
</div>
<% end %>
<main class="container mx-auto h-full max-w-screen-2xl grid grid-cols-1 grid-rows-[5fr,7fr] gap-y-12">
<%= yield %>
</main>
Expand Down
20 changes: 0 additions & 20 deletions app/views/pages/home.html.erb

This file was deleted.

4 changes: 3 additions & 1 deletion app/views/shared/_header.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<header class="container mx-auto max-w-screen-2xl py-12 px-8 flex gap-x-2 items-center justify-between">
<%= image_tag "/citylab-logo-monochrome.svg", class: "w-64 h-auto" %>
<%= link_to root_path, aria: {label: "Startseite"} do %>
<%= image_tag "/citylab-logo-monochrome.svg", class: "w-64 h-auto" %>
<% end %>
<h1 class="text-gray-900 font-extrabold text-2xl">
<%= yield :header_title %>
</h1>
Expand Down
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Rails.application.routes.draw do
root "games#index"

resources :games, only: [:index, :show]
resources :games, only: [:index, :show, :new, :create]

resources :ideas, only: [:show, :create]

Expand Down
3 changes: 3 additions & 0 deletions config/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ module.exports = {
},
gray: colors.gray,
white: "#fff",
warning: {
DEFAULT: "#ff2222",
},
},
extend: {
fontFamily: {
Expand Down
8 changes: 8 additions & 0 deletions db/migrate/20230805105911_make_game_required_for_die.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class MakeGameRequiredForDie < ActiveRecord::Migration[7.0]
def change
change_column_null :dice, :game_id, false
remove_index :dice, :shortcode
remove_index :dice, :game_id
add_index :dice, [:shortcode, :game_id], unique: true
end
end
56 changes: 56 additions & 0 deletions test/controllers/games_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,60 @@ class GamesControllerTest < ActionDispatch::IntegrationTest
get game_url(games(:summer))
assert_response :success
end

test "shows new game page" do
get new_game_url
assert_response :success
end

test "creates game" do
assert_difference("Game.count") do
post games_url, params: {game: {title: "Test game"}}
end
end

test "rejects game without title" do
assert_no_difference("Game.count") do
post games_url, params: {game: {title: nil}}
end
end

test "creates game and all associated resources" do
assert_difference -> { Game.count } => 1,
# We need 3 dice for a game:
-> { Die.count } => 3,
# Each die has 6 sides:
-> { Side.count } => 18,
# Each die creation automatically creates one initial roll:
-> { Roll.count } => 3 do
post games_url, params: valid_nested_game_params
end
end

private

def valid_nested_game_params
{
game: {
title: "Test game",
dice_attributes: [
{
title: "focus_group",
shortcode: "A",
sides_attributes: (1..6).map { |n| {title: "Side #{n}", shortcode: n} }
},
{
title: "topic",
shortcode: "B",
sides_attributes: (1..6).map { |n| {title: "Side #{n}", shortcode: n} }
},
{
title: "medium",
shortcode: "C",
sides_attributes: (1..6).map { |n| {title: "Side #{n}", shortcode: n} }
}
]
}
}
end
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require "test_helper"

class Raspi::ShutdownsControllerTest < ActionDispatch::IntegrationTest
class RaspiControllerTest < ActionDispatch::IntegrationTest
test "shows shutdown page" do
get raspi_shutdown_url
assert_response :success
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/dice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ five:
six:
title: "medium"
shortcode: "C"
game: autumn
game: autumn
2 changes: 1 addition & 1 deletion test/fixtures/games.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ autumn:
title: "My autumn dice game"

fresh:
title: "A fresh game without any dice"
title: "A fresh game without any dice"
12 changes: 12 additions & 0 deletions test/models/side_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,16 @@ class SideTest < ActiveSupport::TestCase

assert second_side.rolls.empty?
end

test "accepts variations as a string and creates array from it" do
side = Side.new(die: dice(:one), title: "Side note", variations: "entry", shortcode: 2)
assert side.valid?
assert_equal side.variations, ["entry"]
end

test "accepts variations as a semicolon-separated string" do
side = Side.new(die: dice(:one), title: "Side note", variations: "hey;there;vars", shortcode: 2)
assert side.valid?
assert_equal side.variations, ["hey", "there", "vars"]
end
end

0 comments on commit 2961be6

Please sign in to comment.