diff --git a/assets/js/app.js b/assets/js/app.js index 321e30e..a9e5a3d 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -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", {}); } - }) + }); } } } diff --git a/lib/coderacer_web/live/game/game_live.ex b/lib/coderacer_web/live/game/game_live.ex index 7f11a03..1b7c122 100644 --- a/lib/coderacer_web/live/game/game_live.ex +++ b/lib/coderacer_web/live/game/game_live.ex @@ -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 = @@ -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 @@ -94,7 +147,6 @@ defmodule CoderacerWeb.GameLive do defp format_char_as_typed(char) do case char do - "\t" -> "" " " -> "" "\r\n" -> "\n" "\n" -> "\n" @@ -105,7 +157,6 @@ defmodule CoderacerWeb.GameLive do defp format_char_as_remaining(char) do case char do - "\t" -> "" " " -> "" "\r\n" -> "\n" "\n" -> "\n" diff --git a/lib/coderacer_web/live/start/start_live.ex b/lib/coderacer_web/live/start/start_live.ex index 9d74a6a..9a210ba 100644 --- a/lib/coderacer_web/live/start/start_live.ex +++ b/lib/coderacer_web/live/start/start_live.ex @@ -4,6 +4,8 @@ defmodule CoderacerWeb.StartLive do alias Coderacer.Game alias CoderacerWeb.Layouts + require Logger + @languages [ {"c", "C"}, {"clojure", "Clojure"}, @@ -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} -> diff --git a/test/coderacer_web/live/game_live_test.exs b/test/coderacer_web/live/game_live_test.exs index 692750c..5d4ffeb 100644 --- a/test/coderacer_web/live/game_live_test.exs +++ b/test/coderacer_web/live/game_live_test.exs @@ -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