< Kembali

Membangun permainan balap kode dengan AI

ðŸŒą Dibuat .ðŸŠī Diperbarui . ✏ïļ Edit catatan ini.

Artikel berikut menceritakan tentang bagaimana saya membangun permainan balap kode berbasis web yang memanfaatkan AI sebagai salah satu fiturnya. Saya akan menceritakan mulai dari ide hingga peluncuran, dari awal hingga proses deployment, dengan berbagai pertimbangan dan keputusan teknis yang diambil sambil jalan.

Harapannya dengan menyimak artikel ini teman-teman bisa mendapatkan pelajaran dan memahami proses berfikir ketika menjalankan proyek atau membangun produk. Saya pribadi banyak belajar selama membangun permainan ini. Terutama tentang proses deployment, penggunaan AI dan keputusan database apa yang akan digunakan.

Semoga teman-teman bisa mendapatkan pelajaran dan memahami proses berpikir ketika menjalankan proyek atau membangun produk. Saya pribadi banyak belajar selama membangun permainan ini. Terutama tentang proses deployment, penggunaan AI, dan keputusan database apa yang akan digunakan.

Ide Balap Kode

Sebenarnya ide ini sudah lama kepikiran, bahkan jauh sebelum era AI berkembang pesat seperti saat ini. Sedari dulu ingin membuat permainan typing game menyerupai monkeytype atau typeracer tapi spesifik buat developer.

Jika typing game pada umumnya menggunakan bahasa manusia (Inggris atau Indonesia) sebagai soal yang harus diketik, mungkin akan menarik kalau soal yang harus diketik itu berbentuk potongan kode dari bahasa pemrograman yang kita pilih.

Bisa digunakan untuk latihan mengetik, adu cepat dan bisa juga menjadi salah satu cara mencoba bahasa pemrograman baru untuk tahu apakah cocok dengan kita atau kurang cocok.

Saya teringat kembali dengan ide ini ketika menyadari bahwa sekarang AI sudah sangat mumpuni menghasilkan baris kode dari banyak bahasa pemrograman. Walaupun masih sering halu, namun untuk menghasilkan potongan kode acak yang tidak perlu dijalankan di server production, rasanya penggunaan AI untuk ide ini akan cocok sekali.

tangkapan layar

Pemilihan teknologi

Untuk mempercepat proses pengembangan aplikasi, saya memilih web framework dengan fitur lengkap. Memudahkan pula jika ingin meminta bantuan AI nantinya karena semua sudah terintegrasi.

Dan saya memilih Phoenix dan bahasa fungsional Elixir selain karena fiturnya lengkap dan dapat membangun aplikasi web real-time, kedua teknologi ini merupakan favorit saya.

Website Phoenix Framework

Langkah berikutnya adalah memilih database yang ingin digunakan. Saya memilih SQLite sebagai database murni karena kesederhanaannya. Tidak membutuhkan konfigurasi apapun layaknya RDBMS lain, namun sudah cukup mumpuni. Terlebih aplikasi ini penggunaan database tidak terlalu tinggi. Mungkin akan digunakan untuk membuat sesi baru (game dimulai) dan menyimpan data leaderboard saja, rasanya SQLite sudah lebih dari cukup.

Yang saat itu tidak terpikirkan adalah bagaimana caranya deployment SQLite. Berbeda dengan PostgreSQL yang biasanya sudah memiliki servis di platform komputasi awan seperti Cloud SQL, atau RDS. SQLite jarang ada yang menyediakan servis siap pakai. Sepengetahuan saya, baru ada Turso. Lebih lanjut mengenai ini akan dibahas dibagian berikutnya.

Deployment dan berbagai tantangannya

Continuous Integration

Mengikuti petuah Mas Ariya Hidayat agar selalu menggunakan Continuous Integration atau CI dalam proyek, sekaligus ajang belajar buat saya. Semua ini dipermudah dengan adanya GitHub workflow, konfigurasinya pun tidaklah sulit.

