Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,6 @@ Metrics/ClassLength:

Metrics/AbcSize:
Enabled: false

Metrics/CyclomaticComplexity:
Max: 8
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ GEM
bindex (0.8.1)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (6.2.2)
brakeman (7.0.2)
racc
builder (3.3.0)
capybara (3.40.0)
Expand Down
9 changes: 7 additions & 2 deletions app/assets/stylesheets/scoreboard.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.scoreboard-container {
max-height: calc(100vh - 4rem);
overflow-y: auto;
margin-top: 5rem;
}

Expand Down Expand Up @@ -36,6 +35,12 @@
font-weight: bold;
}

.scoreboard li.losers {
color: #999;
pointer-events: none;
opacity: 0.6;
}

.scoreboard-player-info {
display: flex;
align-items: center;
Expand All @@ -48,7 +53,7 @@
height: 40px;
border-radius: 50%;
object-fit: cover;
border-radius: var(--pico-border-radius, 2rem);
border-radius: var(--pico-border-radius, 2rem);
}

.scoreboard-player-name {
Expand Down
65 changes: 65 additions & 0 deletions app/assets/stylesheets/streaks.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
:root {
--game-font: "Press Start 2P", cursive;
--sassy: #bf4080;
--coral: #fe5f55;
--royal: #6f2ed8;
--tiffany: #00b4d8;
--peach: #e6f14a;
--off-white: #fffff7;
}

.wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
margin: auto;
text-align: center;
}

.neon-text {
position: relative;
margin: 0;
font-family: var(--game-font);
font-size: 1rem;
width: 100%;
text-transform: uppercase;
-webkit-text-stroke: 1px var(--sassy);
color: var(--off-white);
z-index: 10;
}

.neon-text::before {
content: attr(data-text);
position: absolute;
bottom: 8px;
right: 8px;
font-family: inherit;
font-size: 1rem;
color: var(--sassy);
width: 100%;
height: 100%;
animation: animateTextColor 3s infinite linear;
z-index: -1;
}

@keyframes animateTextColor {
0% {
color: var(--tiffany);
}
20% {
color: var(--coral);
}
50% {
color: var(--royal);
}
60% {
color: var(--peach);
}
80% {
color: var(--sassy);
}
100% {
color: var(--tiffany);
}
}
10 changes: 5 additions & 5 deletions app/controllers/game_player_answers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ class GamePlayerAnswersController < ApplicationController

# POST /games/:id/player_answer
def create
time_at_answer = Time.current
time_taken = @game_question.started_at.present? ? (time_at_answer - @game_question.started_at).round : nil

player_answer = PlayerAnswer.build(
game_player: @game_player,
game_question: @game_question,
answer: @answer,
correct: @answer.correct
correct: @answer.correct,
time_taken: time_taken
)

if player_answer.save
if @answer.correct
@game_player.update(points: @game_player.points + @game_question.question.points)
end

render json: { message: "Answer submitted successfully" }, status: :ok
else
render json: { error: @player_answer.errors.full_messages }, status: :unprocessable_entity
Expand Down
73 changes: 67 additions & 6 deletions app/models/game_player.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
#
# Table name: game_players
#
# id :integer not null, primary key
# points :integer default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
# game_id :integer not null
# user_id :integer not null
# id :integer not null, primary key
# current_streak :integer default(0), not null
# points :integer default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
# game_id :integer not null
# user_id :integer not null
#
# Indexes
#
Expand All @@ -26,8 +27,68 @@ class GamePlayer < ApplicationRecord

validates :points, numericality: { greater_than_or_equal_to: 0 }

# @param [GameQuestion]
# @return [PlayerAnswer, nil]
def find_answer_for(game_question)
player_answers.find_by(game_question: game_question)
end

# Awards points to the game player by incrementing their total points with
# the sum of the current question points with all the bonuses they can get
#
# @param [Integer] question_points
# @param [Integer, nil] time_taken
# @return [GamePlayer]
def award_points!(question_points, time_taken)
streak_bonus = calculate_streak_bonus(question_points)
speed_bonus = calculate_speed_bonus(time_taken)

with_lock do
update_columns(
points: points + question_points + streak_bonus + speed_bonus,
current_streak: streak_length
)
end
end

private

# Calculates the bonus points a player earns for answering quickly.
# A simple tiered bonus is applied.
#
# time_taken is expected to be >= 0, or nil
#
# @param [Integer, nil] time_taken
# @return [Integer]
def calculate_speed_bonus(time_taken)
return 0 if time_taken.blank? || time_taken.negative? || time_taken > 8

case time_taken
when 0..2 then 3
when 2..4 then 2
when 4..6 then 1
else 0
end
end

# Calculates streak bonus based on the player's current
# streak length, the bonus is calculated by
# multiplying the question points with the streak length
# and the bonus multiplier which is 25%
#
# @param [Integer] question_points
# @return [Integer]
def calculate_streak_bonus(question_points)
return 0 if streak_length == 1

(question_points * streak_length * 0.25).round
end

# @return [Integer]
def streak_length
recent_answers = player_answers.order(created_at: :desc).pluck(:correct)
streak = recent_answers.take_while { |correct| correct }.count
[streak, 1].max
end

