leetcode-cli icon indicating copy to clipboard operation
leetcode-cli copied to clipboard

Leetcode contests

Open 152334H opened this issue 3 years ago • 0 comments

TL;DR

You can play leetcode contests on this fork. I played last weekend's contests with this; see the User Experience example.

I want feedback for anything and everything -- code smells, CLI design, even "this sucks and I'm rejecting it".

New feature: Playing with contests in leetcode-cli

This is a short overview of the work I've done to get leetcode contests working in this app. First, I cover the client-server relationship for leetcode contests. Then, I briefly explain the architectural decisions I made in implementation. Finally, I demonstrate what usage of the new leetcode contest command looks like, and go over possible changes.

The git commit history for this PR is dirty; it might be easier to see the files changed here.

Map of the Leetcode contest API

Contests are stored in the leetcode backend as the ContestNode type (see next section), which mostly corresponds to the information available at leetcode.com/contest/$slug. We can retrieve information by querying

  • sending a GET request to https://leetcode.com/contest/api/info/$contest_slug, or
  • querying the graphql API for { contest(titleSlug: String!) }

Before a contest begins, users must register for the contest to participate in it. This can be done with a simple empty POST request to

  • https://leetcode.com/contest/api/$contest_slug/register

Once a contest starts, the client needs to make additional requests to the leetcode API to get information about the contest problems. For normal leetcode.com users, the information appears to be dumped directly into HTML inside a <script> tag. I did not want to implement/import a full blown HTML parser || use a horrible string/regex search hack, so I found an alternative:

  • Contest problems can be queried from the graphql API like normal problems (i.e. both are accessible via { question(titleSlug: String!) } queries). There are some differences between the data returned for a contest problem and a normal problem, and they will need to be handled later in the CLI's code.

After the user finishes implementing the solution for a problem, they need to submit their code to be judged by the leetcode runtime. A dedicated contest API submission route must be used; running code via the normal API routes "works" but doesn't count for gaining contest points.

  • https://leetcode.com/contest/api/$contest/problems/$slug/interpret_solution/
  • https://leetcode.com/contest/api/$contest/problems/$slug/submit/

Users might also want to check the contest scoreboard to see their position. I have not worked on querying/implementing this yet.

Interlude: what's what the fun command?

Leetcode's API is not documented (at all). I discovered all of the information in the section above by a mixture of the Firefox Dev Console && unsolicited queries to leetcode.com/graphql. The latter is what the fun subcommand is for; I made it as a quick debugging tool to enumerate leetcode's graphql API.

As an example, you can get the structure of a ContestQuestionNode like this:

$ leetcode f -t ContestQuestionNode | jq .
  "data": {
    "__type": {
      "name": "ContestQuestionNode",
      "fields": [
        {
          "name": "credit",
          "type": {
            "name": null,
            "kind": "NON_NULL",
            "ofType": {
              "name": "Int",
              "kind": "SCALAR"
            }
          }
        },
        {
          "name": "title",
          "type": {
            "name": "String",
            "kind": "SCALAR",
            "ofType": null
          }
        },
        {
          "name": "titleSlug",
          "type": {
            "name": "String",
            "kind": "SCALAR",
            "ofType": null
          }
        },
        {
          "name": "questionId",
          "type": {
            "name": "String",
            "kind": "SCALAR",
            "ofType": null
          }
        }
      ]
    }
  }

None of this is needed for a normal user of leetcode-cli, of course, so I will probably remove it unless you think it would be a good idea to keep it.

IMPLEMENTATION

So, how does the API translate to code?

We need some way to expose the following operations to the user: 1, get contest info (given a slug, like"weekly-contest-295") 2. register for a contest 3. get contset problem info 4. submit code to test/run on contest problems

Because contest problems are structurally identical to normal leetcode problems, (4) is actually already solved -- the code for the test/exec commands can be used here, with a little modification. (3) is also mostly solved by the existing code, but there are a few issues:

  • The existing code relies on the normal problem url (conf.sys.urls["problem"]) to read problem descriptions, so I added a longer graphql query (Leetcode::get_contest_question_detail) for that
  • The QuestionNode data from leetcode for each problem will be slightly modified after a contest ends && the contest problems are republished as normal problems. This causes a user's code for a contest problem to "disappear" from leetcode-cli after a contest, because the frontendQuestionId for each problem changes && the user's code file in ~/.leetcode/code is no longer tracked properly by leetcode-cli. I have some ideas to handle this, but I haven't done anything about it yet.

That leaves (2) and (1). I added the Contest and ContestQuestionStub structs to models.rs; they represent the ContestNode and ContestQuestionNode types from the leetcode graphql API. Corresponding methods were added in leetcode,rs, cache/mod.rs, and parser.rs.

The data for the contest structs could (and should) easily be cached, but for the time being I have only implemented direct queries for these structs from the leetcode API.

I've also made a substantial number of changes to existing bodies of code, so it's entirely possible I've accidentally broken a feature or two at some point. I've tried to add TODOs to places where I think the code will probably need to change, but there is probably more.

UX

Currently, the end-user experience with leetcode-cli looks like this:

$ leetcode contest weekly-contest-295 -ru
started 1 seconds ago
[weekly-contest-295] Weekly Contest 295
fID    Points Title
------|------|----------------------
2372  |    3 | Rearrange Characters to Make Target String
2373  |    4 | Apply Discount to Prices
2374  |    5 | Steps to Make Array Non-decreasing
2375  |    6 | Minimum Obstacle Removal to Reach Corner
$ leetcode e 2372 # work on the problem
$ leetcode t -c weekly-contest-295 2372 # test solution
Accepted       Runtime: 35 ms

Your input:    "ilovecodingonleetcode"↩ "code"
Output:        2
Expected:      2
$ leetcode x -c weekly-contest-295 2372 # exec solution
Success

Runtime: 64 ms, faster than 11% of Python3 online submissions for Rearrange Characters to Make Target String.

Memory Usage: 13.7 MB, less than 98% of Python3 Rearrange Characters to Make Target String.

This system works well enough, but it could definitely be a lot more ergonomic. I was hoping to implement an ncurses-like interface for contests, but

  • Because of how the other subcommands are implemented, a significant amount of refactoring would be needed to edit/execute code without recursively calling leetcode-cli as a subprocess
  • Adding an interactive UI would cost a lot more lines of code & maintenance. Ncurses in particular (and its rust wrapper, pancake) is not very good at the thread safety / async thing.

Nonetheless, the current output of the contest command is rather ugly, and more work ought to be done here.

End

152334H avatar May 31 '22 03:05 152334H