.github/workflows/elixir.yml

name: Elixir CI
on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
permissions:
  contents: read
jobs:
  build:
    name: Build and test
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v4
    - name: Set up Elixir
      uses: erlef/setup-beam@61e01a43a562a89bfc54c7f9a378ff67b03e4a21 # v1.16.0
      with:
        elixir-version: '1.18.4' # [Required] Define the Elixir version
        otp-version: '27.3.4'      # [Required] Define the Erlang/OTP version
    - name: Restore dependencies cache
      uses: actions/cache@v3
      with:
        path: deps
        key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
        restore-keys: ${{ runner.os }}-mix-
    - name: Install dependencies
      run: mix deps.get
    - name: Run tests
      env:
        GCP_SERVICE_ACCOUNT_JSON: ${{ secrets.GCP_SERVICE_ACCOUNT_JSON }}
      run: mix test

Saya konfigurasi ketika terjadi event push atau pull request, secara otomatis CI akan dijalankan dan akan memberi masukan jika ada pengujian yang tidak lulus, atau kesulitan instalasi dependencies. CI memastikan semua proses integrasi berhasil dan siap di deploy ke produksi.

Tangkapan layar ketika CI otomatis dijalankan

Menggunakan kontainer untuk deployment

Cara paling mudah dari sisi developer experience sebenarnya adalah menggunakan layanan seperti Cloud Run, yang juga pernah saya pakai di proyek #AISprint terdahulu.

Namun untuk proyek kali ini saya ingin menggunakan pendekatan berbeda. Jika servis seperti Cloud Run sangat fleksibel dengan fitur autoscaling yang menyebabkan tagihan bergantung kepada jumlah pengguna, kali ini saya ingin menggunakan servis yang tagihannya tetap meskipun belum bisa autoscaling.

Salah satu alasannya karena saya akan menggunakan API untuk servis LLM yang tagihannya bergantung kepada pengguna. Kalau saya menggunakan Cloud Run, maka saya harus deg-degan dua kali lipat. Sehingga saya memutuskan untuk menggunakan tagihan tetap untuk hosting aplikasi dan tagihan fleksibel untuk AI agar tingkat deg-degan nya 1 kali saja 😉

Untuk mempersiapkan aplikasi Elixir Phoenix supaya siap di-deploy bisa dengan menggunakan perintah berikut untuk sekalian dibuatkan Dockerfile.

mix phx.gen.release --docker

Dengan menjalankan perintah diatas, hasil Dockerfile kurang lebih seperti cuplikan kode dibawah.

Dockerfile

ARG ELIXIR_VERSION=1.18.4
ARG OTP_VERSION=27.3.4
ARG DEBIAN_VERSION=bookworm-20250520-slim

ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}"

FROM ${BUILDER_IMAGE} AS builder

