Skip to content
Merged
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
7 changes: 4 additions & 3 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@ import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

// Hook to prevent tab default behavior
// Hook to convert tab to two spaces
const Hooks = {
PreventTab: {
mounted() {
this.el.addEventListener("keydown", (e) => {
if (e.key === "Tab") {
e.preventDefault()
e.preventDefault();
this.pushEventTo(this.el, "tab_pressed", {});
}
})
});
}
}
}
Expand Down
57 changes: 54 additions & 3 deletions lib/coderacer_web/live/game/game_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ defmodule CoderacerWeb.GameLive do
initial_state = %{streak: 0, wrong: 0}

session = Game.get_session!(id)
snippet = String.trim(session.code_challenge)
# Convert tabs to two spaces for easier typing
snippet = String.trim(session.code_challenge) |> String.replace("\t", " ")
og_image_url = url(socket, ~p"/images/og-image.png")

socket =
Expand Down Expand Up @@ -58,6 +59,58 @@ defmodule CoderacerWeb.GameLive do
end
end

@impl true
def handle_event("tab_pressed", _payload, socket) do
remaining_code_graphemes = String.graphemes(socket.assigns.remaining_code)
original_code = socket.assigns.original_code
current_position = socket.assigns.current_position
session_id = socket.assigns.session.id

case remaining_code_graphemes do
[" ", " " | t_rest] ->
# Target has two spaces, Tab press is correct
new_position = current_position + 2
new_remaining_code = Enum.join(t_rest)

new_score = %{
streak: socket.assigns.score.streak + 2,
wrong: socket.assigns.score.wrong
}

socket_after_match =
socket
|> assign(:current_position, new_position)
|> assign(:remaining_code, new_remaining_code)
|> assign(:display_code, format_code_for_typing(original_code, new_position))
|> assign(:score, new_score)

if Enum.empty?(t_rest) do
# Game finished after matching the tab
Logger.info("Finish after tab")

Game.update_session(socket.assigns.session, %{
streak: new_score.streak,
wrong: new_score.wrong,
time_completion: socket.assigns.elapsed_time.elapsed_time
})

{:noreply,
socket_after_match
|> assign(:elapsed_time, %{socket.assigns.elapsed_time | running: false})
|> push_navigate(to: "/finish/#{session_id}")}
else
# Continue game
{:noreply, socket_after_match}
end

_ ->
# Target does not have two spaces, Tab press is an error
{:noreply,
socket
|> assign(:score, %{streak: 0, wrong: socket.assigns.score.wrong + 1})}
end
end

@impl true
def handle_info(:tick, socket) do
if socket.assigns.elapsed_time.running do
Expand Down Expand Up @@ -94,7 +147,6 @@ defmodule CoderacerWeb.GameLive do

defp format_char_as_typed(char) do
case char do
"\t" -> "<span class=\"text-green-100\">⇥</span>"
" " -> "<span class=\"text-green-100 text-[12px] px-1\">⎵</span>"
"\r\n" -> "<span class=\"text-green-100 font-bold pl-1\">↵</span>\n"
"\n" -> "<span class=\"text-green-100 font-bold pl-1\">↵</span>\n"
Expand All @@ -105,7 +157,6 @@ defmodule CoderacerWeb.GameLive do

defp format_char_as_remaining(char) do
case char do
"\t" -> "<span class=\"text-slate-400 opacity-60\">⇥</span>"
" " -> "<span class=\"text-slate-400 opacity-60 text-[12px] px-1\">⎵</span>"
"\r\n" -> "<span class=\"text-slate-400 opacity-60 font-bold pl-1\">↵</span>\n"
"\n" -> "<span class=\"text-slate-400 opacity-60 font-bold pl-1\">↵</span>\n"
Expand Down
5 changes: 4 additions & 1 deletion lib/coderacer_web/live/start/start_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule CoderacerWeb.StartLive do
alias Coderacer.Game
alias CoderacerWeb.Layouts

require Logger

@languages [
{"c", "C"},
{"clojure", "Clojure"},
Expand Down Expand Up @@ -71,7 +73,8 @@ defmodule CoderacerWeb.StartLive do
}) do
{:noreply, push_navigate(socket, to: ~p"/game/#{session.id}")}
else
{:error, _status, _error} ->
{:error, _status, error} ->
Logger.error("Error generating code: #{inspect(error)}")
{:noreply, put_flash(socket, :error, "🤖 Error generating code. Rate limit exceeded.")}

{:error, _changeset} ->
Expand Down
30 changes: 30 additions & 0 deletions test/coderacer_web/live/game_live_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,35 @@ defmodule CoderacerWeb.GameLiveTest do
# Should track both correct and incorrect characters
assert html =~ "Time"
end

test "tab_pressed advances by two when next two chars are spaces", %{conn: conn} do
session = session_fixture(%{code_challenge: " abc"})
{:ok, view, _html} = live(conn, "/game/#{session.id}")

# Simulate tab_pressed event
render_hook(view, "tab_pressed", %{})

# Should advance by two spaces (streak should be 2, remaining_code should start with "a")
html = render(view)
# streak
assert html =~ "2"
# next char in code
assert html =~ "a"
end

test "tab_pressed increases error when next two chars are not spaces", %{conn: conn} do
session = session_fixture(%{code_challenge: "ab"})
{:ok, view, _html} = live(conn, "/game/#{session.id}")

# Simulate tab_pressed event
render_hook(view, "tab_pressed", %{})

# Should increase wrong count (wrong should be 1, streak should be 0)
html = render(view)
# wrong
assert html =~ "1"
# streak
assert html =~ "0"
end
end
end