Building a quotes game (Did I Say?)

Building a quotes game (Did I Say?)

·

10 min read

Hi, I built a quotes game for the Hashnode & Vercal hackathon.

Disclaimer: You may find bugs in this product as it is still in beta

Vercel app GitHub

The goal is to score the maximum of points on a series of questions. Each round asks you to choose the right answer of the following questions:

  • Which author said that quote?
  • To which reference this quote belongs to?

Only one of both questions is presented each time. The game randomly pick the type of guess (authors or references).

Here is an example:

You can make a series of 5, 10 or 20 questions. One right answer will give you 10 points, a wrong answer will remove 5 points and skipping a question cost 1 point.

The Backstory

During the past few months, I was building a quotes app on mobile and web with Flutter, and I had a new idea on top of the initial one. But I needed to first build a service and REST APIs for that new experiment.

I decided to postpone all these fun thoughts until I saw an opportunity with the Hashnode/Vercel hackathon.

Because I started freelancing some time ago and I have lot of ideas, I wanted to spend maybe 3 days on this hackathon. I spent more than 2 weeks...

2 weeks building the APIs and 2 days for the frontend app.

I started building a quotes game where you have to guess to author or the reference among proposals.

Developing the API

Since I already had the database on Firestore, I deployed a Node.js API on Firebase Cloud Functions.

I used express.js for the backend routing alongside middlewares like cors, or a hand-crafted API key check.

To start a new Cloud Functions project, you can the following:

Create a local repository

mkdir quotes-api
cd quotes-api

Make sure you have node.js installed.