# install build dependencies
RUN apt-get update \
  && apt-get install -y --no-install-recommends build-essential git \
  && rm -rf /var/lib/apt/lists/*

# prepare build dir
WORKDIR /app

# install hex + rebar
RUN mix local.hex --force \
  && mix local.rebar --force

# set build ENV
ENV MIX_ENV="prod"

# install mix dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV
RUN mkdir config

# copy compile-time config files before we compile dependencies
# to ensure any relevant config change will trigger the dependencies
# to be re-compiled.
COPY config/config.exs config/${MIX_ENV}.exs config/
RUN mix deps.compile

RUN mix assets.setup

COPY priv priv

COPY lib lib

COPY assets assets

# compile assets
RUN mix assets.deploy

# Compile the release
RUN mix compile

# Changes to config/runtime.exs don't require recompiling the code
COPY config/runtime.exs config/

COPY rel rel
RUN mkdir -p /app/priv/data && chown nobody /app/priv/data
RUN mix release

# start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
FROM ${RUNNER_IMAGE} AS final

RUN apt-get update \
  && apt-get install -y --no-install-recommends libstdc++6 openssl libncurses5 locales ca-certificates \
  && rm -rf /var/lib/apt/lists/*

# Set the locale
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \
  && locale-gen

ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8

WORKDIR "/app"
RUN chown nobody /app

# set runner ENV
ENV MIX_ENV="prod"

# Only copy the final release from the build stage
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/coderacer ./

COPY entrypoint.sh /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]

CMD ["/app/bin/start"]

Dockerfile sudah jadi, saatnya memilih servis apa yang ingin digunakan untuk deployment!

Memilih Servis untuk Deployment Kontainer

Ada beberapa pilihan deployment kontainer di GCP. Pilihan pertama adalah Cloud Run yang sudah kita bahas sebelumnya. Opsi kedua bisa menggunakan Kubernetes Engine atau GKE. Opsi ketiga ada App Engine Flexible Environment. Opsi terakhir menggunakan virtual machine (VM) dengan Compute Engine.

Demi kesederhanaan dan biaya yang mudah diprediksi, saya memutuskan menggunakan Compute Engine. Berbeda dengan servis lain, agar dapat melakukan deployment kontainer, VM di Compute Engine harus disiapkan terlebih dahulu untuk beberapa hal:

Gambaran sederhana arsitektur aplikasai

Saya awalnya memilih mesin dengan tipe e2-micro, lalu setelah beberapa hari saya update ke mesin e2-small yang lebih mumpuni. Dan agar harganya menjadi lebih ekonomis lagi, saya memutuskan menggunakan tipe Spot VM sebagai provisioning model.

Spot VM adalah tipe mesin virtual yang dapat kapan saja dihentikan oleh GCP agar sumberdaya-nya dapat digunakan servis lainnya. Harganya jadi 60-90% lebih murah namun resikonya aplikasi bisa saja tidak dapat diakses karena sedang dalam keadaan mati. Tidak disarankan digunakan untuk jenis aplikasi penting, namun untuk aplikasi ini rasanya tidak akan menjadi masalah.

Konfigurasi SpotVM

Saya juga menyiapkan servis agar setiap kali server restart akan otomatis menjalankan docker compose up supaya aplikasi otomatis menyala kembali. Servis ini juga dapat dijalankan dan dihentikan manual dengan perintah sudo systemctl start coderacer dan sudo systemctl start coderacer.

File /etc/systemd/coderacer.service

Deployment untuk Database

Sebenarnya jika dari awal memilih database PostgreSQL, pembelajaran yang saya dapatkan tidak sebanyak ini 😉 Untuk database RDBMS seperti PostgreSQL atau MySQL cenderung lebih mudah karena bisa menggunakan servis seperti Cloud SQL, atau cara manual dengan instalasi RDBMS di mesin virtual.

Karena saya menggunakan SQLite, saya jadi belajar tentang bagaimana mengintegrasikannya dengan kontainer. Hal ini memberi pengalaman baru yang tidak saya dapatkan jika sejak awal menggunakan PostgreSQL. Karena kontainer sifatnya stateless, database tidak bisa dimasukkan begitu saja ke kontainer.

Akhirnya, saya belajar tentang Docker volumes yang bisa digunakan untuk menyimpan data. Bisa ditambahkan ke docker-compose.yml untuk volume dan jangan lupa arahkan konfigurasi ke volume tersebut.

docker-compose.yml
services:
  coderacer_deploy:
    image: coderacer_deploy:latest
    container_name: coderacer_deploy
    ports:
      - "4000:4000"
    environment:
      GCP_SERVICE_ACCOUNT_JSON: /app/config/gcp_credentials.json
    env_file:
      - .env
    volumes:
      - coderacer_sqlite:/app/priv/data
      - ~/gcp_credentials.json:/app/config/gcp_credentials.json:ro
    stdin_open: true # equivalent to -it
    tty: true # equivalent to -it
    restart: "no" # equivalent to --rm (no restart)

volumes:
  coderacer_sqlite:

Bonus: Domain

Biar semakin keren, ngide cari domain. Berhubung coderacer.com dan variannya sudah tidak tersedia (atau super mahal), coba diterjemahkan menjadi balapkode. Dan ternyata balapkode.com masih tersedia! Langsung beli dan disambungkan ke server.

Servis AI sering kena Rate-limit

Setelah semuanya beres, woro-woro ke media sosial supaya teman-teman bisa mencoba. Tidak lama setelah itu dari laporan beberapa teman di media sosial, ketika ingin mencoba malah kena rate-limit. Wajar saja karena saya masih menggunakan servis AI dari AIStudio yang memang freemium.

Solusinya bisa menggunakan servis berbayar, salah satunya Vertex AI yang disediakan oleh GCP. Berhubung ini adalah pengalaman pertama menggunakan Vertex, dan sedikit keliru menterjemahkan maksud contoh kode yang ada di dokumentasi menyebabkan pengguna tetap kena rate-limit ketika menggunakan aplikasi.

Vertex API Key

Yang saya lakukan adalah menjalankan perintah gcloud auth print-access-token di terminal, kemudian copas hasilnya ke environment variable. Padahal, perintah ini seharusnya dijalankan setiap kali ada request masuk.

Setelah membaca ulang dokumentasi, saya baru menyadari kesalahan yang saya lakukan. Berbeda dengan AIStudio yang cukup menggunakan API Key yang sifatnya statis tidak berubah, Vertex AI menggunakan API Key yang berubah-ubah dan harus di-refresh secara berkala.

Alhasil, kodenya harus diubah kembali. Untungnya untuk berhubungan dengan operasi GCP seperti ini, sudah ada pustaka atau library Goth yang dibuat oleh komunitas.

lib/coderacer/ai.ex


+ token = Goth.fetch!(Coderacer.Goth)

http_client.post!(
  url,Add commentMore actions
headers: [
      {"Content-Type", "application/json"},
-     {"Authorization", "Bearer #{System.get_env("VERTEX_API_KEY")}"}
+     {"Authorization", "Bearer #{token.token}"}
...

Sederhananya, setiap kali ada request, kita meminta token baru yang kemudian disertakan kedalam http header authorization. Untuk kode lengkapnya bisa diintip langsung disini.

Penutup

Dari sebuah ide lama yang sempat tertunda, akhirnya saya berhasil membangun aplikasi/permainan BalapKode ini — permainan balap mengetik kode dengan sentuhan AI. Sepanjang perjalanan membangun aplikasi ini, saya belajar banyak hal: dari memilih teknologi yang tepat, menyelesaikan berbagai tantangan deployment, memahami keterbatasan dan kekuatan SQLite, hingga mendalami cara kerja layanan AI seperti Vertex AI.

Saya juga belajar bahwa membangun produk bukan cuma soal menulis kode, tapi juga tentang mengambil keputusan teknis, mengelola risiko biaya, dan yang paling penting: terus belajar dari kesalahan.

Menggunakan pendekatan pragmatis dan fokus pada "good enough to launch", proyek ini akhirnya bisa dinikmati publik lewat balapkode.com.

Kalau kamu seorang developer (atau sedang belajar permrograman) dan ingin melatih kecepatan mengetik sambil mengenal berbagai bahasa pemrograman, yuk langsung coba mainkan di:

👉 balapkode.com

Kalau kamu menemukan bug, punya ide fitur baru, atau sekadar ingin lihat isi kode sumbernya, semua tersedia secara terbuka di:

🛠ïļ GitHub: rizafahmi/coderacer

Artikel diatas membahas proses pembuatan aplikasi dari awal hingga berhasil diluncurkan. Namun tidak membahas kodenya secara mendetil. Jika teman-teman tertarik untuk membahas kodenya, boleh ya ditulis di kolom komentar dibawah.

Terima kasih sudah membaca sampai akhir. Semoga perjalanan ini bisa menginspirasi kamu untuk mulai (atau menyelesaikan) ide-ide yang selama ini hanya ada di kepala.