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