---
title: Flow
url: 'https://wiki.teletypegames.org/hu/engines/rubbs/flow'
markdown: 'https://wiki.teletypegames.org/hu/engines/rubbs/flow.md'
lang: hu
date: '2026-10-27'
description: 'A rubbs Flow rétege: sororientált, deklaratív párbeszéd, amivel minden board kezdődik. Ez az oldal a BBS::Flow minden lépését felsorolja szignatúrával, alapértékekkel és példával, elmagyarázza, hogyan osztoznak a lépések az adatokon (a context), mikor áll meg egy flow, hogyan működnek az ERB-scree…'
taxonomy:
  tag:
    - reference
---

# Flow

> A [rubbs](https://wiki.teletypegames.org/hu/engines/rubbs) Flow rétege: sororientált, deklaratív párbeszéd, amivel minden board kezdődik. Ez az oldal a `BBS::Flow` minden lépését felsorolja szignatúrával, alapértékekkel és példával, elmagyarázza, hogyan osztoznak a lépések az adatokon (a context), mikor áll meg egy flow, hogyan működnek az ERB-screenek és a bannerek, a végén pedig egy teljes login-és-menü board és a valódi Impostor BBS jön. A rubbs 1.0.0 alapján.

# Áttekintés

```ruby
require 'bbs'

DEMO = BBS::Flow.define do
  banner 'WELCOME'
  ask :name, prompt: 'Your name'
  text { |ctx| "Hello, #{ctx[:name]}!" }
end

BBS.configure { |c| c.flow = DEMO }
BBS.start
```

A `BBS::Flow.define(screens_dir: nil) { … }` egy új `Flow`-n értékeli ki a blokkot, és befagyasztva adja vissza. A blokk minden DSL-hívása egy **lépést** fűz a `flow.steps` végére: egy kis `BBS::Flow::Steps`-objektumot, aminek `run(runner)` metódusa van; ekkor még senkinek nem megy ki semmi. A flow sablon: betöltéskor egyszer épül fel, és minden hívó ugyanazt használja.

Amikor valaki betárcsáz, a `BBS::FlowRunner` fentről lefelé lefuttatja a lépéseket az ő vonalán. A kiíró lépések ANSI-szöveget írnak, a kérdező lépések egy sort olvasnak a hívótól. A szöveg görög, mint egy teletype-on; semmi nem rajzolódik újra helyben. Teljes képernyőhöz ott az [Application](https://wiki.teletypegames.org/hu/engines/rubbs/application) (ablakok, menük, egér), soralapú boardhoz pedig a 40×25-ös `BBS::Petscii::Screen` a `BBS::Selector`-menüivel (lásd [Commodore 64](https://wiki.teletypegames.org/hu/engines/rubbs/commodore)).

A legtöbb board csak ragasztónak használja a flow-t: a terminál-kérdés, esetleg egy login, aztán egy `call` lépés, ami a boardot futtatja, és még egy az elköszönéshez. Az alábbi lépéseken kívül nincs más; minden egyéb Ruby-kód egy `call`-ban.

Az oldal minden példája egy `DEMO`-hoz hasonló konstans; kipróbáláshoz tedd a `c.flow`-ba, és indítsd el a szervert, ahogy az [Első lépések](https://wiki.teletypegames.org/hu/engines/rubbs/getting-started) mutatja.

## A context

Egy flow minden futásának saját **contextje** van: egy Symbol kulcsú Hash, a blokkokban `ctx` a neve. Az `ask` és a saját `call` blokkjaid írják, a blokkok olvassák, a `persist` mezőket ment belőle, a `screen` átadja az ERB-template-nek.

| Kulcs | Ki állítja | Érték |
|---|---|---|
| `:session_id` | a runner, mindig | a hívó véletlen, 16 hexa számjegyű session-azonosítója |
| bármilyen név | `ask NAME`, a saját `call` blokkjaid | amit beleteszel |

Az al-flow-k (menüpontok, `confirm` blokkok) ugyanazt a contextet használják. A `BBS::FlowRunner.new(session, session_id, flow, context: {...})`-val indított flow ezekkel a kulcsokkal kezd (így adja át egy door a hívó nevét); a `session_id`-t mindig felülírja.

## Mikor áll meg egy flow

A runner sorban futtatja a lépéseket, amíg az alábbiak egyike be nem következik:

- **Elfogynak a lépések.** A vezérlés visszakerül oda, ahonnan a flow-t indították; a `config.flow` esetén ez azt jelenti, hogy a szerver bontja a vonalat.
- **Egy lépés `:halt`-ot ad.** Az egész flow megáll, minden befoglaló menüvel és al-flow-val együtt. `:halt`-ot ad: ha a hívó bármelyik olvasás közben bont, ha egy `ask` validálása elbukik, ha egy `gate` elbukik, ha egy blokk nélküli `confirm`-re "no" a válasz, és ha egy `call` blokk `:halt`-ot ad vissza.
- **Az `exit_menu`** csak a legbelső `menu`-ből lép ki, és a flow utána folytatódik.

Hibák: egy `call` blokk hibája a `BBS.log`-on át a logba kerül (`flow call: …`), a hívó annyit lát, hogy "Something went wrong.", és a flow megy tovább; ha közben megszakad a vonal, a flow megáll. Bármely más lépés hibája (például egy `text` vagy `gate` blokkban, egy ismeretlen banner-font, egy hiányzó ERB-fájl) a session végét jelenti.

# Lépések

| Lépés | Szignatúra | Mit csinál |
|---|---|---|
| [`say`](#say) | `say(text, style: :muted)` | egy sort ír ki üres sorok között |
| [`text`](#text) | `text(content = nil, style: :muted) { … }` | egy sort ír ki, fix vagy számolt szöveggel |
| [`banner`](#banner) | `banner(text, style: :success)` | dupla vonalas keretes cím |
| [`big_banner`](#big-banner) | `big_banner(text, style: :success, font: 'slant')` | FIGlet-cím |
| [`screen`](#screen) | `screen(name, **vars)` | ERB-fájlt renderel |
| [`ask`](#ask) | `ask(name, prompt:, transform: nil, validate: nil)` | egy sort olvas a contextbe |
| [`pause`](#pause) | `pause(message, seconds: 1.0)` | kiír, és vár |
| [`confirm`](#confirm) | `confirm(message, denied: nil)` vagy blokkal | igen/nem kérdés |
| [`gate`](#gate) | `gate(denied:) { … }` | megállítja a flow-t, ha a blokk hamis |
| [`menu`](#menu) | `menu(prompt, loop: false) { option … }` | al-flow-k számozott `BBS::Selector`-a |
| [`exit_menu`](#exit-menu) | `exit_menu` | kilép a legbelső menüből |
| [`persist`](#persist) | `persist(*fields, store:)` | context-mezőket ment egy `BBS::Store`-ba |
| [`call`](#call) | `call { … }` | tetszőleges Ruby-kód a vonallal |
| [`app`](#app) | `app(definition)` | egy `BBS::Application`-t futtat |

A `set`, `line`, `section`, `rows`, `table`, `wait_enter`, `fetch`, `pick`, `body`, `output`, `os` és `tui` lépés az 1.0 óta nincs meg: mindegyik pár sor egy `call`-ban (a runner `line`, `ask` és `select` metódusa a legtöbbet lefedi), egy `BBS::FakeOS` gép pedig `BBS::Door`-ként fut (lásd [Doorok](https://wiki.teletypegames.org/hu/engines/rubbs/doors)).

## Stílusok

A `style:` a `BBS::FlowRunner::STYLES` egy nevét kapja; a színek az egyetlen palettából (`BBS::Ansi`) jönnek:

| Név | Szín |
|---|---|
| `:success` | élénkzöld |
| `:muted` | szürke (sima szöveg) |
| `:error` | élénkpiros |
| `:info` | sárga |
| `:prompt` | fehér |

Minden más érték (ismeretlen Symbol, nyers ANSI-string) `:muted`-del íródik ki. A 0.x színnevei (`:confirm`, `:cyan`, `:magenta`, `:blue`, `:yellow`, `:white`, `:green`, `:red`) megszűntek.

# Kiíró lépések

## say

A `say(text, style: :muted)` két szóközzel behúzva írja ki a `text`-et, előtte és utána egy üres sorral.

```ruby
DEMO = BBS::Flow.define do
  say 'Welcome to the board.'
  say 'Login successful.', style: :success
  say 'The file area is closed tonight.', style: :error
end
```

## text

A `text(content = nil, style: :muted)` egy behúzott sort ír ki, üres sorok nélkül. Blokkal a blokk megkapja a contextet, és a visszatérési értéke kerül ki; `nil` eredménynél nem ír semmit.

```ruby
DEMO = BBS::Flow.define do
  ask :name, prompt: 'Name'
  text 'Your account:', style: :prompt
  text { |ctx| "  name:    #{ctx[:name]}" }
  text { |ctx| "  session: #{ctx[:session_id]}" }
  text(style: :info) { |ctx| 'sysop!' if ctx[:name] == 'sysop' }
end
```

A blokkban dobott kivételt semmi nem kapja el, és a session véget ér; a kockázatos értékeket előbb egy `call`-ban számold ki.

## banner

A `banner(text, style: :success)` dupla vonalas keretbe (`╔═╗ ║ ╚═╝`) írja a `text`-et, két szóköz térközzel.

```ruby
DEMO = BBS::Flow.define do
  banner 'IDENTITY VERIFICATION', style: :info
end
```

```text
  ╔═════════════════════════╗
  ║  IDENTITY VERIFICATION  ║
  ╚═════════════════════════╝
```

## big\_banner

A `big_banner(text, style: :success, font: 'slant')` FIGlettel rajzolja ki a `text`-et az `artii` gemen át. Bármelyik artii-font jó (`slant`, `standard`, `small`, `big`, `doom`, `banner`, …; a `bundle exec artii --list` mindet kiírja). Ismeretlen fontnál kivétel jön, és a session véget ér, ezért az új fontot előbb helyben próbáld ki.

```ruby
DEMO = BBS::Flow.define do
  big_banner 'HARBOR'
  big_banner 'BBS', font: 'doom', style: :info
end
```

## screen

A `screen(name, **vars)` kirendereli és kiírja a `NAME.erb` fájlt. Lásd lent a Screenek (ERB) részt.

```ruby
DEMO = BBS::Flow.define(screens_dir: File.join(__dir__, 'screens')) do
  screen :welcome, year: Time.now.year, node: :session_id
end
```

# Bekérő lépések

## ask

Az `ask(name, prompt:, transform: nil, validate: nil)` kiírja a `prompt:`-ot `:prompt` stílussal, beolvas egy sort, levágja a szélein lévő whitespace-t, és az eredményt a `ctx[name]`-be teszi. Modern terminálon a karaktereket a szerver visszhangozza; PETSCII-vonalon a válasz egy `BBS::Field`-be kerül, ami a kurzorbillentyűkkel szerkeszthető.

| Opció | Értékek | Hatás |
|---|---|---|
| `prompt:` | String | a kettőspont előtti szöveg; kötelező |
| `transform:` | `:upcase`, `:downcase`, egy Proc | a levágott válaszra fut a validálás előtt; a Proc a Stringet kapja, és az új értéket adja vissza |
| `validate:` | `:non_empty`, `:email`, egy Proc vagy Method | a transform után ellenőrzi; a Proc vagy Method a (transzformált) értéket kapja, truthy válasz esetén érvényes |

Az elbukott validálás kiír egy üzenetet, és **az egész flow-t megállítja**: az `:email` azt, hogy "That doesn't look like a valid email address. Farewell.", a többi azt, hogy "Invalid input. Farewell.". Nem kérdez újra. A `validate:`-nek adott bármi mást soha nem ellenőriz (minden átmegy). Újrakérdezéshez vagy saját üzenethez `call` kell (lásd [Újrakérdezés és jelszó](#ujrakerdezes-es-jelszo)).

```ruby
DEMO = BBS::Flow.define do
  ask :handle, prompt: 'Handle', transform: :downcase, validate: :non_empty
  ask :email,  prompt: 'Email',  validate: :email
  ask :age,    prompt: 'Age',    transform: ->(answer) { answer.to_i }
  text { |ctx| "#{ctx[:handle]}, #{ctx[:age]}, #{ctx[:email]}" }
end
```

Saját szabály lambdával; az `abc`-ből a transform 0-t csinál, így elbukik:

```ruby
ADULTS = BBS::Flow.define do
  ask :age, prompt: 'Age', transform: ->(answer) { answer.to_i }, validate: ->(age) { age >= 18 }
  say 'Welcome in.'
end
```

Az `ask`-nek nincs `mask:` opciója; a jelszóprompt egy `call` lépés `runner.readline(mask: '*')`-gal.

## pause

A `pause(message, seconds: 1.0)` `:info` stílussal kiírja a `message`-et, és alszik. A hívó nem tudja átugrani; legyen rövid.

```ruby
DEMO = BBS::Flow.define do
  pause 'Verifying identity...', seconds: 1.2
  say 'Verified.', style: :success
end
```

## confirm

Igen/nem kérdés `[yes/no]` prompttal; a `yes` vagy `y` (kis- és nagybetűtől függetlenül) igen, minden más nem.

**Blokk nélkül** a `confirm(message, denied: nil)` ellenőrzőpont: igennél a flow megy tovább, nemnél kiírja a `denied`-et (ha van), és **megáll**.

**Blokkal** a `confirm(message, denied: nil) { … }` igennél al-flow-ként futtatja a blokkot; nemnél kiírja a `denied`-et (ha van), és a flow a blokk után **folytatódik**.

```ruby
DEMO = BBS::Flow.define do
  confirm 'Are you over 18?', denied: 'Come back later.'
  say 'Welcome to the adult area.'

  confirm 'Subscribe to the newsletter?', denied: 'Maybe next time.' do
    ask :email, prompt: 'Email', validate: :email
    say 'Subscribed.', style: :success
  end

  say 'Main menu next.'
end
```

# Vezérlő lépések

## gate

A `gate(denied:) { |ctx| … }` meghívja a blokkot a contexttel; ha az eredmény hamis, `:error` stílussal kiírja a `denied`-et, és megállítja a flow-t.

```ruby
BANNED = %w[spammer troll].freeze

DEMO = BBS::Flow.define do
  ask :handle, prompt: 'Handle', transform: :downcase
  gate(denied: 'This handle is banned.') { |ctx| !BANNED.include?(ctx[:handle]) }
  say 'Come in.'
end
```

## menu

A `menu(prompt, loop: false) { option … }` a menüpontok számozott `BBS::Selector`-a, 38 oszlop széles, hogy C64-en is elférjen, a `prompt` az állapotsorában áll. A kurzorbillentyűk és a számjegyek kijelölnek egy menüpontot, a RETURN kiválasztja, az Esc (C64-en a RUN/STOP vagy a ←) választás nélkül kilép a menüből. Minden `option(label) { … }` egy al-flow, ami a közös contexttel fut, ha kiválasztják. A blokk nélküli menüpont nem csinál semmit.

| `loop:` | A menüpont lépései után |
|---|---|
| `false` | a menü kész; a flow utána folytatódik |
| `true` | a menü újra megjelenik, amíg egy menüpont `exit_menu`-t nem futtat, vagy a hívó Esc-et nem nyom |

```ruby
DEMO = BBS::Flow.define do
  menu 'Main menu', loop: true do
    option 'Bulletins' do
      say 'Nothing new.'
    end
    option 'Settings' do
      menu 'Settings' do
        option('Colours on')  { say 'Colours on.' }
        option('Colours off') { say 'Colours off.' }
      end
    end
    option 'Log off' do
      exit_menu
    end
  end
  say 'Goodbye!'
end
```

Egy menüponton belüli `:halt` (elbukott gate, `:halt`-ot adó `call`, bontás) az egész flow-t megállítja, nem csak a menüt.

## exit\_menu

Az `exit_menu` kilép a legbelső befoglaló `menu`-ből; a flow utána folytatódik. Beágyazott al-flow-ból is működik (egy menüponton belüli `confirm` blokkból). Menün kívül a flow végét jelenti. Egy `call` blokk nem adhatja vissza (a `call`-on csak a `:halt` jut át).

```ruby
DEMO = BBS::Flow.define do
  menu 'Pick', loop: true do
    option('Again') { say 'Once more.' }
    option('Done') do
      confirm 'Really leave the menu?' do
        exit_menu
      end
    end
  end
  say 'Out of the menu.'
end
```

A különbség: a (blokkos) `confirm`-re adott "no" egyszerűen továbbmegy, és a menü újra megjelenik.

## persist

A `persist(*fields, store:)` a megnevezett context-mezőket egy `BBS::Store`-ba menti, a session-azonosító szerint: egy hívás első `persist`-je új sort ad hozzá, a későbbiek frissítik. A contextből hiányzó mezőket kihagyja; ha egy sincs meg, nem ír semmit. Csak olyan mezőt sorolj fel, ami a store oszlopa (`headers:`). A `timestamp` oszlopot, ha a store-nak van ilyen, a sor létrehozásának idejével tölti ki.

A 0.33 óta a store egy SQLite-fájl táblája: a `.csv` útvonal a mellette lévő `.sqlite3` fájlt jelenti (`data/guestbook.sqlite3`), egy korábbi verzió CSV-fájlját pedig egyszer, üres táblánál beimportálja. Magáról a store-ról az [Adat és hálózat](https://wiki.teletypegames.org/hu/engines/rubbs/data-and-network) oldal szól.

```ruby
GUESTBOOK = BBS::Store.new(path: File.join(__dir__, 'data/guestbook.csv'),
                           headers: %w[session_id timestamp name city message])

DEMO = BBS::Flow.define do
  ask :name, prompt: 'Name'
  ask :city, prompt: 'City'
  persist :name, :city, store: GUESTBOOK
  ask :message, prompt: 'Leave a message'
  persist :message, store: GUESTBOOK
  say 'Thanks for signing the guestbook.'
end

GUESTBOOK.all
# => [{ "session_id" => "3f9c2a1be07d4c11", "timestamp" => "2026-10-27T20:15:02Z",
#       "name" => "Zsolt", "city" => "Budapest", "message" => "Hello from the Harbor" }]
```

# Kódlépések

## call

A `call { |ctx, runner| … }` tetszőleges Ruby-kódot futtat. A blokk megkapja a contextet (szabadon olvashatod és írhatod) és a `FlowRunner`-t, amin át a vonalat éred el:

| `runner.` |  |
|---|---|
| `write(text)` | a hívónak ír (`\r\n` sorvégekkel) |
| `line(text, style: :muted)` | egy sort ír két szóközzel behúzva, a stílus színével |
| `ask(prompt)` | kiírja a `prompt`-ot, és beolvassa a levágott választ (C64-en `BBS::Field`-ben); `nil`, ha a hívó bontott |
| `readline(mask: nil)` | egy sort olvas; `mask: '*'` mellett `*`-ot visszhangoz a karakterek helyett; `nil`, ha a hívó bontott |
| `select(options, hint: nil)` | `[címke, érték]` párok számozott `BBS::Selector`-a; a választott értéket adja, Esc-re `nil`-t |
| `context` | a context Hash (a blokk `ctx`-e) |
| `session` | a `BBS::Session`: `readkey`, `term_cols`, `term_rows`, `petscii?`, `enable_mouse`, … (lásd [Telnet és Store](https://wiki.teletypegames.org/hu/engines/rubbs/telnet-and-store)) |
| `session_id` | a session-azonosító |

`:halt`-tal megállítod a flow-t, bármi más visszatérési értékkel megy tovább. A blokkból korán kilépni `next`-tel lehet (nem `return`-nel). A kivétel a logba kerül, a hívó annyit lát, hogy "Something went wrong.", és a flow folytatódik; a megszakadt kapcsolat megállítja.

```ruby
DEMO = BBS::Flow.define do
  call do |ctx, runner|
    runner.line "Your terminal is #{runner.session.term_cols}x#{runner.session.term_rows}."
    ctx[:wide] = runner.session.term_cols >= 100
    nil
  end
  text { |ctx| ctx[:wide] ? 'Wide screen layout.' : 'Narrow screen layout.' }
end
```

Amit a megszűnt lépések csináltak, az itt pár sor. Egy lista, amiből választani lehet, egy RETURN-re várás, egy érték, amit egy kiírt sor alatt töltünk be:

```ruby
Post = Struct.new(:title, :body)
POSTS = [Post.new('Welcome', 'Hello all.'), Post.new('Rules', 'Be nice.')].freeze

DEMO = BBS::Flow.define do
  call do |_ctx, runner|
    post = runner.select(POSTS.map { |p| [p.title, p] }, hint: 'Read which post?')
    next nil unless post

    runner.line post.title, style: :prompt
    runner.line post.body
    runner.ask('Press RETURN')
    nil
  end
end
```

A `call`-lal futtat egy flow olyan dolgokat is, amik nem lépések: egy `Application`-t, ha kód is kell köré (`APP.run(...)`, lásd [app](#app)), egy másik flow-t (`BBS::FlowRunner.new(runner.session, ctx[:session_id], OTHER, context: ctx).run`), egy doort (`BBS::Door.new(:machine, MACHINE).run(runner.session, ctx)`, FakeOS-gépet is), egy ZMODEM-letöltést, a board saját `BBS::Petscii::Screen`-jét.

### Újrakérdezés és jelszó

Az `ask` rossz válaszra megáll; egy `call`-ciklus addig kérdez, amíg jó választ nem kap, és el is takarhatja a bevitelt:

```ruby
PINS = { 'sysop' => '1234' }.freeze

DEMO = BBS::Flow.define do
  call do |ctx, runner|
    loop do
      answer = runner.ask('  Handle (3-12 letters): ')
      break :halt if answer.nil?

      answer = answer.downcase
      if answer.match?(/\A[a-z]{3,12}\z/)
        ctx[:handle] = answer
        break nil
      end
      runner.line 'Letters only, 3 to 12 of them.', style: :error
    end
  end

  call do |ctx, runner|
    next nil unless PINS.key?(ctx[:handle])

    result = :halt
    3.times do
      runner.write '  PIN: '
      pin = runner.readline(mask: '*')
      break if pin.nil?

      if pin.strip == PINS[ctx[:handle]]
        ctx[:sysop] = true
        result = nil
        break
      end
      runner.line 'Wrong PIN.', style: :error
    end
    result
  end

  text { |ctx| "Welcome, #{ctx[:handle]}#{' (sysop)' if ctx[:sysop]}." }
end
```

A `loop`, `times` vagy `each` blokkjain belül a `next` csak a következő körre ugrik; ezekből `break ÉRTÉK`-kel lépj ki, és ez az érték (vagy a blokk utolsó kifejezése) legyen a `call` eredménye. A bontásnak (az `ask` vagy a `readline` `nil`-t ad) ki kell léptetnie a ciklusból, különben az örökké pörög.

## app

Az `app(definition)` egy `BBS::Application`-nek adja át a vonalat (lásd [Application](https://wiki.teletypegames.org/hu/engines/rubbs/application)): `definition.run(session:, session_id:, context: ctx)`, amíg ki nem lépnek belőle, a doorjain át is (a választott door lefut, utána az Application újraépül). Az Application `context`-je ugyanez a Hash, így a változtatásokat a későbbi lépések is látják; ha megszakad a vonal, a flow megáll.

```ruby
DESKTOP = BBS::Application.define do
  status_hint('F10', 'Back') { :halt }
end

DEMO = BBS::Flow.define do
  ask :handle, prompt: 'Handle'
  app DESKTOP
  say 'Back in the flow.'
end
```

Ugyanez egy `call` lépésből, ahogy a hálózatunk minden boardja csinálja, helyet hagy a futtatás köré írt kódnak:

```ruby
DEMO = BBS::Flow.define do
  ask :handle, prompt: 'Handle'
  call do |ctx, runner|
    DESKTOP.run(session: runner.session, session_id: ctx[:session_id], context: ctx)
    nil
  end
  say 'Thanks for calling.'
end
```

# Screenek (ERB)

A `screen` lépés kirenderel és kiír egy ERB-template-et. A kézzel rajzolt ANSI-címlapok helye a screen.

**Hol van a fájl.** A `screen :welcome` a `welcome.erb`-t a flow saját mappájából olvassa (`BBS::Flow.define(screens_dir: DIR)`), vagy ha a flow-nak nincs ilyen, a `BBS.config.screens_dir`-ből. Ha egyik sincs beállítva, a lépés kivételt dob (`No screens_dir: …`), és a session véget ér. Egy gemben szállított flow a saját mappáját adja meg, így bármelyik boardban ki tudja rajzolni a screenjeit.

**Változók.** A template-ben a context minden kulcsa példányváltozó (`@session_id`, `@name`, …), és a lépés `vars`-a is. Ha egy `vars`-érték Symbol, a contextből veszi az értékét (a `node: :session_id` a `@node`-ba a session-azonosítót teszi); minden más változatlanul megy át.

A template-ben elérhető **helperek**:

| Helper | Mit ad |
|---|---|
| `clr` | képernyőtörlés és kurzor a bal felső sarokba (`\e[2J\e[H`) |
| `reset` | `\e[0m` |
| `green`, `yellow`, `cyan`, `white`, `red` | a paletta (`BBS::Ansi`) élénk színei |
| `gray`, `dim_green` | sima szürke és normál zöld |
| `banner(text, padding: 2)` | zöld, dupla vonalas keret, mint a `banner` lépés |
| `big_banner(text, font: 'slant')` | zöld FIGlet-art, mint a `big_banner` lépés |

Az ERB `trim_mode: '-'`-dal fut, így a `-%>` lenyeli a tag utáni sortörést; az eredmény minden `\n`-je `\r\n`-ként megy ki.

`screens/welcome.erb`:

```erb
<%= clr -%>
<%= big_banner('Harbor') %>

  <%= cyan %>The Harbor BBS<%= reset %>  <%= gray %>node <%= @node %>, <%= @year %><%= reset %>
<% if @name -%>
  <%= white %>Welcome back, <%= @name %>.<%= reset %>
<% end -%>

```

A flow:

```ruby
DEMO = BBS::Flow.define(screens_dir: File.join(__dir__, 'screens')) do
  screen :welcome, year: Time.now.year, node: :session_id
  ask :name, prompt: 'Name'
  screen :welcome, year: Time.now.year, node: :session_id
end
```

Ugyanez a renderer közvetlenül is hívható, pl. egy `call`-ból vagy egy tesztből: a `BBS::Renderer.render('welcome', { year: 2026 }, dir: 'screens')` Stringet ad vissza.

# Bannerek

A `banner` és a `big_banner` lépés, valamint a template-helperek mind a `BBS::Banner`-ből jönnek, amit magad is hívhatsz (egy `call`-ban, egy doorban):

| Metódus | Mit ad vissza |
|---|---|
| `BBS::Banner.render(text, color: BBS::Ansi::BRIGHT_GREEN, padding: 2)` | dupla vonalas keret a `text` körül, két szóközzel behúzva, `\r\n` sorvégekkel, előtte és utána üres sorral |
| `BBS::Banner.render_big(text, color: BBS::Ansi::BRIGHT_GREEN, font: 'slant')` | a `text` FIGlet-artja (`font` nevű artii-font), minden sor két szóközzel behúzva és színezve, `\r\n` sorvégekkel |

A `color` ANSI SGR-szekvencia, általában a paletta egy színe (`BBS::Ansi::CYAN`); egy stílus színét a `BBS::FlowRunner::STYLES[:info]` adja.

```ruby
require 'bbs'

puts BBS::Banner.render('HELLO', color: BBS::FlowRunner::STYLES[:info])
puts BBS::Banner.render_big('BBS', font: 'small')

DEMO = BBS::Flow.define do
  call { |_ctx, runner| runner.write BBS::Banner.render_big('NEWS', color: BBS::Ansi::BRIGHT_CYAN, font: 'standard') }
end
```

```text
  ╔═════════╗
  ║  HELLO  ║
  ╚═════════╝

   ___ ___ ___
  | _ ) _ ) __|
  | _ \ _ \__ \
  |___/___/___/
```

# Egy teljes board: login és menü

Egy board üdvözlőképernyővel, sysop-PIN-es loginnal, egy felhasználói store-ral, egy üzenetolvasóval és egy fiókoldallal. A mappa:

```text
harbor/
├── bbs.rb
├── screens/welcome.erb   # a fenti "Screenek (ERB)" template-je
└── data/                 # itt jön létre a users.sqlite3
```

`bbs.rb`:

```ruby
require 'bbs'

USERS = BBS::Store.new(path: File.join(__dir__, 'data/users.csv'),
                       headers: %w[session_id timestamp handle email])

Post = Struct.new(:title, :author, :body)
POSTS = [
  Post.new('Welcome aboard', 'sysop', 'This is the Harbor. Be nice, keep it short.'),
  Post.new('Door games', 'sysop', 'Three doors are coming next month.')
].freeze

PINS = { 'sysop' => '1234' }.freeze

READ_POSTS = lambda do |_ctx, runner|
  loop do
    post = runner.select(POSTS.map { |p| ["#{p.title} (#{p.author})", p] }, hint: 'Read which post?')
    break nil unless post

    runner.line post.title, style: :prompt
    runner.line post.body
    break :halt if runner.ask('  RETURN for the list: ').nil?
  end
end

MAIN = BBS::Flow.define(screens_dir: File.join(__dir__, 'screens')) do
  screen :welcome, year: Time.now.year, node: :session_id

  banner 'LOGIN', style: :info
  ask :handle, prompt: 'Handle', transform: :downcase, validate: :non_empty

  call do |ctx, runner|
    next nil unless PINS.key?(ctx[:handle])

    runner.write '  PIN: '
    pin = runner.readline(mask: '*')
    next :halt if pin.nil?

    ctx[:sysop] = pin.strip == PINS[ctx[:handle]]
    nil
  end

  gate(denied: 'Wrong PIN.') { |ctx| !PINS.key?(ctx[:handle]) || ctx[:sysop] }
  persist :handle, store: USERS
  banner 'ACCESS GRANTED'

  menu 'Main menu', loop: true do
    option('Read posts') { call(&READ_POSTS) }

    option 'Your account' do
      text { |ctx| "Handle:  #{ctx[:handle]}" }
      text { |ctx| "Session: #{ctx[:session_id]}" }
      text { |ctx| "Sysop:   #{ctx[:sysop] ? 'yes' : 'no'}" }
      confirm 'Subscribe to the newsletter?', denied: 'Maybe next time.' do
        ask :email, prompt: 'Email', validate: :email
        persist :email, store: USERS
        say 'Subscribed.', style: :success
      end
    end

    option 'Log off' do
      exit_menu
    end
  end

  say 'Fair winds!', style: :info
end

BBS.configure do |c|
  c.flow = MAIN
end

BBS.start
```

Ha a hívó feliratkozott, a `USERS.find(session_id)` ezt adja: `{ "session_id" => "3f9c2a1be07d4c11", "timestamp" => "2026-10-27T22:17:56Z", "handle" => "sysop", "email" => "a@b.hu" }`; a sor a `data/users.sqlite3`-ban van.

# Egy valódi board: Impostor

Az Impostor BBS a *Definitely not an Impostor* játék debrief-terminálja. A teljes párbeszéde egyetlen Flow egy gemben (`lib/impostor.rb`), saját `screens/` mappával, így ugyanaz a Flow önálló boardként és egy másik board doorjaként is fut (`door :impostor, Impostor::Modern.flow(sessions: store), label: '&Impostor'`). A két arc két modul, az `Impostor::Modern` és az `Impostor::Petscii`, mindkettő a saját screenjeivel (`screens/modern/`, `screens/petscii/`, utóbbi 40 oszlopra rajzolva).

```ruby
require 'bbs'
require_relative 'impostor/code_generator'

module Impostor
  SCREENS = File.expand_path('../screens', __dir__)

  SESSION_HEADERS = %w[session_id timestamp player_name code feedback email].freeze

  def self.sessions_store(path) = BBS::Store.new(path: path, headers: SESSION_HEADERS)

  # The flow both faces share; +screens_dir+ holds the face's screens.
  def self.debrief(sessions:, screens_dir:)
    BBS::Flow.define(screens_dir: screens_dir) do
      screen :welcome

      banner "IDENTITY VERIFICATION", style: :info

      ask :player_name, prompt: "Player Name", transform: :upcase
      ask :code,        prompt: "Code"
      persist :player_name, :code, store: sessions

      pause "Verifying identity...", seconds: 1.2

      gate denied: "Access denied. Code mismatch." do |ctx|
        CodeGenerator.decrypt(ctx[:code]) == ctx[:player_name][0, CodeGenerator::NAME_LEN]
      end

      confirm "Norman, is that you?", denied: "Very well. Goodbye." do
        screen :congrats

        menu ">", loop: true do
          option "Leave feedback" do
            ask :feedback, prompt: "Feedback"
            persist :feedback, store: sessions
            say "Feedback recorded. Thank you.", style: :success
          end

          option "Subscribe to updates" do
            ask :email, prompt: "Email address", validate: :email
            persist :email, store: sessions
            say "Perfect. We'll be in touch.", style: :success
          end

          option "Exit" do
            exit_menu
          end
        end
      end
    end
  end

  module Modern
    def self.flow(sessions:) = Impostor.debrief(sessions: sessions, screens_dir: File.join(SCREENS, 'modern'))
  end

  module Petscii
    def self.flow(sessions:) = Impostor.debrief(sessions: sessions, screens_dir: File.join(SCREENS, 'petscii'))
  end
end
```

Amit érdemes észrevenni: a flow-t egy metódus építi, így a store és az arc paraméter, nem globális; a kódot még a gate előtt menti, így a sikertelen próbálkozások is megmaradnak; a board többi része a `confirm` blokkjában van, így a "no" udvariasan lezárja; a board `bbs.rb`-je pedig csak összeköti:

```ruby
require 'bundler/setup'
require_relative 'lib/impostor'

sessions = Impostor.sessions_store(File.expand_path('data/sessions.csv', __dir__))

BBS.configure do |c|
  c.flow = BBS::Terminal.flow(title: 'Impostor BBS',
                              modern: Impostor::Modern.flow(sessions: sessions),
                              petscii: Impostor::Petscii.flow(sessions: sessions))
end

BBS.start
```

# Flow futtatása kézzel

A `BBS::FlowRunner.new(session, session_id, flow, context: {}).run` bármelyik flow-t lefuttatja egy sessionön, és a végén visszatér; a visszatérési érték `:halt`, ha megállt, egyébként `nil`. Így futtatja a `BBS::Terminal.flow` a hívó által választott arcot, és így fut egy flow-door egy Applicationön belül. Egy `call` lépésből:

```ruby
RULES = BBS::Flow.define do
  banner 'RULES', style: :info
  text '1. Be nice.'
  text '2. No spam.'
  pause 'Back to the board...', seconds: 2
end

DEMO = BBS::Flow.define do
  ask :handle, prompt: 'Handle'
  call do |ctx, runner|
    BBS::FlowRunner.new(runner.session, ctx[:session_id], RULES, context: ctx).run
  end
  say 'Rules read.'
end
```

Ha a `call` a runner eredményét adja vissza, a belső flow `:halt`-ja a külsőre is átjut. A belső flow saját contextet kap, a `ctx`-ből feltöltve (a kulcsok másolata: amit a belső flow `ask`-ol, az nem jelenik meg a külső `ctx`-ben).

# Lásd még

- [rubbs](https://wiki.teletypegames.org/hu/engines/rubbs) — a főoldal
- [Első lépések](https://wiki.teletypegames.org/hu/engines/rubbs/getting-started) — telepítés, konfiguráció, mappaszerkezet
- [Application](https://wiki.teletypegames.org/hu/engines/rubbs/application) — a widget framework, az `app` lépéssel vagy `APP.run`-nal
- [Doorok](https://wiki.teletypegames.org/hu/engines/rubbs/doors) — flow-k doorként, `BBS::FakeOS` gépek
- [Commodore](https://wiki.teletypegames.org/hu/engines/rubbs/commodore) — `BBS::Terminal.flow`, PETSCII-arcok, `BBS::Petscii::Screen` és `BBS::Selector`
- [Adat és hálózat](https://wiki.teletypegames.org/hu/engines/rubbs/data-and-network) — `BBS::Store`, `BBS::SQLite`, `BBS.log`
- [Telnet és Store](https://wiki.teletypegames.org/hu/engines/rubbs/telnet-and-store) — `runner.session`, billentyűk

'; 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/hu/engines/rubbs.md)
- Previous: [Rajzolás és témák](https://wiki.teletypegames.org/hu/engines/rubbs/drawing.md)
- Next: [Első lépések](https://wiki.teletypegames.org/hu/engines/rubbs/getting-started.md)
