webdevcody / code-racer

https://code-racer-eight.vercel.app
MIT License
694 stars 514 forks source link

#716 | [TASK]: Refactor Multiplayer & Typing Logic #721

Open Ragudos opened 1 year ago

Ragudos commented 1 year ago

title: #716 | [TASK]: Refactor Multiplayer & Typing Logic

Discord Username: @aaronragudos

What type of PR is this? (select all that apply)

Description

Large refactor of the race logic of the game. Namely:

First, I made the logic for typing, instead of handleKeyDowns, to listen to onChange. This is simpler, in my opinion, since we won't be doing slices, and such, as much as possible. Refactored the typing logic to be reusable as well using useReducer, and added some optimizations (useMemo, useCallback, and memo). Second, I added a button in practice to get a new snippet if a user wanted to.

Third, I finished the prototype for the room multiplayer functionality using the new typing logic. Lastly, I refactored the websocket server to separate the memory/state handling and the logic for the game (checking for status, etc.). I also made it clear about what a function does (well, not all of them, could do with more abstraction, but it's good for now).

Take note: The current rooms implementation does not do any call to the database except for when getting a snippet (getRandomSnippet).

The client will only connect to the socket if a user want to play multiplayer. I added autoConnect: false to the socket config.

Securities:

Possible features:

Related Tickets & Documents

QA Instructions, Screenshots, Recordings

TESTS TAKEN ON:

How can you test the changes?

For typing changes (on practice):

  1. Go to Practice Race

  2. Try to choose a language that does not exist in your local database. A message like "Uh oh", should show.

  3. Now, try to choose a language that exists.

  4. Click on the snippet, a focus indicator should then pop up (outline) on the black rectanlge.

  5. The snippet should not overflow and should break into new lines.

  6. As you type, the progress bar should track the changes and the row line tracker (the number on the side) should track the current line you are in. The row line tracker will based on whether you have typed a new Enter (⏎) character.

  7. The timer should be running at this point.

  8. Try clicking Reset.

  9. This should reset your progress and time.

  10. Now type something again and click Get New Snippet.

  11. A loading indicator "Getting Snippets..." should replace the text on the button and the whole black rectangle should be disabled (buttons, textarea, etc.).

  12. Try finishing a race.

  13. The result page should: a. use sessionStorage b . Display the words you typed for each timestamp (for example, on the first timestamp, you would have "c", then on the next, it will be "co", and on another, "coo", etc.). c. The graph should be displayed and on each hover of the timestamp, the snippet displayed below should follow (highlight) the latest character type. d. Not have topten and history tabs e. Should have a replay f. Be able to play again with the snippet you used.

  14. The replay should: a. Play on pressing play button. b. Stop when pressing pause c. Restart and pause when pressing rotating arrow button d. Stop playing and is paused when the replay finishes. e. Play from the top upon playing the play button when the replay has finsihed. f. Should follow how you typed everything on the snippet.

For multiplayer:

  1. Go to /race.
  2. Click Go Now!
  3. You should be at /race/rooms
  4. Try creating a room
  5. If you aren't logged in, you should have a random username (like, ImTheBestTyper123) and a fallback image.
  6. There should be toasts that notify you of: a. Room is being created b. Room has been created, then the room should show up.
  7. You should be at /race/rooms/[roomID]
  8. Try refreshing the page
  9. You should be kicked out since this room would be deleted, saying (roomID not found). This is because there's only one player in the room.
  10. Try creating a new one (Should be the same process).
  11. Now, try joining that room while logged in on the other tab.
  12. You should not be allowed to connect and a toast should pop up saying that you are already connected.
  13. Now, try joining that room on a tab where you aren't logged in. You should be able to join.
  14. First, of cousrse, you should be able to use a displayName you desire.
  15. On the currently connected clients, there should be a notification (a toast should pop up) about the name of the user that joined and the visuals updated.
  16. Okay, now try pressing backspace or leaving the current page (/race/rooms/[roomID]).
  17. You should see that the client that left the page left the room and this should be notified to the currently connected clients and update the visuals.
  18. Join the room again
  19. On the client that's the owner of the room, try leaving. The new owner should be the other client that just joined.
  20. Join again.
  21. Try to start the race.
  22. There should be a countdown that happens.
  23. Try leaving on one of the clients.
  24. The game should revert back to "waiting" since only one client would be connected.
  25. Join again
  26. Click start or play
  27. After the countdown, the snippet should show up along with the progress tracker of all connected clients. The timer should have started, but right now, it's a bit buggy where it will start once a connected client type (try playing around with it to see what I mean).
  28. As you type on one client, that client's progress should be updated live to all clients.
  29. The client with the most progress should have its progress tracker on top (It's sorted).
  30. When a client finishes while the others are not yet finished, you should see on a finished client a live representation of the average of all the timestamps of all the clients.
  31. Before the race finishes (It will only be considered as finish if every client's progress is the MAX, which is 100) , try leaving the room.
  32. The race should revert back to "waiting"
  33. Try to redo everything and finish.
  34. A table should show the details of every client.
  35. Now, try leaving on one client. The connected client should still see the table displayed.
  36. Try rejoining on the client that just left. They should be notified by a toast that the game has just finished.
  37. Try playing again.
  38. Rejoin.
  39. Redo everything.

https://discord.com/channels/663478877355507769/1126627632461643797/1150827943749226647

NOTE: This change needs more refactoring just to make things cleaner and add the integration of the database.

UI accessibility concerns?

Added/updated tests?

E2E tests still aren't updated, so I'm not sure how it will affect stuff. Don't worry, I believe the multiplayer mode will only show up on development for this version.

[optional] Are there any post deployment tasks we need to perform?

[optional] What gif best describes this PR or how it makes you feel?

Very fine. I've enjoyed tinkering around with the codebase

Ragudos commented 1 year ago

Here it is. Take note that I've changed the logic from keydowns to onchange. If we'd like it to still be keydown, then it won't be a problem.

Ragudos commented 1 year ago

So, I reran it again, and it still fails. I checked the following that failed, and they exist Namely:

[data-cy='language-dropdown'] and [data-cy='search-language-input']:

 <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          className={cn("justify-between w-full px-4 py-3", className)}
          data-cy="language-dropdown"
        >
          {value
            ? snippetLanguages.find((language) => language.value === value)
              ?.label
            : "Select language..."}
          <ChevronsUpDown className="w-4 h-4 ml-2 opacity-50 shrink-0" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-full p-0 h-44">
        <Command>
          <CommandInput
            placeholder="Search a Language..."
            value={search}
            onValueChange={setSearch}
            data-cy="search-language-input"
          />
          <CommandEmpty>No language found.</CommandEmpty>
          <CommandGroup className="overflow-y-auto">
            {snippetLanguages
              .filter((language) =>
                language.label.toLowerCase().includes(search.toLowerCase()),
              )
              .map((language) => (
                <CommandItem
                  key={language.label}
                  value={language.value}
                  onSelect={(currentValue) => {
                    const parsedValue = languageTypes.parse(currentValue);
                    onChange(parsedValue);
                    window.localStorage.setItem(
                      "codeLanguage",
                      parsedValue,
                    );
                    setOpen(false);
                  }}
                  data-cy={`${language.value}-value`}
                >
                  <Check
                    className={cn(
                      "mr-2 h-4 w-4",
                      value === language.value ? "opacity-100" : "opacity-0",
                    )}
                  />
                  {language.label}
                </CommandItem>
              ))}
          </CommandGroup>
        </Command>
      </PopoverContent>
    </Popover>

Practice Race Card or [data cy='practice-card']:

    <Card
      className="flex flex-col justify-between flex-1 border-2 border-warning"
      data-cy="practice-card"
    >
      <CardHeader>
        <div className="grid text-center place-content-center">
          <Target className="justify-self-center" size={40} />
          <h2
            style={bruno_ace_sc.style}
            className="text-3xl font-bold text-warning"
          >
            Practice
          </h2>
          <p className="font-light">
            Practice typing with a random snippet from your snippets
          </p>
        </div>
      </CardHeader>
      <CardContent>
        <form
          onSubmit={handleSubmit}
          className="grid items-start grid-cols-2 gap-2"
        >
          <div className="flex flex-col">
            <LanguageDropDown
              className={cn(
                "w-full",
                buttonVariants({ variant: "ghost" }),
                error && "border-red-500",
              )}
              value={selectedPracticeLanguage}
              onChange={handleSetCodeLanguage}
            />
            <span className="text-red-500">{error}</span>
          </div>
          <Button
            disabled={selectedPracticeLanguage === undefined}
            variant="black"
            className="relative justify-start border"
            data-cy="practice-button"
          >
            Practice{" "}
            <ArrowRight
              size="20"
              className="absolute -translate-y-1/2 right-4 top-1/2"
            />
          </Button>
        </form>
      </CardContent>
    </Card>