end
33 changes: 33 additions & 0 deletions app/models/game_question.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#
# id :integer not null, primary key
# current_phase :integer default("idle"), not null
# started_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# game_id :integer not null
Expand All @@ -24,8 +25,40 @@ class GameQuestion < ApplicationRecord
belongs_to :question
has_many :player_answers, dependent: :destroy

before_save :set_started_at_if_answering
before_save :reward_players_if_completed

def answers_count
player_answers.group(:answer_id).count
end

private

def set_started_at_if_answering
return unless current_phase_changed?(from: :reading, to: :answering)
return unless started_at.nil?

self.started_at = Time.current
end

def reward_players_if_completed
return unless current_phase_changed?(from: :answering, to: :completed)

all_players = game.game_players.includes(:player_answers)
all_players.find_each do |player|
current_question_answer = player.player_answers.find_by(game_question_id: id)
unless current_question_answer

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Use of -1 as a sentinel value in player_answers creation.

Extract the -1 sentinel into a named constant and verify downstream logic handles it correctly.

Suggested implementation:

class GameQuestion < ApplicationRecord
  INVALID_ANSWER_SENTINEL = -1
      player.player_answers.create(game_question: self, answer_id: GameQuestion::INVALID_ANSWER_SENTINEL, correct: false)

Ensure all references to the sentinel value in related downstream logic now use GameQuestion::INVALID_ANSWER_SENTINEL instead of the literal -1.

player.player_answers.create(game_question: self, answer_id: -1, correct: false)
end

if current_question_answer&.correct
player.award_points!(question.points, current_question_answer.time_taken)
else
player.update(current_streak: 0)
end
end

true
end

end
3 changes: 2 additions & 1 deletion app/models/player_answer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#
# id :integer not null, primary key
# correct :boolean not null
# time_taken :integer
# created_at :datetime not null
# updated_at :datetime not null
# answer_id :integer not null
Expand All @@ -20,7 +21,7 @@
#
class PlayerAnswer < ApplicationRecord

belongs_to :answer
belongs_to :answer, optional: true
belongs_to :game_player
belongs_to :game_question

Expand Down
8 changes: 4 additions & 4 deletions app/models/question.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# Table name: questions
#
# id :integer not null, primary key
# duration :integer default(120), not null
# duration :integer default(40), not null
# points :integer default(1)
# position :integer
# text :text
Expand Down Expand Up @@ -36,13 +36,13 @@ class Question < ApplicationRecord
numericality: {
only_integer: true,
greater_than: 0,
less_than_or_equal_to: 100
less_than_or_equal_to: 10
}
validates :duration,
numericality: {
only_integer: true,
greater_than: 30,
less_than_or_equal_to: 240
greater_than_or_equal_to: 10,
less_than_or_equal_to: 100
}

validate :image_size
Expand Down
2 changes: 2 additions & 0 deletions app/views/games/components/_players_status.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
</li>
<% end %>
</ul>
<hr>
<p>Players joined (<%= game.players.count %>)</p>
<% else %>
<p>Waiting for players to join...</p>
<% end %>
Expand Down
6 changes: 6 additions & 0 deletions app/views/games/components/_scoreboard.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
<div class="scoreboard-player-info">
<%= image_tag player.user.image, alt: player.user.name, width: 40, class: "scoreboard-players-avatar" %>
<span class="scoreboard-player-name"><%= player.user.name %></span>
<% if player.current_streak > 1 && game.ended_at.nil? %>
<div class='wrapper'>
<% streak_text = "Streak of #{player.current_streak}!" %>
<p class="neon-text" data-text="<%= streak_text %>"><%= streak_text %></p>
</div>
<% end %>
</div>
<span class="scoreboard-player-points"><%= player.points %> pts</span>
</li>
Expand Down
2 changes: 1 addition & 1 deletion app/views/games/screens/_start.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<% if host_user? %>
<hr>
<h4>
Invite players by sending them a
Invite players by sending them a
<%= link_to root_url + @game.key, data: { controller: "clipboard", action: "click->clipboard#copy", tooltip: "copy" } do %>
<span>link to the game</span>
<%= image_tag "copy.svg", width: 20, alt: "Copy link" %>
Expand Down
1 change: 1 addition & 0 deletions app/views/layouts/_head.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<link rel="icon" href="/icon.png" type="image/png">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.png">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">

<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
Expand Down
4 changes: 2 additions & 2 deletions app/views/questions/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
<%= form.textarea :text, required: true %>

<%= form.label :points %>
<%= form.number_field :points, min: 0, max: 100 %>
<%= form.number_field :points, min: 0, max: 10 %>

<%= form.label :duration %>
<%= form.number_field :duration, min: 30, max: 240 %>
<%= form.number_field :duration, min: 10, max: 100 %>

<%= form.label :image %>
<% if question.image.representable? %>
Expand Down
7 changes: 7 additions & 0 deletions db/migrate/20250401145708_add_started_at_to_game_questions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddStartedAtToGameQuestions < ActiveRecord::Migration[8.1]

def change
add_column :game_questions, :started_at, :datetime
end

end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddCurrentStreakToGamePlayers < ActiveRecord::Migration[8.1]

def change
add_column :game_players, :current_streak, :integer, default: 0, null: false
end

end
7 changes: 7 additions & 0 deletions db/migrate/20250408114324_add_time_taken_to_player_answers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddTimeTakenToPlayerAnswers < ActiveRecord::Migration[8.1]

def change
add_column :player_answers, :time_taken, :integer
end

end
Loading