Initialize Firebase (install the npm cli if you don't have it)

firebase init --functions

This will create a functions/ repository inside your project. I used TypeScript as I like types.

When the project is correctly setup, you can the following to your index.ts

// index.ts
import * as functions         from 'firebase-functions';
import { apiModule }          from './api/index';

// API
export const api = functions
  .region('us-central1')
  .https
  .onRequest(apiModule);

And here is what my apiModule looks like

// api/index.ts
import * as express from 'express';
const cors = require('cors');

import { v1Router } from './v1/index';

const main = express();

main.use(cors()); // accept request from external domains, basically
main.use(express.json()); // send JSON
main.use('/v1', v1Router);

export const apiModule = main;

In the code above, the main.use('/v1', v1Router); redirects all request to the https://api.fig.style/v1 to a sub-router.

// api/v1/index.ts
import * as express         from 'express';
import { quotesRouter }     from './quotes';
import { disRouter }        from './dis';

export const v1Router = express.Router()
  .use('/dis', disRouter)
  .use('/quotes', quotesRouter);

The v1 sub-router uses the quotes and dis (Did I Say?) sub-routers (I think you get the idea).

Although I could generate the questions and answers on the Frontend, I wanted to do these operations on the backend for security reason. The users could not edit their score.

This is briefly what I did to generate random questions

// dis.ts
export const disRouter = express.Router()
  .use(checkAPIKey)
  .get('/random', async (req, res, next) => {

    const rand = getRandomIntInclusive(0, 1);
      guessType = rand === 0 
        ? 'author' 
        : 'reference';

    try { 
      randQuoteRes = await getRandomQuoteAuthored({
        lang,
        guessType,
      }); 
    } catch (error) { 
      next(error); 
      return; 
    }

    const selectedQuote = randQuoteRes.quote;

    let answerAuthorSnap: any;

      try {
        answerAuthorSnap = await adminApp.firestore()
          .collection('authors')
          .doc(selectedQuote.author.id)
          .get();
      } catch (error) {
        next(error);
        return;
      }

     let randAuthorsRes: RandomMapArray;

      try { randAuthorsRes = await getRandomAuthors(); } 
      catch (error) { next(error); return; }

      responsePayload.proposals.type = randAuthorsRes.type;
      responsePayload.proposals.values.push(...randAuthorsRes.values);

    // 4. Prepare the response payload.
    responsePayload.question.guessType = randQuoteRes.guessType;
    responsePayload.question.quote.id = selectedQuote.id;
    responsePayload.question.quote.name = selectedQuote.name;
    responsePayload.question.quote.topics = selectedQuote.topics;

    responsePayload.requestState.success = true;

    res.send({ response: responsePayload });
)}

On a GET request, I do:

  • Choose a random number between 0 and 1
  • This, in order to choose the guess type between author and reference
  • Get a random quote which has an author or a reference to guess
  • Get random (wrong) values to put among the right answer
  • Send back the response

Some parts are removed for readability.

To check the user's answer, I wrote:

export const disRouter = express.Router()
.use(checkAPIKey)
.post('/check', async (req, res, next) => {
    const answerProposalId: string = req.body.answerProposalId;
    const guessType: string = req.body.guessType;
    const quoteId: string = req.body.quoteId;

    const checkResult = checkValidateRouteParams(req.body);

    const quoteSnap = await adminApp.firestore()
        .collection('quotes')
        .doc(quoteId)
        .get();

     const quoteSnapData = quoteSnap.data();


    if (guessType === 'author') {
      responsePayload.isCorrect = 
        quoteSnapData.author.id === answerProposalId;

      if (!responsePayload.isCorrect) {
        responsePayload.correction = {
          id: quoteSnapData.author.id,
          name: quoteSnapData.author.name,
        };
      }
    }

    if (guessType === 'reference') {
      responsePayload.isCorrect = 
        quoteSnapData.mainReference.id === answerProposalId;

      if (!responsePayload.isCorrect) {
        responsePayload.correction = {
          id: quoteSnapData.mainReference.id,
          name: quoteSnapData.mainReference.name,
        };
      }
    }    

    responsePayload.requestState.success = true;
    res.send({ response: responsePayload });
});
  • Check the post payload
  • Fetch the quote representing the question
  • Check its author or reference (depending of the guess type)
  • Create a suitable data response according to the user answer
  • Send back the response

You can find the APIs repo on GitHub.

Developing the frontend

Working with Flutter for almost a year from now, I'm really at ease when coding with Dart even for a Web project. Since Flutter Web SDK is still in beta, I would not recommend you to follow my dangerous path.

As I did all the heavy lifting on the Backend, the Frontend is more focused on the UI design and presentation.

The app first shows the quote to guess and then 3 proposals.

Screenshot 2021-02-08 at 01.29.18.png

You choose you answer by clicking on a card and wait for it.

If you scroll, you can see the general rules in case you forget.

Screenshot 2021-02-08 at 01.29.04.png

Vercel deployment

I already had a Vercel account, so I directly created a new app.

Screenshot_2021-02-08 New Project – Vercel.png

Screenshot_2021-02-08 New Project – Vercel(1).png

Flutter is not supported out-of-the-box, and you have to select "Other" configuration and enter the following build script

if cd flutter; then git pull && cd ..; else git clone --single-branch --branch beta https://github.com/flutter/flutter.git; fi && flutter/bin/flutter config --enable-web && flutter/bin/flutter build web --release

Basically, this will check if Flutter is locally available, then install Flutter if not (by cloning the remote repository). Then it will build the project with --enable-web flag to support the Web target.

Configuring CORS

This part will be useful to you if you have an external assets server.

I had the awful surprise to see all my remote images fail to load due to missing CORS policy on my cloud storage. Thanks to this Stackoverflow post and some time later, I finally succeeded to correctly allow cross origin request to my image server.

What's next

As I really rushed the publication of the project, I plan to greatly improve the mini-game with the following features:

  • Save user progression
  • Avoid redundant questions (this may happens right now)
  • Add more quotes & questions
  • Add a timer (limit time to answer)
  • Reconnect to a game after a disconnection
  • A Battle Royal mode with ~10 participants (really not sure about that one)

Thank you for reading! <3

You may find me on [GitHub}(github.com/rootasjey), Twitter, Instagram or my personal website.