---
title: 'Minimal project'
url: 'https://wiki.teletypegames.org/engines/rubbs/minimal-project'
markdown: 'https://wiki.teletypegames.org/engines/rubbs/minimal-project.md'
lang: en
date: '2026-11-02'
description: 'The smallest complete rubbs project: a board that asks two questions and says whether each answer is right. Four files, nothing optional. Written against rubbs 1.1.0. For everything beyond the minimum, see Getting started. Files quiz-bbs/ ├── Gemfile ├── bbs.rb ├── Dockerfile └── compose.yaml Gem…'
taxonomy:
  tag:
    - reference
---

# Minimal project

> The smallest complete [rubbs](https://wiki.teletypegames.org/engines/rubbs) project: a board that asks two questions and says whether each answer is right. Four files, nothing optional. Written against rubbs 1.1.0. For everything beyond the minimum, see [Getting started](https://wiki.teletypegames.org/engines/rubbs/getting-started).

# Files

```text
quiz-bbs/
├── Gemfile
├── bbs.rb
├── Dockerfile
└── compose.yaml
```

## Gemfile

The gem is called `bbs` and comes from our Git server, not from rubygems.org. The `tag` pins a release.

```ruby
source 'https://rubygems.org'

gem 'bbs', git: 'https://git.teletypegames.org/engines/rubbs.git', tag: 'v1.1.0'
```

## bbs.rb

The whole board. `ask` reads a line into the context under the given name, `text` writes the block's return value, and when the steps run out the server hangs up.

```ruby
require 'bbs'

QUESTIONS = {
  'What is 2 + 2?' => '4',
  'Which year did the C64 come out?' => '1982'
}.freeze

BBS.configure do |c|
  c.flow = BBS::Flow.define do
    big_banner 'Quiz BBS'
    QUESTIONS.each_with_index do |(question, answer), i|
      ask :"answer#{i}", prompt: question
      text { |ctx| ctx[:"answer#{i}"] == answer ? 'Correct!' : "Wrong, it is #{answer}." }
    end
    pause 'Bye!', seconds: 2
  end
end

BBS.start
```

## Dockerfile

`git` fetches the gem, `build-base` compiles the `sqlite3` native extension that rubbs depends on.

```dockerfile
FROM ruby:3.3-alpine
RUN apk add --no-cache git build-base
WORKDIR /app
COPY Gemfile ./
RUN bundle install
COPY bbs.rb ./
EXPOSE 2323
CMD ["bundle", "exec", "ruby", "bbs.rb"]
```

## compose.yaml

The board listens on 2323 inside the container; the left side of `ports` is the port on your machine.

```yaml
services:
  quiz-bbs:
    build: .
    ports:
      - "2323:2323"
    restart: unless-stopped
```

# Running it

## With Docker (Windows, Linux, macOS)

Install Docker: on Windows and macOS [Docker Desktop](https://www.docker.com/products/docker-desktop/) (or OrbStack on macOS), on Linux Docker Engine with the compose plugin. Then, in the project folder:

```text
docker compose up -d --build
```

Stop it with `docker compose down`.

## Without Docker (Linux, macOS)

Ruby 3.2 or newer, Git and a C compiler are needed.

| OS | Install |
|---|---|
| macOS | `xcode-select --install`, then `brew install ruby` |
| Debian / Ubuntu | `sudo apt install ruby-full build-essential git` |

```text
bundle install
bundle exec ruby bbs.rb
```

`Ctrl-C` stops the server. On Windows, use Docker, or run the Linux commands inside WSL.

# Calling the board

Any telnet client works; connect to `localhost`, port `2323`.

| OS | Client |
|---|---|
| Windows | Turn on the built-in client once: `dism /online /Enable-Feature /FeatureName:TelnetClient` (admin prompt), then `telnet localhost 2323`. Or PuTTY with connection type Telnet. |
| macOS | `brew install telnet`, then `telnet localhost 2323` |
| Linux | `sudo apt install telnet`, then `telnet localhost 2323` |

```text
     ____        _          ____  ____ _____
    / __ \__  __(_)___     / __ )/ __ ) ___/
   / / / / / / / /_  /    / __  / __  \__ \
  / /_/ / /_/ / / / /_   / /_/ / /_/ /__/ /
  \___\_\__,_/_/ /___/  /_____/_____/____/

  What is 2 + 2?: 4
  Correct!
  Which year did the C64 come out?: 1984
  Wrong, it is 1982.

  Bye!
```

# See also

- [Getting started](https://wiki.teletypegames.org/engines/rubbs/getting-started) — every configuration key, a real board's layout, the production Dockerfile
- [Flow](https://wiki.teletypegames.org/engines/rubbs/flow) — every step of the dialogue DSL
- [rubbs](https://wiki.teletypegames.org/engines/rubbs) — the main page

'; var prevLevel = 2; for (var i = 0; i < headings.length; i++) { var h = headings\[i\]; var text = h.textContent.trim(); var slug = text.toLowerCase() .replace(/\[áàâä\]/g,'a').replace(/\[éèêë\]/g,'e').replace(/\[íìîï\]/g,'i') .replace(/\[óòôöő\]/g,'o').replace(/\[úùûüű\]/g,'u') .replace(/\[^a-z0-9\]+/g, '-').replace(/^-|-$/g, ''); if (!slug) slug = 'heading-' + i; if (used\[slug\]) { slug += '-' + i; } used\[slug\] = true; h.id = slug; var level = parseInt(h.tagName.charAt(1)); var cls = level > 2 ? ' class="toc-child"' : ''; html += '- [' + text + '](#'%20+%20slug%20+%20')
'; } html += '
'; toc.innerHTML = html; // Scroll-spy: highlight current heading var links = toc.querySelectorAll('a'); var ids = \[\]; for (var j = 0; j < links.length; j++) ids.push(links\[j\].getAttribute('href').slice(1)); function updateActive() { var current = ''; for (var k = 0; k < ids.length; k++) { var el = document.getElementById(ids\[k\]); if (el && el.getBoundingClientRect().top <= 80) current = ids\[k\]; } for (var m = 0; m < links.length; m++) { links\[m\].parentNode.classList.toggle('active', links\[m\].getAttribute('href') === '#' + current); } } window.addEventListener('scroll', updateActive, { passive: true }); updateActive(); })();

---

## Navigation

- Parent: [RuBBS](https://wiki.teletypegames.org/engines/rubbs.md)
- Previous: [Getting started](https://wiki.teletypegames.org/engines/rubbs/getting-started.md)
- Next: [Telnet, session and store](https://wiki.teletypegames.org/engines/rubbs/telnet-and-store.md)
