Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 30 commits into
base: main
Choose a base branch
from

Conversation

Ragudos
Copy link
Contributor

@Ragudos Ragudos commented Sep 21, 2023


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

Discord Username: @aaronragudos

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

  • [x ] 🍕 Feature
  • [ x] 🐛 Bug Fix
  • [ x] 🚧 Breaking Change
  • [ x] 🧑‍💻 Code Refactor
  • 📝 Documentation Update

Description

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

  • Typing Logic
  • Result Page
  • Multiplayer (Rooms)

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:

  • No one can join a room if:
  1. The room is full (specified by the constant of MAX_PARTICIPANTS)
  2. The room is ongoing, or is finished. The room's state must be === "waiting"
  3. The room does not exist
  • If a room only has one user and that user disconnects, the room will be removed. A user is considered disconnected if:
  1. They refresh the page.
  2. They navigate out of the room
  3. They enter a full room, or get errors while in a room.
  • There is a notification catcher that wraps the layout of the folder "rooms" to handle socket disconnections for:
  1. Errors
  2. Room Full
  3. Server Full (MAX_ROOMS), this is to prevent users, if they wanted, to keep creating rooms.
  4. And others.
  • A user who's logged in cannot connect to the socket again. They must be disconnected.

  • The socket uses middleware to initialize a user (displayName, displayImage, userID).

  • If a user is not logged in, the provided socket.id is used instead.

  • If a room is not "waiting" and is either on "countdown", or "running" states and a user leaves, the server will check if there is one player remaining (for example, two users will play and one leaves). If there is only one player left, then the room will go back to "waiting" state no matter what.

  • If a room owner leaves the room, the next room owner will be the player that came after the room owner.

  • When a race is finished, a room owner can restart the game.

  • We can add displayNames when joining a room using searchqueries. If there is no displayName provided, we use either:

  1. Logged in user's displayName
  2. A random username (which is constant right now)

Possible features:

  • Add a videolike replay just like in results page during multiplayer to see where each user is on a snippet
  • Add a functionality to change a snippet while waiting on a lobby.

Related Tickets & Documents

QA Instructions, Screenshots, Recordings

TESTS TAKEN ON:

  • Google Chrome
  • Brave

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?

  • 👍 yes
  • 🙅 no, because they aren't needed
  • 🙋 no, because I need help

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
Copy link
Contributor Author

Ragudos commented Sep 21, 2023

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
Copy link
Contributor Author

Ragudos commented Oct 6, 2023

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[TASK]: Refactor Multiplayer & Typing Logic
1 participant