İçeriğe geç
XDD.baby Yazılım Öğrenme
Sözlük Hata rehberi 0/56

Blok 2 · Ders 17

Blok 2 projesi: terminalde metin tabanlı oyun

Modüller, sınıflar, async ve kullanıcı girdisiyle terminalde oynanan, kayıt dosyası tutan bir mini RPG yazıp GitHub'a koyuyorsun.

  • 150–210 dk
  • Önce: Ders 16
  • Proje dersi

Bu dersin sonunda

  • Bir projeyi özellik listesi ve dosya yapısıyla planlayıp her adımda çalışan ara sürümlerle kurabilirsin
  • node:readline/promises ile kullanıcıdan girdi alıp menülü bir oyun döngüsü yazabilirsin
  • Sayı olmayan ya da menüde olmayan girdiyi doğrulama ve try/catch ile yakalayabilirsin
  • Oyunun durumunu node:fs/promises ve JSON ile dosyaya kaydedip geri yükleyebilir, eksik ya da bozuk kayıt dosyasını idare edebilirsin
  • Programına boru (|) ile hazır girdi verip onu otomatik deneyebilirsin
  • README ve .gitignore yazıp projeyi GitHub'a göndermeye hazırlayabilirsin
Bu sayfada
  1. Proje planı
  2. Adım 1: klasör ve package.json
  3. Kullanıcıdan girdi almak
  4. Adım 2: girdi.js ve ana menü
  5. Adım 3: yanlış girdiye dayanmak
  6. Adım 4: Oyuncu sınıfı
  7. Adım 5: savaş
  8. Adım 6: dükkân
  9. Adım 7: kaydetmek ve yüklemek
  10. Adım 8: oyunu otomatik denemek
  11. Modüllere bölmek: geriye bakış
  12. README, .gitignore ve GitHub
  13. Genişletme fikirleri
  14. Görevler
  15. Kendini sına
  16. Kayıt noktası

Blok 2'nin son dersine geldin. Bu derste şimdiye kadar öğrendiğin her şeyi tek bir projede birleştiriyorsun: terminalde oynanan, menüsü, savaşı, dükkânı ve kayıt dosyası olan küçük bir rol yapma oyunu (RPG). Kodu modüllere böleceksin, oyuncuyu bir sınıfla tutacaksın, kullanıcıdan girdi alacaksın, oyunu yanlış girdiye dayanıklı hâle getireceksin ve ilerlemeyi bir JSON dosyasına kaydedeceksin.

Ders uzun; tek oturumda bitirmek zorunda değilsin. Proje adım adım büyüyor ve her adımın sonunda oyun çalışır durumda. Ara verirsen kaldığın adımdan devam edersin.

Proje planı

Kod yazmaya başlamadan önce neyi yapacağını bilmen gerek. Önce oyunun bitmiş hâline bak; dersin sonunda senin terminalinde de böyle çalışacak:

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 1 Karşına bir Alacakarga çıktı! (Can 40, Güç 8)1) Saldır  2) İksir iç  3) KaçSeçimin: 1Alacakarga 15 hasar aldı. Kalan canı: 25Alacakarga sana 6 hasar verdi. Canın: 94/1001) Saldır  2) İksir iç  3) KaçSeçimin: 1Alacakarga 15 hasar aldı. Kalan canı: 10Alacakarga sana 5 hasar verdi. Canın: 89/1001) Saldır  2) İksir iç  3) KaçSeçimin: 1Alacakarga 12 hasar aldı. Kalan canı: 0Alacakarga yenildi! +20 altın, +25 XP 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Ahri | Seviye 1 | XP 25/50Can 89/100 | Güç 12 | Altın 70 | İksir 1 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

Düşmanlar ve hasarlar rastgele seçiliyor; sende başka sayılar çıkacak. Oyuncunun yazdıkları (Ahri, 1, 3, 5) soruların hemen yanında görünüyor.

Özellik listesi

Bir projeyi planlamanın ilk adımı özellik listesi: program ne yapacak, madde madde. Listeyi yazınca ikiye ayır: ilk sürümde mutlaka olacaklar ve sonraya kalabilecekler. Bizim listemiz:

İlk sürüm:

  • Açılışta kahramanın adını sorar; kayıt dosyası varsa oradan devam eder.
  • Ana menü: Savaş, Dükkân, Durum, Kaydet, Çık.
  • Savaş: rastgele bir düşman çıkar; her tur saldır, iksir iç ya da kaç. Kazanınca altın ve XP (tecrübe puanı) gelir, XP dolunca seviye atlanır. Yenilirsen altınının yarısını kaybedersin.
  • Dükkân: iksir, kılıç ve kristal satar; altın yetmezse uyarır.
  • Kaydet ve yükle: ilerleme kayit.json dosyasında durur.
  • Yanlış girdide çökmez, yeniden sorar.

Sonraya: envanter, zorluk seviyesi, güçlü bir boss, kayda tarih yazmak. Bunlar dersin sonunda görev olarak seni bekliyor.

Dosya yapısı

Bütün kodu tek dosyaya yazabilirdin, ama 300 satırlık bir dosyada aradığını bulmak zorlaşır. Ders 14'te kodu modüllere bölmeyi öğrendin. Burada asıl soru şu: hangi kod hangi dosyaya? Kural: her dosyanın tek bir işi olsun ve o işi adından anlayabilesin.

DosyaGöreviDışa aktardıkları (export)
package.jsonProje bilgisi, "type": "module", npm startyok
index.jsGiriş noktası: açılış ve ana menü döngüsüyok
girdi.jsKlavyeden satır okumak, seçimi denetlemeksor, secimeCevir, secimIste, girdiyiKapat
oyuncu.jsOyuncu sınıfı: can, altın, seviye, iksirOyuncu
savas.jsRastgele düşman, tur tur savaşsavas
dukkan.jsEşya listesi, satın almadukkan
kayit.jskayit.json dosyasına yazmak ve okumakkaydet, yukle

Dersin sonunda bunlara README.md ve .gitignore eklenecek; kayit.json ise oyunu kaydedince kendiliğinden oluşacak. Dosyalar kadar önemli bir şey daha var: hangisi hangisini kullanıyor?

index.js açılış + menü savas.js rastgele düşman dukkan.js eşya satın alma kayit.js dosyaya yaz / oku girdi.js satır oku, denetle oyuncu.js class Oyuncu index.js'in kullandıkları öteki modüllerin kullandıkları
Şekil 17.1 Kim kimi kullanıyor? Her ok "şunu import eder" demek. Oklar hep soldan sağa gider.

Oklara dikkat et: hiçbiri index.js'e dönmüyor. index.js herkesi kullanıyor ama hiçbir dosya index.js'i kullanmıyor. En sağdaki girdi.js ve oyuncu.js başka hiçbir proje dosyasını bilmiyor. Bu düzen sayesinde bir dosyayı değiştirdiğinde etkisinin nereye kadar gideceğini okları tersinden izleyerek bulursun.

Adım 1: klasör ve package.json

Projeye yeni bir klasör aç. VS Code'un terminalinde (PowerShell) şu komutları sırayla yaz:

PowerShell
cd C:\kodmkdir metin-rpgcd metin-rpggit initnpm init -ycode .

mkdir klasörü oluşturur, git init klasörde Git'i başlatır, npm init -y bir package.json yazar. code . içinde bulunduğun klasörü (nokta "bu klasör" demek) VS Code'un yeni bir penceresinde açar, tıpkı Ders 6'daki code takim-savasi gibi. Güven sorusu gelirse Yes, I trust the authors'a bas. Bundan sonra o yeni pencerede çalış: menüden Terminal → New Terminal ile terminali aç, istemde PS C:\kod\metin-rpg> görmelisin. Eski penceredeki terminalde git init ile npm init -y şunları yazmış olmalı:

Terminal — PowerShell
PS C:\kod\metin-rpg> git initInitialized empty Git repository in C:/kod/metin-rpg/.git/PS C:\kod\metin-rpg> npm init -yWrote to C:\kod\metin-rpg\package.json: {  "name": "metin-rpg",  "version": "1.0.0",  "description": "",  "main": "index.js",  "scripts": {    "test": "echo \"Error: no test specified\" && exit 1"  },  "keywords": [],  "author": "",  "license": "ISC",  "type": "commonjs"}

Son satıra dikkat: npm'in yeni sürümleri "type": "commonjs" yazıyor. Bu, dosyaların eski CommonJS (require) düzeninde okunacağı demek. Biz import ve export kullanacağız. VS Code'da package.json'u aç ve şu hâle getir (değişen satırlar vurgulu):

package.json
{  "name": "metin-rpg",  "version": "1.0.0",  "description": "Terminalde oynanan küçük bir metin RPG'si",  "main": "index.js",  "scripts": {    "start": "node index.js"  },  "keywords": [],  "author": "",  "license": "ISC",  "type": "module"}
  • "type": "module": dosyalar ES modülü olarak okunur. import, export ve fonksiyon dışında await serbest olur.
  • "start": "node index.js": npm start yazınca oyun başlar (Ders 14'teki npm betikleri). npm'in koyduğu "test" satırını sildik; test yazmayı Ders 53'te öğreneceksin.
  • "description": projenin tek cümlelik tanımı.

Kullanıcıdan girdi almak

Şimdiye kadar yazdığın programlar hep konuştu: console.log ile ekrana yazdılar. Bir oyunun dinlemesi de gerekiyor. Oyuncu bir seçim yazacak, program onu okuyup ona göre davranacak.

Klavyeden gelen metni satır satır okumak için Node'un içinde hazır bir modül var: node:readline/promises. Adı read line, yani "satır oku" demek; sondaki /promises da modülün Promise döndüren sürümünü seçer, böylece cevabı await ile bekleyebilirsin (Ders 15). node: ile başlayan modüller Node'la birlikte gelir; npm ile kurulmaz.

Önce oyunun dışında küçük bir deneme yapalım. Proje klasöründe deneme.js adında yeni bir dosya oluştur ve şunu yaz:

deneme.js
import { createInterface } from "node:readline/promises"import { stdin, stdout } from "node:process" const rl = createInterface({ input: stdin, output: stdout }) const isim = await rl.question("Adın ne? ")console.log(`Merhaba ${isim}!`) rl.close()
Terminal — PowerShell
PS C:\kod\metin-rpg> node deneme.jsAdın ne? AhriMerhaba Ahri!

Program Adın ne? yazdı ve durdu. Sen Ahri yazıp Enter'a basınca kaldığı yerden devam etti. Satır satır:

  • import { stdin, stdout } from "node:process": standart girdi ve standart çıktı kanallarını alır. node:process modülü, Node'un o an çalışan programın kendisi hakkında verdiği bilgileri ve araçları tutar.
  • createInterface({ input: stdin, output: stdout }): "girdiyi klavyeden oku, soruları ekrana yaz" diyen bir arayüz (interface) kurar. rl adı readline'ın kısaltması; çoğu kodda bu adı görürsün.
  • await rl.question("Adın ne? "): soruyu yazar ve oyuncu Enter'a basana kadar bekler. question bir Promise döndürür; await ile içindeki cevabı alırsın (Ders 15).
  • rl.close(): arayüzü kapatır. Bu satır olmazsa program bitmez.

await'i bir fonksiyonun içinde değil, dosyanın en üst düzeyinde kullandık. Ders 15'te gördüğün gibi buna yalnız ES modüllerinde izin var; bu projede package.json'daki "type": "module" satırı her .js dosyasını ES modülü yapıyor.

Sık hataSyntaxError: Cannot use import statement outside a module

Ne zaman çıkar? package.json'daki "type": "commonjs" satırını "module" yapmayı unutunca. Node dosyayı eski düzende okur ve import'u tanımaz. Bu hatayı Ders 14'ten tanıyorsun; yeni bir projede npm init -y'den hemen sonra en sık karşına çıkan hata bu.

Terminal — PowerShell
PS C:\kod\metin-rpg> node deneme.js(node:32380) Warning: Failed to load the ES module: C:\kod\metin-rpg\deneme.js. Make sure to set "type": "module" in the nearest package.json file or use the .mjs extension.(Use `node --trace-warnings ...` to show where the warning was created)C:\kod\metin-rpg\deneme.js:1import { createInterface } from "node:readline/promises"^^^^^^ SyntaxError: Cannot use import statement outside a module    at wrapSafe (node:internal/modules/cjs/loader:1861:18)    ... Node.js v24.20.0

Nasıl düzelir? Uyarı satırı çözümü söylüyor: en yakın package.json içinde "type": "module" olmalı. Değiştir, kaydet, yeniden çalıştır. Parantezdeki node:32380 programın süreç numarası; sende başka bir sayı çıkar.

Cevap her zaman metindir

question ne yazarsan yaz bir metin döndürür. Oyuncu 7 yazsa bile elindeki "7"dir. Bunu unutursan Ders 1'deki "metin + sayı = metin" tuzağına düşersin. deneme.js'e ikinci bir soru ekle:

deneme.js
import { createInterface } from "node:readline/promises"import { stdin, stdout } from "node:process" const rl = createInterface({ input: stdin, output: stdout }) const isim = await rl.question("Adın ne? ")console.log(`Merhaba ${isim}!`)const seviye = await rl.question("Kaçıncı seviyedesin? ")console.log(`Bir sonraki seviyen: ${seviye + 1}`) rl.close()
Terminal — PowerShell
PS C:\kod\metin-rpg> node deneme.jsAdın ne? AhriMerhaba Ahri!Kaçıncı seviyedesin? 7Bir sonraki seviyen: 71

"7" + 1 toplama değil yapıştırma oldu. Sayıya ihtiyacın varsa cevabı Number ile çevir (Ders 9):

deneme.js
const seviye = Number(await rl.question("Kaçıncı seviyedesin? "))console.log(`Bir sonraki seviyen: ${seviye + 1}`)
Terminal — PowerShell
PS C:\kod\metin-rpg> node deneme.jsAdın ne? AhriMerhaba Ahri!Kaçıncı seviyedesin? 7Bir sonraki seviyen: 8

Oyuncu sayı yerine abc yazarsa ne olacak? Number("abc") sonucu NaN. Bunu Adım 3'te ele alacağız. Önce readline'ın üç sık hatası:

Sık hataÇıktıda [object Promise] görmek

Ne zaman çıkar? rl.question'ın önüne await yazmayı unutunca.

deneme.js
import { createInterface } from "node:readline/promises"import { stdin, stdout } from "node:process" const rl = createInterface({ input: stdin, output: stdout }) const isim = rl.question("Adın ne? ")console.log(`Merhaba ${isim}!`) rl.close()
Çıktı
Adın ne? Merhaba [object Promise]!

Program cevabı beklemedi bile. isim kutusuna cevap değil, cevap sözü (Promise) kondu; metne çevrilince [object Promise] oldu. Ardından rl.close() çalıştı ve program bitti.

Nasıl düzelir? const isim = await rl.question("Adın ne? "). Promise döndüren her çağrının önünde await olmalı (Ders 15).

Sık hataAbortError: Aborted with Ctrl+C

Ne zaman çıkar? Program question ile cevap beklerken Ctrl+C'ye basınca. Node bekleyen soruyu iptal eder; Promise hatayla sonuçlanır ve onu yakalayan olmadığı için program çöker.

Terminal — PowerShell
PS C:\kod\metin-rpg> node deneme.jsAdın ne? node:internal/readline/interface:1347            this[kQuestionReject]?.(new AbortError('Aborted with Ctrl+C'));                                    ^ AbortError: Aborted with Ctrl+C    at [_ttyWrite] (node:internal/readline/interface:1347:37)    ...    at Readable.push (node:internal/streams/readable:398:5) {  code: 'ABORT_ERR'} Node.js v24.20.0

Yığın izinde (Ders 13) senin dosyan hiç yok; hatayı Node'un kendi içi fırlattı. Mesaj yine de ne olduğunu söylüyor: "Ctrl+C ile iptal edildi".

Nasıl düzelir? Aslında bu bir arıza değil, oyuncunun "çık" demesi. Şimdilik oyundan menüdeki 5 ile çık. Adım 8'te girdi modülünü yeniden yazınca Ctrl+C programı sessizce kapatacak.

Sık hataError [ERR_USE_AFTER_CLOSE]: readline was closed

Ne zaman çıkar? rl.close()'dan sonra yeniden rl.question çağırınca.

deneme.js
import { createInterface } from "node:readline/promises"import { stdin, stdout } from "node:process" const rl = createInterface({ input: stdin, output: stdout }) const isim = await rl.question("Adın ne? ")rl.close()const sehir = await rl.question("Nerelisin? ")console.log(`${isim}, ${sehir}`)
Terminal — PowerShell
PS C:\kod\metin-rpg> node deneme.jsAdın ne? Ahrinode:internal/readline/interface:450      throw new ERR_USE_AFTER_CLOSE('readline');            ^ Error [ERR_USE_AFTER_CLOSE]: readline was closed    at [kQuestion] (node:internal/readline/interface:450:13)    ...    at file:///C:/kod/metin-rpg/deneme.js:8:24    at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {  code: 'ERR_USE_AFTER_CLOSE'} Node.js v24.20.0

Nasıl düzelir? Yığın izindeki deneme.js:8:24 seni 8. satıra götürüyor: kapattığın arayüzü kullanmaya çalışmışsın. close'u bütün sorular bittikten sonra, en sona koy.

Adım 2: girdi.js ve ana menü

deneme.js'deki kodu oyunun her yerinde kullanacağız: menüde, savaşta, dükkânda. Her dosyada ayrı createInterface kurmak yerine girdiyle ilgili her şeyi tek bir modülde toplayalım. girdi.js adında yeni bir dosya oluştur:

girdi.js
import { createInterface } from "node:readline/promises"import { stdin, stdout } from "node:process" const rl = createInterface({ input: stdin, output: stdout }) export async function sor(soru) {    const cevap = await rl.question(soru)    return cevap.trim()} export function girdiyiKapat() {    rl.close()}
  • sor(soru) soruyu sorar, cevabın başındaki ve sonundaki boşlukları trim ile atıp döndürür (Ders 9). Oyuncu yanlışlıkla " 5 " yazarsa "5" olur.
  • girdiyiKapat() arayüzü kapatır.
  • rl dışa aktarılmıyor. Öteki dosyalar onu bilmiyor, yalnız bu iki fonksiyonu kullanıyor. Yarın girdiyi başka bir yoldan okumaya karar verirsen yalnız bu dosyayı değiştirirsin.

Bir modül kaç dosyada import edilirse edilsin yalnız bir kez çalışır. Yani girdi.js'i üç dosya kullansa da tek bir rl olur ve hepsi aynı klavyeyi paylaşır.

Şimdi oyunun giriş noktası olan index.js'i oluştur. Bu ilk sürümde menünün iskeletini kuruyoruz; dalların çoğu şimdilik "yakında" diyor:

index.js — metin-rpg — Visual Studio Code
index.js
import { sor, girdiyiKapat } from "./girdi.js" console.log("=== Sihirdar Vadisi: Metin RPG ===")const isim = await sor("Kahramanının adı ne? ")console.log(`Hoş geldin, ${isim}!`) while (true) {    console.log("\n1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) Çık")    const secim = await sor("Seçimin: ")    if (secim === "5") {        break    } else if (secim === "1") {        console.log("Savaş yakında.")    } else if (secim === "2") {        console.log("Dükkân yakında.")    } else if (secim === "3") {        console.log("Durum yakında.")    } else if (secim === "4") {        console.log("Kayıt yakında.")    }} console.log("Görüşürüz!")girdiyiKapat()
PROBLEMSOUTPUTDEBUG CONSOLETERMINAL
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Durum yakında. 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 1Savaş yakında. 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!
Ln 1, Col 1Spaces: 4UTF-8JavaScript

Ara sürüm 1: menü dönüyor, 5 ile çıkılıyor.

  • while (true) kendiliğinden hiç bitmeyen bir döngü kurar; içindeki break döngüden çıkmanın tek yolu (Ders 5).
  • "\n" (ters eğik çizgi ve n) metnin içinde yeni satır demek; ekrana iki harf olarak değil, satır sonu olarak çıkar. Burada menüden önce boş bir satır bırakır, ekran okunaklı olur.
  • sor metin döndürdüğü için seçimi "5" gibi bir metinle karşılaştırdık.
Açılış kaydı yükle ya da adını sor Menüyü yaz Seçimi iste (1–5) yanlışsa yeniden sorar 5 Çık: girdiyi kapat seçime göre bir iş 1 → savas(oyuncu) 2 → dukkan(oyuncu) 3 → oyuncu.durum() 4 → kaydet(oyuncu) 1–4 menüye dön
Şekil 17.2 Oyun döngüsü: menü, seçim, iş ve yeniden menü. 5 seçilince döngüden çıkılır.

Adım 3: yanlış girdiye dayanmak

Ara sürüm 1'i biraz zorla ve menüde olmayan şeyler yaz: bir sözcük (abc), menüde olmayan bir sayı (7) ve hiçbir şey yazmadan Enter. En sonda 5 ile çık.

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: abc 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 7 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

abc, 7 ve boş Enter sessizce yutuldu; menü hiçbir şey demeden yeniden geldi. Oyuncu ne olduğunu anlamaz. Daha kötüsü, seçimleri sayıya çevirmeye başlayınca sessiz hatalar çıkar. Number'ın böyle girdilerle ne yaptığına bak; deneme.js'in içini sil ve şunu yaz:

deneme.js
console.log(Number("3"))console.log(Number(" 3 "))console.log(Number("abc"))console.log(Number(""))console.log(Number("2.5"))console.log(Number.isInteger(Number("2.5")))
Çıktı
33NaN02.5false

Boş metin 0 oluyor! Dükkânda 0 "çık" demek olacak; oyuncu yanlışlıkla Enter'a basınca dükkândan atılırdı. Son satırdaki Number.isInteger(x)'i ilk kez görüyorsun: "x bir tam sayı mı?" diye sorar, NaN ve 2.5 için false döndürür.

Çözüm Ders 13'teki girdi doğrulama: girdiyi kullanmadan önce denetle, uymuyorsa açık bir mesajla hata fırlat. girdi.js'e iki fonksiyon ekle:

girdi.js
import { createInterface } from "node:readline/promises"import { stdin, stdout } from "node:process" const rl = createInterface({ input: stdin, output: stdout }) export async function sor(soru) {    const cevap = await rl.question(soru)    return cevap.trim()} export function secimeCevir(metin, enAz, enCok) {    const sayi = Number(metin)    if (metin === "" || !Number.isInteger(sayi)) {        throw new Error(`"${metin}" bir tam sayı değil`)    }    if (sayi < enAz || sayi > enCok) {        throw new Error(`${enAz} ile ${enCok} arasında bir sayı yaz`)    }    return sayi} export async function secimIste(soru, enAz, enCok) {    while (true) {        const cevap = await sor(soru)        try {            return secimeCevir(cevap, enAz, enCok)        } catch (hata) {            console.log(`Geçersiz seçim: ${hata.message}.`)        }    }} export function girdiyiKapat() {    rl.close()}
  • secimeCevir tek bir iş yapar: metni sayıya çevirir ya da açıklamalı bir hata fırlatır. Klavyeyle ilgisi yok; bu yüzden tek başına denenebilir.
  • secimIste doğru bir seçim gelene kadar sorar. try içindeki return başarılı olursa fonksiyondan hemen çıkılır. Hata olursa catch mesajı yazar ve döngü aynı soruyu yeniden sorar.
  • Hata mesajı oyuncu için yazıldı: neyin yanlış olduğunu ve ne yapması gerektiğini söylüyor.

secimeCevir'i oyuna bağlamadan önce sınır değerleriyle dene (Ders 2): 1 ve 5 kabul edilmeli, 0 ve 6 reddedilmeli. deneme.js'in içini sil ve şunu yaz:

deneme.js
import { secimeCevir, girdiyiKapat } from "./girdi.js" const denemeler = ["3", "abc", "", "0", "1", "5", "6", "2.5"]for (const metin of denemeler) {    try {        console.log(`"${metin}" -> ${secimeCevir(metin, 1, 5)}`)    } catch (hata) {        console.log(`"${metin}" -> hata: ${hata.message}`)    }} girdiyiKapat()
Terminal — PowerShell
PS C:\kod\metin-rpg> node deneme.js"3" -> 3"abc" -> hata: "abc" bir tam sayı değil"" -> hata: "" bir tam sayı değil"0" -> hata: 1 ile 5 arasında bir sayı yaz"1" -> 1"5" -> 5"6" -> hata: 1 ile 5 arasında bir sayı yaz"2.5" -> hata: "2.5" bir tam sayı değil

Sondaki girdiyiKapat() satırına dikkat. girdi.js'i import edince arayüz kuruldu; kapatmasaydık program hiç soru sormadığı hâlde girdi beklemeye devam ederdi.

Şimdi index.js'i sayılarla çalışacak hâle getir. Değişen satırlar vurgulu:

index.js
import { sor, secimIste, girdiyiKapat } from "./girdi.js" console.log("=== Sihirdar Vadisi: Metin RPG ===")const isim = await sor("Kahramanının adı ne? ")console.log(`Hoş geldin, ${isim || "İsimsiz"}!`) while (true) {    console.log("\n1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) Çık")    const secim = await secimIste("Seçimin: ", 1, 5)    if (secim === 5) {        break    } else if (secim === 1) {        console.log("Savaş yakında.")    } else if (secim === 2) {        console.log("Dükkân yakında.")    } else if (secim === 3) {        console.log("Durum yakında.")    } else if (secim === 4) {        console.log("Kayıt yakında.")    }} console.log("Görüşürüz!")girdiyiKapat()

isim || "İsimsiz": oyuncu adını boş bırakırsa boş metin yanlış sayılır (Ders 12) ve yerine "İsimsiz" kullanılır. Yanlış girdileri yeniden dene; bu kez 2.5 gibi küsuratlı bir sayı da yaz:

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: abcGeçersiz seçim: "abc" bir tam sayı değil.Seçimin:Geçersiz seçim: "" bir tam sayı değil.Seçimin: 9Geçersiz seçim: 1 ile 5 arasında bir sayı yaz.Seçimin: 2.5Geçersiz seçim: "2.5" bir tam sayı değil.Seçimin: 3Durum yakında. 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

Artık her yanlış girdi açıklanıyor ve aynı soru yeniden geliyor. Ara sürüm 2 hazır.

Adım 4: Oyuncu sınıfı

Oyuncunun adı, canı, altını ve seviyesi hep birlikte değişiyor ve uyması gereken kurallar var: can sıfırın altına inmez, iksir canı en yüksek değerin üstüne çıkaramaz. Veriyi ve kurallarını bir arada tutmanın yolunu Ders 16'da gördün: sınıf. oyuncu.js adında yeni bir dosya oluştur:

oyuncu.js
export class Oyuncu {    constructor(isim) {        this.isim = isim        this.seviye = 1        this.xp = 0        this.maxHp = 100        this.hp = 100        this.guc = 12        this.gold = 50        this.iksir = 1    }     get canliMi() {        return this.hp > 0    }     get gerekenXp() {        return this.seviye * 50    }     hasarAl(miktar) {        this.hp = Math.max(0, this.hp - miktar)    }     iksirIc() {        if (this.iksir === 0) {            return false        }        this.iksir--        this.hp = Math.min(this.maxHp, this.hp + 40)        return true    }     xpKazan(miktar) {        this.xp = this.xp + miktar        let atlanan = 0        while (this.xp >= this.gerekenXp) {            this.xp = this.xp - this.gerekenXp            this.seviye++            this.maxHp = this.maxHp + 20            this.guc = this.guc + 3            this.hp = this.maxHp            atlanan++        }        return atlanan    }     durum() {        const satir1 = `${this.isim} | Seviye ${this.seviye} | XP ${this.xp}/${this.gerekenXp}`        const satir2 = `Can ${this.hp}/${this.maxHp} | Güç ${this.guc} | Altın ${this.gold} | İksir ${this.iksir}`        return satir1 + "\n" + satir2    }}
  • canliMi ve gerekenXp birer getter: parantezsiz okunur (oyuncu.canliMi) ama her okunuşta yeniden hesaplanır. Seviye 2'de gereken XP kendiliğinden 100 olur.
  • hasarAl, canı Math.max(0, …) ile sıfırda durdurur (Ders 9); can hiç eksiye düşmez. iksirIc de Math.min ile canın en yüksek değeri aşmasını engeller.
  • iksirIc iksir yoksa false döndürür. Savaş kodu bu cevaba bakıp "İksirin kalmadı!" diyecek.
  • xpKazan bir while ile çalışır: tek seferde çok XP gelirse birden fazla seviye atlanabilir. Kaç seviye atlandığını döndürür.
  • durum ekrana bir şey yazmaz, metin döndürür. Yazdırmak çağıranın işi; böylece metni denemek ve başka yerde kullanmak kolaylaşır.

Sınıfı oyuna bağlamadan önce tek başına dene. oyuncu.js'in en altına, bir boş satırdan sonra şunları geçici olarak ekle ve node oyuncu.js ile çalıştır:

oyuncu.js
const ahri = new Oyuncu("Ahri")console.log(ahri.durum())ahri.hasarAl(130)console.log(ahri.hp, ahri.canliMi)console.log(ahri.iksirIc(), ahri.hp)console.log(ahri.iksirIc(), ahri.hp)console.log(ahri.xpKazan(120))console.log(ahri.durum())console.log(ahri.xpKazan(30), ahri.seviye, ahri.xp)
Çıktı
Ahri | Seviye 1 | XP 0/50Can 100/100 | Güç 12 | Altın 50 | İksir 10 falsetrue 40false 401Ahri | Seviye 2 | XP 70/100Can 120/120 | Güç 15 | Altın 50 | İksir 01 3 0

Sınır değerlerine bak. Can 100 iken gelen 130 hasar canı 0'a indirdi, eksiye değil. İkinci iksir yokken false geldi ve can değişmedi.

120 XP bir seviye atlattı, 70 XP arttı. Ardından gelen 30 XP sayacı tam 100'e çıkardı ve tam sınırda seviye atlandı.

Şimdi index.js'te oyuncuyu oluştur ve Durum seçeneğini bağla. Yeni ve değişen satırlar vurgulu; geri kalanı aynı:

index.js
import { Oyuncu } from "./oyuncu.js"import { sor, secimIste, girdiyiKapat } from "./girdi.js" console.log("=== Sihirdar Vadisi: Metin RPG ===")const isim = await sor("Kahramanının adı ne? ")const oyuncu = new Oyuncu(isim || "İsimsiz")console.log(`Hoş geldin, ${oyuncu.isim}!`) while (true) {    console.log("\n1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) Çık")    const secim = await secimIste("Seçimin: ", 1, 5)    if (secim === 5) {        break    } else if (secim === 1) {        console.log("Savaş yakında.")    } else if (secim === 2) {        console.log("Dükkân yakında.")    } else if (secim === 3) {        console.log(oyuncu.durum())    } else if (secim === 4) {        console.log("Kayıt yakında.")    }} console.log("Görüşürüz!")girdiyiKapat()
Terminal — PowerShell
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Ahri | Seviye 1 | XP 0/50Can 100/100 | Güç 12 | Altın 50 | İksir 1 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

Ara sürüm 3 hazır: Durum seçeneği oyuncunun gerçek bilgilerini gösteriyor. İlk kez kendi yazdığın bir dosyadan sınıf import ettin. Bu adımda iki hata çok sık çıkar:

Sık hataError [ERR_MODULE_NOT_FOUND]: Cannot find module

Ne zaman çıkar? import satırındaki dosya yolunda .js uzantısını unutunca: from "./oyuncu".

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.jsnode:internal/modules/run_main:107    triggerUncaughtException(    ^ Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'C:\kod\metin-rpg\oyuncu' imported from C:\kod\metin-rpg\index.jsDid you mean to import "./oyuncu.js"?    at finalizeResolution (node:internal/modules/esm/resolve:272:11)    ... Node.js v24.20.0

Nasıl düzelir? Hata satırının hemen altındaki Did you mean… ("şunu mu demek istedin?") satırı cevabı veriyor: "./oyuncu.js". ES modüllerinde dosya uzantısı zorunlu.

Sık hataSyntaxError: The requested module './oyuncu.js' does not provide an export named 'Oyuncu'

Ne zaman çıkar? İçe aktardığın ad öteki dosyada dışa aktarılmamışsa; örneğin class Oyuncu yazıp başına export koymayı unutunca.

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.jsfile:///C:/kod/metin-rpg/index.js:1import { Oyuncu } from "./oyuncu.js"         ^^^^^^SyntaxError: The requested module './oyuncu.js' does not provide an export named 'Oyuncu'    at #asyncInstantiate (node:internal/modules/esm/module_job:455:21)    ... Node.js v24.20.0

Nasıl düzelir? oyuncu.js'te export class Oyuncu yaz. Süslü parantezin içindeki ad da birebir aynı olmalı: dosyanın adı değil, export'tan sonra yazan ad içe aktarılır. Dosya adına bakıp import { oyuncu } (küçük o ile) yazarsan bizim index.js'te bu kez başka bir hata çıkar: 6. satırda zaten const oyuncu olduğu için Node SyntaxError: Identifier 'oyuncu' has already been declared der. Ders 1'de aynı adla iki kez let yazınca gördüğün hatanın aynısı.

Adım 5: savaş

savas.js adında yeni bir dosya oluştur. Oyunun en uzun dosyası bu; önce baştan sona yaz, sonra parça parça bakalım:

savas.js
import { secimIste } from "./girdi.js" const DUSMANLAR = [    { isim: "Minyon", hp: 30, guc: 6, gold: 15, xp: 20 },    { isim: "Alacakarga", hp: 40, guc: 8, gold: 20, xp: 25 },    { isim: "Kurt", hp: 55, guc: 10, gold: 30, xp: 35 },    { isim: "Gromp", hp: 70, guc: 12, gold: 40, xp: 45 }] function zar(enAz, enCok) {    return enAz + Math.floor(Math.random() * (enCok - enAz + 1))} export async function savas(oyuncu) {    const sablon = DUSMANLAR[zar(0, DUSMANLAR.length - 1)]    const dusman = { ...sablon }    console.log(`\nKarşına bir ${dusman.isim} çıktı! (Can ${dusman.hp}, Güç ${dusman.guc})`)     while (oyuncu.canliMi && dusman.hp > 0) {        const secim = await secimIste("1) Saldır  2) İksir iç  3) Kaç\nSeçimin: ", 1, 3)        if (secim === 1) {            const hasar = zar(oyuncu.guc - 4, oyuncu.guc + 4)            dusman.hp = Math.max(0, dusman.hp - hasar)            console.log(`${dusman.isim} ${hasar} hasar aldı. Kalan canı: ${dusman.hp}`)        } else if (secim === 2) {            if (!oyuncu.iksirIc()) {                console.log("İksirin kalmadı!")                continue            }            console.log(`İksir içtin. Canın: ${oyuncu.hp}/${oyuncu.maxHp}`)        } else {            if (Math.random() < 0.5) {                console.log("Kaçmayı başardın.")                return            }            console.log("Kaçamadın!")        }         if (dusman.hp > 0) {            const hasar = zar(dusman.guc - 3, dusman.guc + 3)            oyuncu.hasarAl(hasar)            console.log(`${dusman.isim} sana ${hasar} hasar verdi. Canın: ${oyuncu.hp}/${oyuncu.maxHp}`)        }    }     if (oyuncu.canliMi) {        oyuncu.gold = oyuncu.gold + dusman.gold        console.log(`${dusman.isim} yenildi! +${dusman.gold} altın, +${dusman.xp} XP`)        const atlanan = oyuncu.xpKazan(dusman.xp)        if (atlanan > 0) {            console.log(`Seviye atladın! Artık ${oyuncu.seviye}. seviyedesin, canın doldu.`)        }    } else {        const kayip = Math.floor(oyuncu.gold / 2)        oyuncu.gold = oyuncu.gold - kayip        oyuncu.hp = oyuncu.maxHp        console.log(`Yenildin... ${kayip} altın düşürdün. Üste yeniden doğdun.`)    }}
  • DUSMANLAR her düşman türünün şablonunu tutar. Hiç değişmeyecek ayar listelerinin adını büyük harfle yazmak yaygın bir alışkanlık; okuyan "bu bir ayar" diye anlar. Dizi dışa aktarılmıyor: düşmanları yalnız savas.js bilir.
  • zar(enAz, enCok) iki sınır dahil rastgele bir tam sayı verir (Ders 6'daki Math.random ve Math.floor). zar(0, DUSMANLAR.length - 1) rastgele bir sıra numarası seçer.
  • const dusman = { ...sablon } şablonun kopyasını alır (Ders 12). Neden şart olduğunu aşağıdaki hata kartında göreceksin.
  • while (oyuncu.canliMi && dusman.hp > 0): ikisi de ayaktayken tur döner. Senin hasarın gücünün 4 altı ile 4 üstü arasında, düşmanınki 3 altı ile 3 üstü arasında.
  • İksir yoksa continue turu baştan başlatır (Ders 5); düşman bu yüzden bedava vuruş kazanmaz.
  • Kaçma şansı yüzde 50: Math.random() < 0.5. Kaçarsan return savaşı hemen bitirir.
  • Döngüden sonra kim ayakta kaldıysa ona göre ödül ya da ceza gelir.

Savaşın bütün hasarları zar'dan geliyor; ona güvenmeden önce dene. Rastgele bir fonksiyonu bir kez çalıştırmak bir şey kanıtlamaz. Ders 6'daki gibi çok kez atıp en küçük ve en büyük sonuca bakarsın; bu kez ikisini döngüyle aramak yerine diziyi Math.min ile Math.max'a yayıyoruz (Ders 12). deneme.js'in içini sil ve şunu yaz:

deneme.js
function zar(enAz, enCok) {    return enAz + Math.floor(Math.random() * (enCok - enAz + 1))} console.log(zar(8, 16), zar(8, 16), zar(8, 16)) const atislar = []for (let i = 0; i < 1000; i++) {    atislar.push(zar(8, 16))}console.log("En küçük:", Math.min(...atislar), "En büyük:", Math.max(...atislar))
Çıktıörnek, sende farklı çıkabilir
12 13 8En küçük: 8 En büyük: 16

zar(8, 16), gücü 12 olan Ahri'nin vuruşu: en az 8, en çok 16 hasar. İlk satır her çalıştırmada değişir, ikinci satır hep aynıdır; 1000 atışta 8'in ya da 16'nın hiç gelmemesi pratikte imkânsız. Formüldeki + 1'i unutursan ikinci satır En küçük: 8 En büyük: 15 olur ve 16 hiç gelmez. Hata mesajı vermeyen bu yanlışı ancak böyle bir sınır denemesiyle yakalarsın.

Sık hataDüşman canı 0 ile doğuyor

Ne zaman çıkar? Düşmanı şablondan kopyalamadan kullanınca: const dusman = sablon. Hata mesajı yok, ama oyun bozuk:

Terminal — PowerShell
# ... önceki savaşlarda bir Gromp ve bir Minyon yenildi1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 1 Karşına bir Gromp çıktı! (Can 0, Güç 12)Gromp yenildi! +40 altın, +45 XP

İkinci Gromp savaşmadan yenildi ve bedava altın verdi. Sebebi Ders 12'deki referans: dusman ile DUSMANLAR içindeki obje aynı kutu. Birinci savaşta dusman.hp'yi sıfıra indirince şablonun canı da sıfırlandı. Küçük bir örnekle:

deneme.js
const DUSMANLAR = [{ isim: "Kurt", hp: 55 }] const birinci = DUSMANLAR[0]birinci.hp = 0console.log("Birinci kurt yenildi.") const ikinci = DUSMANLAR[0]console.log("İkinci kurdun canı:", ikinci.hp)
Çıktı
Birinci kurt yenildi.İkinci kurdun canı: 0

Nasıl düzelir? Her savaşta şablonun kopyasıyla çalış: const dusman = { ...sablon }. Kopyanın canı azalır, şablonunki hep aynı kalır.

Savaşın yenilgi dalını denemek için oyunu dakikalarca oynaman gerekmez. Bir parçayı ayrı denemek (Ders 6) burada da işe yarar. savas-deneme.js adında yeni bir dosya oluştur; canı az bir Teemo'yu savaşa sok:

savas-deneme.js
import { Oyuncu } from "./oyuncu.js"import { savas } from "./savas.js"import { girdiyiKapat } from "./girdi.js" const teemo = new Oyuncu("Teemo")teemo.hp = 15await savas(teemo)console.log(teemo.durum())girdiyiKapat()
Terminal — PowerShell
PS C:\kod\metin-rpg> node savas-deneme.js Karşına bir Kurt çıktı! (Can 55, Güç 10)1) Saldır  2) İksir iç  3) KaçSeçimin: 1Kurt 16 hasar aldı. Kalan canı: 39Kurt sana 8 hasar verdi. Canın: 7/1001) Saldır  2) İksir iç  3) KaçSeçimin: 1Kurt 10 hasar aldı. Kalan canı: 29Kurt sana 11 hasar verdi. Canın: 0/100Yenildin... 25 altın düşürdün. Üste yeniden doğdun.Teemo | Seviye 1 | XP 0/50Can 100/100 | Güç 12 | Altın 25 | İksir 1

15 canla başlayan Teemo iki turda yenildi, 50 altının yarısını kaybetti ve canı doldu: yenilgi dalı çalışıyor. Rastgele olduğu için sende başka bir düşman ve başka sayılar çıkacak. Teemo bu kez kazandıysa programı yeniden çalıştır ya da teemo.hp'yi 1 yap; o zaman düşmanın ilk vuruşu yeter.

Şimdi savaşı menüye bağla. index.js'in en üstündeki iki import satırının altına import { savas } from "./savas.js" satırını ekle ve 1'in dalındaki console.log("Savaş yakında.") satırını await savas(oyuncu) yap. await'i unutma: savas bir async fonksiyon. await olmazsa menü savaşı beklemez; menü ile savaş aynı anda soru sormaya kalkar, ekran karışır ve oyun kilitlenir.

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 1 Karşına bir Minyon çıktı! (Can 30, Güç 6)1) Saldır  2) İksir iç  3) KaçSeçimin: 1Minyon 9 hasar aldı. Kalan canı: 21Minyon sana 5 hasar verdi. Canın: 95/1001) Saldır  2) İksir iç  3) KaçSeçimin: 1Minyon 8 hasar aldı. Kalan canı: 13Minyon sana 8 hasar verdi. Canın: 87/1001) Saldır  2) İksir iç  3) KaçSeçimin: 1Minyon 14 hasar aldı. Kalan canı: 0Minyon yenildi! +15 altın, +20 XP 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 1 Karşına bir Kurt çıktı! (Can 55, Güç 10)1) Saldır  2) İksir iç  3) KaçSeçimin: 1Kurt 9 hasar aldı. Kalan canı: 46Kurt sana 11 hasar verdi. Canın: 76/1001) Saldır  2) İksir iç  3) KaçSeçimin: 1Kurt 15 hasar aldı. Kalan canı: 31Kurt sana 12 hasar verdi. Canın: 64/1001) Saldır  2) İksir iç  3) KaçSeçimin: 1Kurt 10 hasar aldı. Kalan canı: 21Kurt sana 9 hasar verdi. Canın: 55/1001) Saldır  2) İksir iç  3) KaçSeçimin: 1Kurt 10 hasar aldı. Kalan canı: 11Kurt sana 10 hasar verdi. Canın: 45/1001) Saldır  2) İksir iç  3) KaçSeçimin: 1Kurt 11 hasar aldı. Kalan canı: 0Kurt yenildi! +30 altın, +35 XPSeviye atladın! Artık 2. seviyedesin, canın doldu. 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Ahri | Seviye 2 | XP 5/100Can 120/120 | Güç 15 | Altın 95 | İksir 1 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

İki savaşta toplam 55 XP geldi: 50'si seviye atlamaya gitti, 5'i yeni sayaca kaldı. Ara sürüm 4 hazır.

Adım 6: dükkân

Dükkânın işi küçük ama içinde güzel bir fikir var: her ürün, satın alınınca ne olacağını kendi taşıyor. dukkan.js adında yeni bir dosya oluştur:

dukkan.js
import { secimIste } from "./girdi.js" const URUNLER = [    {        isim: "Can İksiri",        fiyat: 30,        aciklama: "+1 iksir",        etki: (oyuncu) => { oyuncu.iksir++ }    },    {        isim: "Uzun Kılıç",        fiyat: 90,        aciklama: "+4 güç",        etki: (oyuncu) => { oyuncu.guc = oyuncu.guc + 4 }    },    {        isim: "Yakut Kristali",        fiyat: 120,        aciklama: "+25 en yüksek can",        etki: (oyuncu) => {            oyuncu.maxHp = oyuncu.maxHp + 25            oyuncu.hp = oyuncu.hp + 25        }    }] export async function dukkan(oyuncu) {    while (true) {        console.log(`\n--- Dükkân (altının: ${oyuncu.gold}) ---`)        URUNLER.forEach((urun, i) => {            console.log(`${i + 1}) ${urun.isim}: ${urun.fiyat} altın (${urun.aciklama})`)        })        console.log("0) Dükkândan çık")         const secim = await secimIste("Ne alacaksın? ", 0, URUNLER.length)        if (secim === 0) {            return        }        const urun = URUNLER[secim - 1]        if (oyuncu.gold < urun.fiyat) {            console.log(`Altının yetmiyor: ${urun.fiyat - oyuncu.gold} altın eksik.`)        } else {            oyuncu.gold = oyuncu.gold - urun.fiyat            urun.etki(oyuncu)            console.log(`${urun.isim} aldın. Kalan altın: ${oyuncu.gold}`)        }    }}
  • etki alanında bir fonksiyon duruyor (Ders 10: fonksiyon da bir değerdir). Dükkân hangi ürünün ne yaptığını bilmek zorunda değil; urun.etki(oyuncu) demesi yeter. Yeni bir ürün eklemek için yalnız diziye bir obje eklersin.
  • forEach((urun, i) => …) ikinci parametrede sıra numarasını da verir (Ders 11). Ekranda 1'den başlamak için i + 1 yazdık; seçimi diziye geri çevirirken de secim - 1.
  • secimIste("Ne alacaksın? ", 0, URUNLER.length): sınırlar dizinin uzunluğundan geliyor. Dördüncü bir ürün eklersen menü kendiliğinden 0–4 olur.
  • Dükkân da kendi küçük döngüsüne sahip: oyuncu 0 diyene kadar alışveriş sürer.

index.js'te savas import'unun altına import { dukkan } from "./dukkan.js" satırını ekle ve 2'nin dalındaki console.log("Dükkân yakında.") satırını await dukkan(oyuncu) yap. Sonra dükkânı dene; parası yetmeyen bir eşya da seç:

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 2 --- Dükkân (altının: 50) ---1) Can İksiri: 30 altın (+1 iksir)2) Uzun Kılıç: 90 altın (+4 güç)3) Yakut Kristali: 120 altın (+25 en yüksek can)0) Dükkândan çıkNe alacaksın? 1Can İksiri aldın. Kalan altın: 20 --- Dükkân (altının: 20) ---1) Can İksiri: 30 altın (+1 iksir)2) Uzun Kılıç: 90 altın (+4 güç)3) Yakut Kristali: 120 altın (+25 en yüksek can)0) Dükkândan çıkNe alacaksın? 2Altının yetmiyor: 70 altın eksik. --- Dükkân (altının: 20) ---1) Can İksiri: 30 altın (+1 iksir)2) Uzun Kılıç: 90 altın (+4 güç)3) Yakut Kristali: 120 altın (+25 en yüksek can)0) Dükkândan çıkNe alacaksın? 0 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Ahri | Seviye 1 | XP 0/50Can 100/100 | Güç 12 | Altın 20 | İksir 2 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

Ara sürüm 5 hazır: savaş, dükkân ve durum çalışıyor. Eksik tek şey kayıt.

Adım 7: kaydetmek ve yüklemek

Oyunu kapatınca bütün değişkenler silinir; oyuncu her açılışta 1. seviyeden başlar. Bir bilgiyi programdan uzun yaşatmanın en sade yolu onu bir dosyaya yazmak.

Neyi kaydediyoruz?

Ders 12'deki JSON.stringify bir objeyi metne çevirir. Bir sınıftan new ile üretilmiş objeyi (Ders 16'daki adıyla bir örneği) çevirince ne olduğuna bak:

deneme.js
class Oyuncu {    constructor(isim) {        this.isim = isim        this.hp = 100    }     get canliMi() {        return this.hp > 0    }     durum() {        return `${this.isim}: ${this.hp} can`    }} const ahri = new Oyuncu("Ahri")console.log(JSON.stringify(ahri))console.log(JSON.stringify(ahri, null, 2))
Çıktı
{"isim":"Ahri","hp":100}{  "isim": "Ahri",  "hp": 100}

Metne yalnız objenin kendi alanları girdi: isim ve hp. canliMi getter'ı ve durum metodu sınıfta durur, objenin içinde değil; dosyaya da girmez. Zaten girmeleri gerekmez: dosyada veri durur, davranış kodda. Üçüncü argüman 2, metni her düzeyde 2 boşluk içeri alarak okunaklı yazar.

Dosyaya yazmak ve okumak

Dosya işleri için Node'un node:fs/promises modülü var (fs: file system, dosya sistemi). Bize iki fonksiyonu yetiyor:

  • writeFile(dosya, metin, "utf8"): metni dosyaya yazar. Dosya yoksa oluşturur, varsa üzerine yazar.
  • readFile(dosya, "utf8"): dosyanın içini metin olarak okur.

İkisi de Promise döndürür, await ile beklenir. "utf8" metnin kodlaması, yani harflerin dosyaya hangi kuralla yazılacağı; Türkçe harfler de bu kuralla bozulmadan yazılıp okunur. readFile'a kodlamayı vermezsen metin yerine ham baytlar alırsın. Dosya sistemini Ders 29'da ayrıntılı göreceksin. kayit.js adında yeni bir dosya oluştur:

kayit.js
import { readFile, writeFile } from "node:fs/promises"import { Oyuncu } from "./oyuncu.js" const DOSYA = "kayit.json" export async function kaydet(oyuncu) {    try {        const metin = JSON.stringify(oyuncu, null, 2)        await writeFile(DOSYA, metin, "utf8")        console.log(`Oyun kaydedildi: ${DOSYA}`)    } catch (hata) {        console.log(`Kaydedilemedi: ${hata.message}`)    }} export async function yukle() {    let metin    try {        metin = await readFile(DOSYA, "utf8")    } catch (hata) {        if (hata.code === "ENOENT") {            return null        }        throw hata    }     try {        const veri = JSON.parse(metin)        if (!veri.isim) {            throw new Error("kayıtta isim yok")        }        return new Oyuncu(veri.isim, veri)    } catch (hata) {        console.log(`Kayıt dosyası bozuk (${hata.message}). Yeni oyun başlıyor.`)        return null    }}
  • kaydet yazma hatasını yakalar (disk dolu, dosyaya erişim izni yok gibi). Kayıt başarısız olsa bile oyun çökmez, mesaj yazar ve oyuncu oynamaya devam eder.
  • yukle iki ayrı try kullanıyor, çünkü iki ayrı şey ters gidebilir: dosya hiç olmayabilir, ya da var olup bozuk olabilir.
  • Dosya yoksa Node'un hatasının code alanında "ENOENT" yazar. Bu bizim için hata değil, "kayıt yok, yeni oyun" demek; null döndürüyoruz. Başka bir hatayı ise ne yapacağımızı bilmediğimiz için yeniden fırlatıyoruz (Ders 13: yalnız elinden bir şey geleni yakala).
  • JSON.parse metin bozuksa SyntaxError fırlatır. İçinde isim olmayan bir kayıt da işe yaramaz; onu kendi hatamızla (throw new Error) aynı yola sokuyoruz.
  • Sondaki new Oyuncu(veri.isim, veri) şimdilik yarım iş görür: Oyuncu'nun yapıcısı ikinci bir argüman beklemiyor. Kayıttan yalnız adı alır, öteki değerleri görmezden gelir ve 1. seviyeden yeni bir oyuncu kurar. Nedenini ve çözümünü hemen görüyoruz.

Düz obje sınıf değildir

JSON.parse sana bir obje verir, ama bu bir Oyuncu değil: verisi tamam, metotları yok.

Sık hataTypeError: veri.durum is not a function

Ne zaman çıkar? Dosyadan okuyup JSON.parse ile çevirdiğin objenin metodunu çağırınca.

deneme.js
const metin = '{ "isim": "Ahri", "hp": 86 }'const veri = JSON.parse(metin)console.log(veri.isim)console.log(veri.durum())
Hata çıktısı
Ahrifile:///C:/kod/metin-rpg/deneme.js:4console.log(veri.durum())                 ^ TypeError: veri.durum is not a function    at file:///C:/kod/metin-rpg/deneme.js:4:18    ... Node.js v24.20.0

veri.isim okundu, çünkü veri dosyadaydı. durum ise sınıfta yazılıydı, dosyada değil.

Nasıl düzelir? Okuduğun veriyle gerçek bir Oyuncu kur: new Oyuncu(veri.isim, veri). Bunun için yapıcının kayıttan gelen değerleri kabul etmesi gerekiyor (aşağıda).

oyuncu.js'te yalnız yapıcıyı (constructor) değiştir; sınıfın geri kalanı aynı:

oyuncu.js
export class Oyuncu {    constructor(isim, kayit = {}) {        this.isim = isim        this.seviye = kayit.seviye ?? 1        this.xp = kayit.xp ?? 0        this.maxHp = kayit.maxHp ?? 100        this.hp = kayit.hp ?? this.maxHp        this.guc = kayit.guc ?? 12        this.gold = kayit.gold ?? 50        this.iksir = kayit.iksir ?? 1    }
  • kayit = {} varsayılan parametre (Ders 10): yeni oyunda new Oyuncu("Ahri") yazınca kayit boş bir obje olur.
  • kayit.seviye ?? 1 (Ders 12): kayıtta seviye varsa onu, yoksa (undefined) 1'i kullanır. || değil ?? kullandık, çünkü 0 geçerli bir değer: xp ya da gold sıfır olabilir ve sıfır yanlış sayılıp varsayılana dönmemeli.
  • this.hp = kayit.hp ?? this.maxHp: canı yazılmamış bir kayıt, en yüksek canla başlar. Bu satır bu yüzden maxHp satırının altında: this.maxHp önce dolmalı ki burada okunabilsin.

Bu yapının güzel bir yanı daha var: ileride oyuncuya yeni bir alan eklediğinde eski kayıt dosyaları bozulmaz; eksik alan varsayılan değeri alır.

Oyuncu objesi isim, hp, gold, iksir… + durum(), hasarAl()… metotlarıyla birlikte kaydet JSON.stringify + writeFile kayit.json { "isim": "Ahri", "hp": 86, … } yükle readFile + JSON.parse düz obje veri var, metot yok metotlar geri gelir: new Oyuncu(veri.isim, veri)
Şekil 17.3 Kaydetme ve yükleme döngüsü. Dosyaya yalnız veri gider; metotlar, yüklerken veriden yeni bir Oyuncu kurulunca geri gelir.

Son olarak index.js: açılışta kaydı yüklemeyi dene, kayıt yoksa adı sor; menüde 4 kaydetsin. Bu, index.js'in son hâline çok yakın:

index.js
import { Oyuncu } from "./oyuncu.js"import { sor, secimIste, girdiyiKapat } from "./girdi.js"import { savas } from "./savas.js"import { dukkan } from "./dukkan.js"import { kaydet, yukle } from "./kayit.js" console.log("=== Sihirdar Vadisi: Metin RPG ===") let oyuncu = await yukle()if (oyuncu === null) {    const isim = await sor("Kahramanının adı ne? ")    oyuncu = new Oyuncu(isim || "İsimsiz")    console.log(`Hoş geldin, ${oyuncu.isim}!`)} else {    console.log(`Kayıt bulundu. Tekrar hoş geldin, ${oyuncu.isim}!`)} while (true) {    console.log("\n1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) Çık")    const secim = await secimIste("Seçimin: ", 1, 5)    if (secim === 5) {        break    } else if (secim === 1) {        await savas(oyuncu)    } else if (secim === 2) {        await dukkan(oyuncu)    } else if (secim === 3) {        console.log(oyuncu.durum())    } else if (secim === 4) {        await kaydet(oyuncu)    }} console.log("Görüşürüz!")girdiyiKapat()

oyuncu artık let: ya kayıttan ya da yeni oluşturularak dolacak. Bir savaş yapıp kaydet ve çık:

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 1 Karşına bir Minyon çıktı! (Can 30, Güç 6)1) Saldır  2) İksir iç  3) KaçSeçimin: 1Minyon 10 hasar aldı. Kalan canı: 20Minyon sana 5 hasar verdi. Canın: 95/1001) Saldır  2) İksir iç  3) KaçSeçimin: 1Minyon 15 hasar aldı. Kalan canı: 5Minyon sana 9 hasar verdi. Canın: 86/1001) Saldır  2) İksir iç  3) KaçSeçimin: 1Minyon 12 hasar aldı. Kalan canı: 0Minyon yenildi! +15 altın, +20 XP 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 4Oyun kaydedildi: kayit.json 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

VS Code'un dosya listesinde kayit.json belirdi. Aç ve içine bak:

kayit.json
{  "isim": "Ahri",  "seviye": 1,  "xp": 20,  "maxHp": 100,  "hp": 86,  "guc": 12,  "gold": 65,  "iksir": 1}

Oyunu yeniden başlat. Bu kez ad sorulmaz; Ahri kaldığı yerden devam eder:

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Kayıt bulundu. Tekrar hoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Ahri | Seviye 1 | XP 20/50Can 86/100 | Güç 12 | Altın 65 | İksir 1 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

Ara sürüm 6 hazır: oyun kaydediyor ve yüklüyor.

Kayıt yoksa, kayıt bozuksa

yukle'deki try/catch'ler neden gerekli? Onlarsız ne olduğunu gör. Önce dosya yokken okumaya çalışan çıplak bir kod:

Sık hataError: ENOENT: no such file or directory

Ne zaman çıkar? Var olmayan bir dosyayı okumaya çalışınca. ENOENT (error: no entry), "böyle bir dosya ya da klasör yok" demek; Ders 14'te npm'in mesajında görmüştün. Kendin görmek için önce VS Code'da kayit.json'un adını kayit-yedek.json yap (dosyaya sağ tıkla, Rename); deneme bitince eski adına döndür, birazdan ona yine ihtiyacın olacak. Sonra deneme.js'in içini sil ve şunu yaz:

deneme.js
import { readFile } from "node:fs/promises" const metin = await readFile("kayit.json", "utf8")console.log(metin)
Terminal — PowerShell
PS C:\kod\metin-rpg> node deneme.jsnode:internal/fs/promises:1345  return new FileHandle(await PromisePrototypeThen(                        ^ Error: ENOENT: no such file or directory, open 'C:\kod\metin-rpg\kayit.json'    at async open (node:internal/fs/promises:1345:25)    at async readFile (node:internal/fs/promises:1996:14)    at async file:///C:/kod/metin-rpg/deneme.js:3:15 {  errno: -4058,  code: 'ENOENT',  syscall: 'open',  path: 'C:\\kod\\metin-rpg\\kayit.json'} Node.js v24.20.0

Hatanın altındaki süslü parantezde hata objesinin alanları var (Ders 13'te name ve message'ı görmüştün): code: 'ENOENT' hangi tür hata olduğunu, path hangi dosyanın arandığını söylüyor. yukle tam olarak hata.code'a bakarak karar veriyor.

Nasıl düzelir? Dosyanın olmaması beklenen bir durumsa try/catch ile yakala ve code alanına göre karar ver. Bizim oyunda bunun anlamı "yeni oyun".

Dosya var ama bozuksa ne olur? Kayıt yarıda kesilmiş olabilir ya da biri onu elle bozmuş olabilir:

Sık hataSyntaxError: Expected double-quoted property name in JSON

Ne zaman çıkar? JSON.parse'a JSON kurallarına uymayan bir metin verince. Buradaki metin yarıda kesilmiş:

deneme.js
const metin = '{ "isim": "Ahri", "seviye": 3,'const veri = JSON.parse(metin)console.log(veri)
Hata çıktısı
<anonymous_script>:1{ "isim": "Ahri", "seviye": 3, SyntaxError: Expected double-quoted property name in JSON at position 30 (line 1 column 31)    at JSON.parse (<anonymous>)    ... Node.js v24.20.0

Mesaj, virgülden sonra çift tırnaklı yeni bir anahtar beklediğini ama metnin bittiğini söylüyor. position saymaya 0'dan, column 1'den başlar; position 30 ile column 31 aynı yeri, metnin bittiği yeri gösteriyor. Bozulmanın türüne göre mesaj değişir: boş bir dosya Unexpected end of JSON input verir, tırnaksız bir anahtar Expected property name or '}' verir.

Nasıl düzelir? Bozuk metni düzeltmek programın işi değil; yapabileceği en iyi şey çökmemek ve durumu söylemek. yukle bu yüzden JSON.parse'ı try içinde çağırıyor. Kendin dene: kayit.json'u aç, "hp" satırının sonundaki virgülü sil, kaydet ve oyunu başlat:

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Kayıt dosyası bozuk (Expected ',' or '}' after property value in JSON at position 76 (line 7 column 3)). Yeni oyun başlıyor.Kahramanının adı ne? LuxHoş geldin, Lux! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

Oyun çökmedi; mesajı yazıp yeni oyuna geçti. Uzun satırın sonundaki (line 7 column 3) "7. satır, 3. sütun" demek. position sayısı dosyandaki değerlere göre değişebilir, satır ve sütun aynı çıkar.

Eksik virgül ise 6. satırın sonunda. JSON okuyucusu canın değerinden sonra virgül ya da } bekliyordu; yanlışlığı ancak 7. satırdaki "guc"'u görünce anladı. Hata mesajının gösterdiği yer bazen asıl kusurun bir adım ötesidir; orada bir şey bulamazsan bir önceki satıra bak.

Deneme bitince bozuk kayit.json'u sil (VS Code'da dosyaya sağ tıkla, Delete). Bu kayıtla işimiz bitti; oyun bir sonraki kaydedişte yenisini yazar.

Adım 8: oyunu otomatik denemek

Her değişiklikten sonra oyunu baştan elle oynamak yorucu. Girdileri önceden hazırlayıp programa toplu verirsen saniyeler içinde dener, çıktıyı okursun. Bunun yolu boru.

PowerShell'de virgülle ayrılmış metinler bir liste olur; | bu listedeki her metni bir satır olarak node'a yollar. Ad olarak Ahri, sonra 3 (Durum), sonra 5 (Çık) verelim.

Bu deneme yeni bir oyunla başlamalı, yani proje klasöründe kayit.json olmamalı. Bozuk kaydı Adım 7'de sildin; sonradan yeniden kaydettiysen Remove-Item kayit.json ile sil. Kayıt dursaydı oyun ad sormadan ondan başlardı ve bütün girdiler bir soru kayardı. Şimdi dene:

Terminal — PowerShell
PS C:\kod\metin-rpg> "Ahri", "3", "5" | node index.js=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? Ahri35Hoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: Warning: Detected unsettled top-level await at file:///C:/kod/metin-rpg/index.js:20    const secim = await secimIste("Seçimin: ", 1, 5)                  ^

Bu beklediğimiz şey değil. Üç satırın üçü de en başta ekrana basıldı (readline okuduğu her şeyi ekrana da yazar), Durum hiç yazılmadı ve program bir uyarıyla kapandı. Neden?

Klavyeden oynarken her satırı Enter'la tek tek gönderirsin ve question o sırada hep bekliyordur. Borudan ise üç satır aynı anda gelir. question yalnız beklediği satırı (Ahri) alır.

Arkasından gelen 3 ve 5'i bekleyen kimse yoktur; readline onları bir kenara atar. Program ikinci soruda cevap beklemeye başlar, ama girdi çoktan bitmiştir. Node bekleyecek başka bir şey kalmadığını görünce programı kapatır ve uyarı verir.

question ile borudan aynı anda: Ahri 3 5 question tek satır bekler Ahri alındı 3 5 kayboldu satır sırası ile borudan aynı anda: Ahri 3 5 satır sırası Ahri 3 5 1. soru: Ahri 2. soru: 3 3. soru: 5 Her soru sıradaki satırı alır; bekleyen satır kaybolmaz.
Şekil 17.4 question yalnız beklediği satırı alır. Satır sırası ise gelen her satırı sırayla saklar.
Sık hataWarning: Detected unsettled top-level await

Ne zaman çıkar? En üst düzeydeki bir await'in beklediği Promise hiç sonuçlanmadan program bitince. Burada question bir cevap bekliyordu ama girdi bitmişti; cevap asla gelmeyecekti.

Uyarıdaki index.js:20 programın nerede takılı kaldığını gösterir: menüdeki secimIste satırı.

Her program biterken Windows'a bir sayı bırakır: çıkış kodu (exit code). 0 "sorunsuz bitti" demek, başka her sayı "bir şey ters gitti". Node böyle yarım kalan bir programa 13 verir. PowerShell son programın çıkış kodunu $LASTEXITCODE adlı değişkende tutar; borulu komuttan hemen sonra onu yazarsan görürsün:

Terminal — PowerShell
PS C:\kod\metin-rpg> $LASTEXITCODE13

Nasıl düzelir? Girdiyi satır kaybetmeyecek biçimde oku ve girdi bittiğinde bunu programa haber ver. İkisini de aşağıdaki yeni sor yapıyor.

Çözüm, gelen satırları bir sıraya koymak ve her soruda sıradaki satırı almak. readline bunu hazır veriyor. girdi.js'in üst kısmını ve sor fonksiyonunu değiştir; secimeCevir aynı kalıyor, secimIste'ye üç satır ekleniyor:

girdi.js
import { createInterface } from "node:readline/promises"import { stdin, stdout } from "node:process" const rl = createInterface({ input: stdin })const satirlar = rl[Symbol.asyncIterator]() export async function sor(soru) {    stdout.write(soru)    const sonuc = await satirlar.next()    if (sonuc.done) {        console.log()        return null    }    if (!stdin.isTTY) {        console.log(sonuc.value)    }    return sonuc.value.trim()} export function secimeCevir(metin, enAz, enCok) {    const sayi = Number(metin)    if (metin === "" || !Number.isInteger(sayi)) {        throw new Error(`"${metin}" bir tam sayı değil`)    }    if (sayi < enAz || sayi > enCok) {        throw new Error(`${enAz} ile ${enCok} arasında bir sayı yaz`)    }    return sayi} export async function secimIste(soru, enAz, enCok) {    while (true) {        const cevap = await sor(soru)        if (cevap === null) {            return null        }        try {            return secimeCevir(cevap, enAz, enCok)        } catch (hata) {            console.log(`Geçersiz seçim: ${hata.message}.`)        }    }} export function girdiyiKapat() {    rl.close()}
  • createInterface({ input: stdin }): bu kez output vermedik. readline artık ekrana hiçbir şey yazmıyor; soruyu biz yazıyoruz. Klavyeden yazdıklarını terminal yine kendisi gösterir.
  • rl[Symbol.asyncIterator](): gelen satırları sırayla, hiçbirini kaybetmeden veren bir satır sırası kurar. Köşeli parantezli bu yazım, JavaScript'in "sırayla ve bekleyerek değer veren şeyler" için kullandığı özel bir anahtar. Zamanla parça parça gelen veriyi sırayla bekleme fikri Ders 27'de yeniden karşına çıkacak; şimdilik kalıp olarak yaz.
  • stdout.write(soru): console.log gibi yazar ama sonuna yeni satır koymaz; cevap sorunun yanına gelsin diye.
  • await satirlar.next(): sıradaki satırı bekler, satır hazırsa hemen verir. Sonuç bir obje: { value: "3", done: false }. Girdi bittiyse done true olur; o zaman boş bir console.log() ile sorunun satırını bitirip null döndürüyoruz.
  • stdin.isTTY: girdi klavyeden, yani bir terminalden geliyorsa true (TTY, eski dilde terminal demek), borudan geliyorsa undefined. Klavyede yazdığını terminal zaten gösterir. Borudan gelen satır ise ekrana düşmez; onu biz yazdırıyoruz ki çıktı elle oynanmış gibi okunsun.
  • secimIste de null görünce null döndürüyor: "cevap gelmeyecek".

null yeni bir anlaşma: artık secimIste "cevap yok" diyebiliyor. Onu kullanan üç yer de bunu dinlemeli. Dinlemezse menü null ile dönmeye devam eder ve saniyede binlerce kez ekrana basılır; Ctrl+C ile durdurman gerekir. Üç küçük değişiklik:

index.js
    const secim = await secimIste("Seçimin: ", 1, 5)    if (secim === null || secim === 5) {        break
savas.js
        const secim = await secimIste("1) Saldır  2) İksir iç  3) Kaç\nSeçimin: ", 1, 3)        if (secim === null) {            return        }
dukkan.js
        const secim = await secimIste("Ne alacaksın? ", 0, URUNLER.length)        if (secim === null || secim === 0) {            return

savas.js'teki üç satır yeni; index.js ve dukkan.js'te yalnız koşul uzadı. Aynı denemeyi tekrarla:

Terminal — PowerShell
PS C:\kod\metin-rpg> "Ahri", "3", "5" | node index.js=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Ahri | Seviye 1 | XP 0/50Can 100/100 | Güç 12 | Altın 50 | İksir 1 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

Her girdi doğru soruya gitti ve çıktı, elle oynanmış bir oyun gibi okunuyor. Girdiler erken biterse de program artık takılmıyor:

Terminal — PowerShell
PS C:\kod\metin-rpg> "Ahri", "3" | node index.js=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Ahri | Seviye 1 | XP 0/50Can 100/100 | Güç 12 | Altın 50 | İksir 1 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin:Görüşürüz!

Bir değişiklik daha oldu: klavyeyle oynarken Ctrl+C artık AbortError yazmıyor, oyunu anında ve sessizce kapatıyor. Kaydetmediğin ilerleme gider; düzgün çıkış yine 5. Ara sürüm 7, yani oyunun son hâli hazır.

Modüllere bölmek: geriye bakış

Adım 8'de girdi okumanın yolunu baştan değiştirdik. Değişiklik neredeyse tamamen girdi.js'in içinde kaldı; öteki dosyalarda yalnız null denetimi eklendi. Oyuncu sınıfına, kayıt koduna, düşmanlara ve ürünlere hiç dokunmadık. Kodu modüllere bölmenin karşılığını tam burada alırsın.

Bu projeden yanına alacağın kurallar:

  • Bir dosya, bir iş. Dosyanın adı işini söylesin. "Bu kod nereye gider?" sorusunun cevabı tek olsun.
  • Oklar tek yönlü. index.js herkesi kullanır, onu kimse kullanmaz; bir modül de kendisini kullanan modülü geri import etmez. İki dosyanın birbirini import etmesi (döngüsel import) kafa karıştırır, kaçın.
  • Yalnız gerekeni dışa aktar. DUSMANLAR, zar ve URUNLER kendi dosyalarının içinde kaldı. Kimsenin bilmediği bir şeyi değiştirmek kolaydır.
  • Ekrana yazmak ile hesaplamak ayrı olsun. durum() metin döndürüyor, secimeCevir sayı ya da hata döndürüyor; ikisi de bu yüzden tek başına denenebildi.

Projenin son hâli, VS Code'da:

index.js — metin-rpg — Visual Studio Code
index.js
import { Oyuncu } from "./oyuncu.js"import { sor, secimIste, girdiyiKapat } from "./girdi.js"import { savas } from "./savas.js"import { dukkan } from "./dukkan.js"import { kaydet, yukle } from "./kayit.js" console.log("=== Sihirdar Vadisi: Metin RPG ===") let oyuncu = await yukle()if (oyuncu === null) {    const isim = await sor("Kahramanının adı ne? ")    oyuncu = new Oyuncu(isim || "İsimsiz")    console.log(`Hoş geldin, ${oyuncu.isim}!`)} else {    console.log(`Kayıt bulundu. Tekrar hoş geldin, ${oyuncu.isim}!`)} while (true) {    console.log("\n1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) Çık")    const secim = await secimIste("Seçimin: ", 1, 5)    if (secim === null || secim === 5) {        break    } else if (secim === 1) {        await savas(oyuncu)    } else if (secim === 2) {        await dukkan(oyuncu)    } else if (secim === 3) {        console.log(oyuncu.durum())    } else if (secim === 4) {        await kaydet(oyuncu)    }} console.log("Görüşürüz!")girdiyiKapat()
PROBLEMSOUTPUTDEBUG CONSOLETERMINAL
PS C:\kod\metin-rpg> npm start > metin-rpg@1.0.0 start> node index.js === Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 4Oyun kaydedildi: kayit.json 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!
Ln 1, Col 1Spaces: 4UTF-8JavaScript

npm start, package.json'daki start betiğini çalıştırır; çıktının başındaki iki satırı npm yazar. Bozuk kaydı Adım 7'de silmiştin; bu turda 4 ile yeniden kaydettik, kayit.json dosya listesine geri döndü.

README, .gitignore ve GitHub

Oyun bitti; şimdi onu başkalarına gösterilecek hâle getir. Önce deneme dosyalarını (deneme.js, savas-deneme.js) sil: VS Code'da dosyaya sağ tıkla, Delete. Proje deposunda yalnız oyunun kendisi kalsın.

README.md

GitHub bir deponun ana sayfasında README.md dosyasını gösterir (Ders 7). Bir işverenin ya da arkadaşının projeni anlamak için ilk okuyacağı şey o. Proje klasöründe README.md adında yeni bir dosya oluştur:

README.md
# Metin RPG Terminalde oynanan küçük bir rol yapma oyunu. Sihirdar Vadisi'nde canavarlarla savaş, altın topla, dükkândan eşya al, seviye atla ve ilerlemeni kaydet. ## Özellikler - Menülü oyun döngüsü: savaş, dükkân, durum, kaydet, çık- Rastgele düşmanlar ve tur tur savaş (saldır, iksir iç, kaç)- Altın, tecrübe puanı (XP) ve seviye atlama- Kaydetme ve yükleme (`kayit.json`)- Yanlış girdiye dayanıklı menüler ## Nasıl çalıştırılır? Gerekenler: [Node.js](https://nodejs.org) 24 ya da daha yeni bir sürüm. ```git clone https://github.com/denizyilmaz/metin-rpg.gitcd metin-rpgnpm start``` Proje hiçbir paket kullanmıyor; `npm install` gerekmez. Oyun ilerlemeni `kayit.json` dosyasına yazar; bu dosya Git'e eklenmez. ## Ekran çıktısı ```=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 1 Karşına bir Minyon çıktı! (Can 30, Güç 6)1) Saldır  2) İksir iç  3) KaçSeçimin: 1Minyon 11 hasar aldı. Kalan canı: 19``` ## Dosyalar | Dosya | Görevi ||---|---|| `index.js` | Açılış ve ana menü döngüsü || `girdi.js` | Klavyeden satır okuma, seçim doğrulama || `oyuncu.js` | `Oyuncu` sınıfı: can, altın, seviye, iksir || `savas.js` | Rastgele düşman ve savaş || `dukkan.js` | Eşya listesi ve satın alma || `kayit.js` | `kayit.json` dosyasına yazma ve okuma | ## Neler öğrendim? Node.js'te kullanıcıdan girdi almak (`node:readline/promises`), kodu modüllere bölmek, sınıf kullanmak, dosyaya JSON kaydetmek ve hatalı girdiyi yakalamak.

İyi bir README beş soruya cevap verir: bu ne, ne yapıyor, nasıl çalıştırılır, nasıl görünüyor, içinde ne var. Son bölüm, yani bu projeden ne öğrendiğin, portföyde çok işe yarar: okuyan, neyi bildiğini oradan anlar. Adresteki denizyilmaz yerine, Ders 7'deki gibi kendi GitHub kullanıcı adını yaz.

kayit.json Git'e girmeli mi?

git status ile Git'in neyi gördüğüne bak:

Terminal — PowerShell
PS C:\kod\metin-rpg> git statusOn branch master No commits yet Untracked files:  (use "git add <file>..." to include in what will be committed)        README.md        dukkan.js        girdi.js        index.js        kayit.js        kayit.json        oyuncu.js        package.json        savas.js nothing added to commit but untracked files present (use "git add" to track)

kayit.json da listede. Onu depoya koymalı mısın? İki tarafı da düşün:

  • Koyma: Kayıt senin kişisel oyun durumun; her kaydedişte değişir ve commit geçmişini gereksiz değişikliklerle doldurur. Depoyu indiren biri de oyuna senin kaydınla, senin kahramanınla başlar.
  • Koy: Okuyana örnek bir kayıt göstermek isteyebilirsin. Ama bunun için asıl kaydı değil, ayrı bir ornek-kayit.json dosyasını koymak daha doğru.

Karar: kayit.json depoya girmesin. Proje klasöründe .gitignore adında yeni bir dosya oluştur (Ders 7):

.gitignore
kayit.jsonnode_modules/

node_modules/ satırı şimdilik gereksiz, çünkü hiç paket kurmadık; ama ileride kurarsan hazır olsun (Ders 14). Yeniden bak:

Terminal — PowerShell
PS C:\kod\metin-rpg> git statusOn branch master No commits yet Untracked files:  (use "git add <file>..." to include in what will be committed)        .gitignore        README.md        dukkan.js        girdi.js        index.js        kayit.js        oyuncu.js        package.json        savas.js nothing added to commit but untracked files present (use "git add" to track)

kayit.json listeden çıktı, yerine .gitignore geldi. Artık ilk commit'i alabilirsin:

Terminal — PowerShell
PS C:\kod\metin-rpg> git add .warning: in the working copy of 'package.json', LF will be replaced by CRLF the next time Git touches itPS C:\kod\metin-rpg> git commit -m "Metin RPG: ilk çalışan sürüm"[master (root-commit) 288b3a4] Metin RPG: ilk çalışan sürüm 9 files changed, 351 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 dukkan.js create mode 100644 girdi.js create mode 100644 index.js create mode 100644 kayit.js create mode 100644 oyuncu.js create mode 100644 package.json create mode 100644 savas.js

warning satırını Ders 14'ten tanıyorsun: hata değil, bilgi. package.json'u npm, Linux'taki gibi LF satır sonlarıyla yazdı; VS Code'da oluşturduğun dosyalar ise Windows'un CRLF'siyle yazıldı. Git aradaki farkı kendisi idare eder ve yalnız haber verir; Git'in kurulum ayarına göre bu satırı hiç görmeyebilirsin de.

Commit kimliği (288b3a4) sende farklı olacak, satır sayısı da yazdığın koda göre biraz değişebilir.

GitHub'a göndermek

Ders 7'de yaptığın gibi:

  1. GitHub'da sağ üstteki + simgesine tıkla ve New repository'yi seç.
  2. Repository name kutusuna metin-rpg yaz, istersen kısa bir açıklama ekle ve deponun herkese açık (Public) olmasını seç.
  3. README, .gitignore ya da lisans ekleme seçeneklerinin hiçbirini işaretleme; bunlar sende zaten var. GitHub'ın belgesi de var olan bir projeyi gönderirken yeni depoyu boş açmanı söyler, yoksa ilk push'ta hata alırsın.
  4. Create repository'ye tıkla. Açılan sayfada deponun adresini kopyala.
  5. C:\kod\metin-rpg klasöründe aşağıdaki üç komutu çalıştır. Adresteki denizyilmaz yerine kendi kullanıcı adın gelecek.
PowerShell
git remote add origin https://github.com/denizyilmaz/metin-rpg.gitgit branch -M maingit push -u origin main
  • git remote add origin …: depona origin adıyla bir uzak depo adresi ekler.
  • git branch -M main: dalın adını master'dan main'e çevirir; GitHub'ın varsayılan dal adı main.
  • git push -u origin main: commit'lerini GitHub'a gönderir. -u yerel main dalını GitHub'dakine bağlar; sonraki gönderimlerde yalnız git push yazman yeter. İlk gönderimde tarayıcıda giriş penceresi açılabilir.

Adresin doğru eklendiğini git remote -v ile görebilirsin:

Terminal — PowerShell
PS C:\kod\metin-rpg> git remote -vorigin  https://github.com/denizyilmaz/metin-rpg.git (fetch)origin  https://github.com/denizyilmaz/metin-rpg.git (push)

Gönderim bitince depo sayfanı yenile: dosyaların ve onların altında README'n görünür. Bu bağlantı artık portföyünün bir parçası.

Genişletme fikirleri

Oyun çalışıyor, ama bir oyun hiç "bitmez". Kendi başına ekleyebileceğin fikirler; ilk beşi aşağıdaki görevlerde:

  • Kayda tarih: son kaydın ne zaman alındığını göster.
  • Otomatik deneme: npm run deneme ile hazır girdilerle oyunu oynat.
  • Boss: yalnız yüksek seviyede çıkan güçlü bir düşman.
  • Zorluk seviyesi: kolay, normal, zor.
  • Envanter: satın alınan eşyaları listele.
  • Kritik vuruş: her saldırıda yüzde 20 ihtimalle iki kat hasar (Math.random() < 0.2).
  • Mana ve yetenek: 20 mana harcayan güçlü bir saldırı; mana her tur biraz dolsun.
  • Kayıt yuvaları: kayit-1.json, kayit-2.json; açılışta hangi yuvayla oynanacağını sor.
  • Yeni oyun seçeneği: kayıt varken "devam mı, yeni oyun mu?" diye sor.
  • Renkli yazı: Node'la gelen node:util modülündeki styleText ile hasarı kırmızı, altını sarı yaz.
  • Skor tablosu: ulaşılan en yüksek seviyeleri ayrı bir JSON dosyasında tut.
  • Güvenli kayıt: önce geçici bir dosyaya yaz, sonra adını kayit.json yap; yazarken elektrik kesilirse eski kayıt bozulmasın.
  • Testler: Ders 53'te secimeCevir ve Oyuncu için otomatik testler yazacaksın.
  • TypeScript: Ders 39'dan sonra projeyi tiplerle yeniden yaz.

Görevler

Görevleri oyunun son hâli üzerinde yap; her biri öbürlerinden bağımsız. Her görevden sonra oyunu hem elle hem de boruyla dene, eski bir kayit.json ile de aç: yeni bir alan eklediğinde eski kayıtların bozulmaması da işin parçası.

Görev 1

Kayda tarih yaz

Oyun kaydedilirken kayda tarih ve saat de yazılsın. Açılışta kayıt bulunursa önce bir Son kayıt: satırı çıksın. Tarih alanı olmayan eski bir kayıtta Son kayıt: bilinmiyor yazsın.

Metin
=== Sihirdar Vadisi: Metin RPG ===Son kayıt: 11.10.2026 19:39:25Kayıt bulundu. Tekrar hoş geldin, Ahri!
İpucu 1

new Date() şu anki tarihi ve saati tutan hazır bir obje üretir; bunu ilk kez görüyorsun. new Date().toLocaleString("tr-TR") onu Türkiye'de alışık olduğun biçimde bir metne çevirir.

İpucu 2

oyuncu'yu olduğu gibi kaydetmek yerine yayma ile genişlet (Ders 12): { ...oyuncu, kayitTarihi: … }. Yeni alanı Oyuncu sınıfına eklemen gerekmez.

İpucu 3

Yüklerken veri.kayitTarihi ?? "bilinmiyor". Mesajı yukle içinde, new Oyuncu(...) satırından hemen önce yazdır.

Çözümü göster

kayit.js'te iki yer değişiyor:

kayit.js
export async function kaydet(oyuncu) {    try {        const veri = { ...oyuncu, kayitTarihi: new Date().toLocaleString("tr-TR") }        const metin = JSON.stringify(veri, null, 2)        await writeFile(DOSYA, metin, "utf8")        console.log(`Oyun kaydedildi: ${DOSYA}`)    } catch (hata) {        console.log(`Kaydedilemedi: ${hata.message}`)    }}
kayit.js
    try {        const veri = JSON.parse(metin)        if (!veri.isim) {            throw new Error("kayıtta isim yok")        }        console.log(`Son kayıt: ${veri.kayitTarihi ?? "bilinmiyor"}`)        return new Oyuncu(veri.isim, veri)

Kaydettikten sonra kayit.json'un sonunda yeni alan görünür:

kayit.json
{  "isim": "Ahri",  "seviye": 1,  "xp": 0,  "maxHp": 100,  "hp": 100,  "guc": 12,  "gold": 50,  "iksir": 1,  "kayitTarihi": "11.10.2026 19:39:25"}

Oyunu kapatıp node index.js ile yeniden açınca başlığın hemen altında tarih görünür (tarih ve saat sende farklı olacak):

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Son kayıt: 11.10.2026 19:39:25Kayıt bulundu. Tekrar hoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Ahri | Seviye 1 | XP 0/50Can 100/100 | Güç 12 | Altın 50 | İksir 1 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

Sınır denemesi: kayit.json'un içini elle {"isim":"Ahri","seviye":2} yap ve oyunu aç. Başlığın altında Son kayıt: bilinmiyor çıkmalı; eksik alanlar da yapıcıdaki ?? sayesinde varsayılan değerlerini alır. toLocaleString bilgisayarın saat dilimine göre yazar; başka bir ülkedeki bilgisayarda aynı kayıt başka bir saat gösterir.

Görev 2

Otomatik deneme betiği

npm run deneme yazınca oyun, deneme-girdi.txt dosyasındaki girdilerle kendi kendine oynasın. Girdiler sırasıyla: ad olarak Ahri, Durum, Dükkân, iksir al, dükkândan çık, yine Durum, Çık. Son Durum'da altın 20, iksir 2 olmalı.

İpucu 1

Her girdi dosyada ayrı bir satır: ilk satır Ahri, ikincisi 3 ve böyle sürer.

İpucu 2

npm betikleri Windows'ta PowerShell'de değil cmd'de çalışır. cmd'de < işareti bir dosyayı programın standart girdisine verir: node index.js < deneme-girdi.txt.

İpucu 3

package.json'daki scripts içine "deneme": "node index.js < deneme-girdi.txt" satırını ekle. Bir önceki satırın sonuna virgül koymayı unutma; JSON'da son elemandan sonra virgül olmaz, aradakilerden sonra olur.

Çözümü göster
package.json
  "scripts": {    "start": "node index.js",    "deneme": "node index.js < deneme-girdi.txt"  },
deneme-girdi.txt
Ahri321035

Terminalde npm run deneme yazınca oyun kendi kendine oynar:

Terminal — PowerShell
PS C:\kod\metin-rpg> npm run deneme > metin-rpg@1.0.0 deneme> node index.js < deneme-girdi.txt === Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriHoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Ahri | Seviye 1 | XP 0/50Can 100/100 | Güç 12 | Altın 50 | İksir 1 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 2 --- Dükkân (altının: 50) ---1) Can İksiri: 30 altın (+1 iksir)2) Uzun Kılıç: 90 altın (+4 güç)3) Yakut Kristali: 120 altın (+25 en yüksek can)0) Dükkândan çıkNe alacaksın? 1Can İksiri aldın. Kalan altın: 20 --- Dükkân (altının: 20) ---1) Can İksiri: 30 altın (+1 iksir)2) Uzun Kılıç: 90 altın (+4 güç)3) Yakut Kristali: 120 altın (+25 en yüksek can)0) Dükkândan çıkNe alacaksın? 0 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Ahri | Seviye 1 | XP 0/50Can 100/100 | Güç 12 | Altın 20 | İksir 2 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

Savaşı deneme girdisine koymadık, çünkü kaç tur süreceği rastgele: fazla ya da eksik bir 1 sonraki girdileri kaydırır.

Sınır denemesi: klasörde bir kayit.json varken aynı komutu çalıştır. Oyun ad sormaz; Ahri satırı menüye gider:

Terminal — PowerShell
PS C:\kod\metin-rpg> npm run deneme > metin-rpg@1.0.0 deneme> node index.js < deneme-girdi.txt === Sihirdar Vadisi: Metin RPG ===Kayıt bulundu. Tekrar hoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: AhriGeçersiz seçim: "Ahri" bir tam sayı değil.Seçimin: 3Ahri | Seviye 1 | XP 0/50Can 100/100 | Güç 12 | Altın 50 | İksir 1...

Doğrulama sayesinde oyun çökmedi; geri kalan girdiler de yerine oturdu. Ama deneme artık yeni bir oyunu değil, senin kaydını deniyor: Durum'daki sayılar kayda göre değişir, kayıtta 30'dan az altın varsa iksir bile alınamaz. Otomatik denemeden önce kayit.json'u sil ya da kenara al.

Görev 3

Baron Nashor

Yeni bir düşman türü ekle: Baron Nashor (can 160, güç 20, 200 altın, 150 XP). Ama yalnız 3. seviyeye ulaşmış oyunculara çıksın; daha düşük seviyede asla görünmesin.

İpucu 1

Her düşmana bir enAzSeviye alanı ekle: dört eski düşmanınki 1, Baron'unki 3.

İpucu 2

Zar atmadan önce oyuncunun seviyesine uygun düşmanları süz (Ders 11'deki filter) ve zarı süzülmüş dizi üzerinde at.

İpucu 3

const uygunlar = DUSMANLAR.filter((d) => d.enAzSeviye <= oyuncu.seviye). < değil <=: tam 3. seviyedeki oyuncu Baron'la karşılaşabilmeli.

Çözümü göster

savas.js'te düşman listesi ve seçimin yapıldığı satırlar değişiyor:

savas.js
const DUSMANLAR = [    { isim: "Minyon", hp: 30, guc: 6, gold: 15, xp: 20, enAzSeviye: 1 },    { isim: "Alacakarga", hp: 40, guc: 8, gold: 20, xp: 25, enAzSeviye: 1 },    { isim: "Kurt", hp: 55, guc: 10, gold: 30, xp: 35, enAzSeviye: 1 },    { isim: "Gromp", hp: 70, guc: 12, gold: 40, xp: 45, enAzSeviye: 1 },    { isim: "Baron Nashor", hp: 160, guc: 20, gold: 200, xp: 150, enAzSeviye: 3 }]
savas.js
export async function savas(oyuncu) {    const uygunlar = DUSMANLAR.filter((d) => d.enAzSeviye <= oyuncu.seviye)    const sablon = uygunlar[zar(0, uygunlar.length - 1)]    const dusman = { ...sablon }

Seçim kuralını rastgelelik olmadan denemek için filter kısmını tek başına çalıştır. Sınır değerleri 2 ve 3:

deneme.js
const DUSMANLAR = [    { isim: "Minyon", enAzSeviye: 1 },    { isim: "Kurt", enAzSeviye: 1 },    { isim: "Baron Nashor", enAzSeviye: 3 }] function uygunDusmanlar(seviye) {    return DUSMANLAR.filter((d) => d.enAzSeviye <= seviye).map((d) => d.isim)} console.log(uygunDusmanlar(1))console.log(uygunDusmanlar(2))console.log(uygunDusmanlar(3))
Çıktı
[ 'Minyon', 'Kurt' ][ 'Minyon', 'Kurt' ][ 'Minyon', 'Kurt', 'Baron Nashor' ]

Oyunun içinde denemek için kayit.json'daki seviye değerini elle 2 yapıp birkaç savaşa gir; Baron hiç çıkmamalı. Sonra 3 yap. Baron artık beş düşmandan biri olarak çıkabilir; ama ona karşı 3. seviyede kazanmak zor, iksirini hazır tut.

Görev 4

Zorluk seviyesi

Yeni oyunda zorluk sorulsun: Zorluk? 1) Kolay 2) Normal 3) Zor:. Kolayda düşmanların canı ve gücü yüzde 30 az, zorda yüzde 50 fazla olsun (en yakın tam sayıya yuvarla). Zorluk kayda girsin; zorluğu olmayan eski kayıtlar normal sayılsın.

Metin
Kahramanının adı ne? AhriZorluk? 1) Kolay  2) Normal  3) Zor: 3Hoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 1 Karşına bir Minyon çıktı! (Can 45, Güç 9)
İpucu 1

Zorluğu Oyuncu'ya alan olarak ekle: yapıcıda this.zorluk = kayit.zorluk ?? "normal". Bu alan neden kayda kendiliğinden girer, düşün.

İpucu 2

Çarpanları bir objede tut: { kolay: 0.7, normal: 1, zor: 1.5 }. Düşmanın kopyasını alırken canını ve gücünü çarp, Math.round ile yuvarla. Zorluğu sormak için yeni bir şey yazman gerekmez: secimIste(…, 1, 3).

İpucu 3

Kayıt elle bozulup bilinmeyen bir zorluk adı gelirse çarpan undefined olur ve hesap NaN verir. ?? 1 ile koru.

Çözümü göster

oyuncu.js'te yapıcıya bir satır:

oyuncu.js
        this.iksir = kayit.iksir ?? 1        this.zorluk = kayit.zorluk ?? "normal"    }

index.js'te yeni oyun açılırken zorluğu sor. ["kolay", "normal", "zor"][zorluk - 1] seçimi (1–3) dizinin sıra numarasına (0–2) çevirir. Girdi bittiyse zorluk null olur, dizi undefined verir ve ?? "normal" devreye girer:

index.js
if (oyuncu === null) {    const isim = await sor("Kahramanının adı ne? ")    oyuncu = new Oyuncu(isim || "İsimsiz")    const zorluk = await secimIste("Zorluk? 1) Kolay  2) Normal  3) Zor: ", 1, 3)    oyuncu.zorluk = ["kolay", "normal", "zor"][zorluk - 1] ?? "normal"    console.log(`Hoş geldin, ${oyuncu.isim}!`)

savas.js'te DUSMANLAR'ın hemen altına çarpanları ve kopyayı ayarlayan fonksiyonu ekle:

savas.js
const CARPANLAR = { kolay: 0.7, normal: 1, zor: 1.5 } function zorlukUygula(sablon, zorluk) {    const carpan = CARPANLAR[zorluk] ?? 1    return {        ...sablon,        hp: Math.round(sablon.hp * carpan),        guc: Math.round(sablon.guc * carpan)    }}

savas fonksiyonunda const dusman = { ...sablon } satırını const dusman = zorlukUygula(sablon, oyuncu.zorluk) yap. Kopya yine alınıyor; yayma artık zorlukUygula'nın içinde. Fonksiyonu sınır değerleriyle dene:

deneme.js
const CARPANLAR = { kolay: 0.7, normal: 1, zor: 1.5 } function zorlukUygula(sablon, zorluk) {    const carpan = CARPANLAR[zorluk] ?? 1    return {        ...sablon,        hp: Math.round(sablon.hp * carpan),        guc: Math.round(sablon.guc * carpan)    }} const kurt = { isim: "Kurt", hp: 55, guc: 10 }console.log(zorlukUygula(kurt, "kolay"))console.log(zorlukUygula(kurt, "normal"))console.log(zorlukUygula(kurt, "zor"))console.log(zorlukUygula(kurt, "efsane"))console.log(kurt)
Çıktı
{ isim: 'Kurt', hp: 39, guc: 7 }{ isim: 'Kurt', hp: 55, guc: 10 }{ isim: 'Kurt', hp: 83, guc: 15 }{ isim: 'Kurt', hp: 55, guc: 10 }{ isim: 'Kurt', hp: 55, guc: 10 }

55 * 0.7 38.5 eder ve 39'a, 55 * 1.5 82.5 eder ve 83'e yuvarlanır. Bilinmeyen "efsane" normal gibi davrandı ve son satır şablonun hiç değişmediğini gösteriyor. Oyunda, kayıt yokken node index.js ile yeni bir oyun açınca (düşman ve kaçış rastgele, sende başka çıkabilir):

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Kahramanının adı ne? AhriZorluk? 1) Kolay  2) Normal  3) Zor: 7Geçersiz seçim: 1 ile 3 arasında bir sayı yaz.Zorluk? 1) Kolay  2) Normal  3) Zor: 3Hoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 1 Karşına bir Minyon çıktı! (Can 45, Güç 9)1) Saldır  2) İksir iç  3) KaçSeçimin: 3Kaçmayı başardın. 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 5Görüşürüz!

Kaydedince kayit.json'da "zorluk": "zor" satırı belirir. Zorluk alanı olmayan eski bir kayıtla açtığında düşmanlar normal güçte çıkar.

Görev 5

Envanter

Dükkândan alınan kılıç ve kristaller bir envantere girsin. Durum ekranında üçüncü bir satır çıksın: Eşyalar: Uzun Kılıç x2, Yakut Kristali x1; hiç eşya yoksa Eşyalar: yok. Envanter kayda girsin, envanteri olmayan eski kayıtlar boş envanterle açılsın. İksirler envantere girmesin; onların sayacı zaten var.

İpucu 1

Oyuncu'ya this.envanter = kayit.envanter ?? [] ekle. Dükkândaki kılıcın ve kristalin etki fonksiyonlarında oyuncu.envanter.push(...).

İpucu 2

Aynı eşyaları saymak için reduce ile bir sayaç objesi kur (Ders 11): ["Uzun Kılıç", "Uzun Kılıç"] dizisinden { "Uzun Kılıç": 2 } çıksın.

İpucu 3

Sayaçtan metne: Object.entries(sayilar) (Ders 12) sana [["Uzun Kılıç", 2]] verir. map ile her çifti "Uzun Kılıç x2" yap. Son adım için yeni bir metot gerekiyor, ilk kez görüyorsun: parcalar.join(", ") dizinin elemanlarını aralarına ", " koyarak tek bir metinde birleştirir. Ders 9'daki split'in tersidir.

Çözümü göster

oyuncu.js'te yapıcıya bir satır, sınıfa bir metot; durum da üçüncü satırı ekliyor:

oyuncu.js
        this.iksir = kayit.iksir ?? 1        this.envanter = kayit.envanter ?? []    }
oyuncu.js
    envanterYazisi() {        if (this.envanter.length === 0) {            return "Eşyalar: yok"        }        const sayilar = this.envanter.reduce((sayac, esya) => {            sayac[esya] = (sayac[esya] ?? 0) + 1            return sayac        }, {})        const parcalar = Object.entries(sayilar).map(([esya, adet]) => `${esya} x${adet}`)        return "Eşyalar: " + parcalar.join(", ")    }     durum() {        const satir1 = `${this.isim} | Seviye ${this.seviye} | XP ${this.xp}/${this.gerekenXp}`        const satir2 = `Can ${this.hp}/${this.maxHp} | Güç ${this.guc} | Altın ${this.gold} | İksir ${this.iksir}`        return satir1 + "\n" + satir2 + "\n" + this.envanterYazisi()    }

join(", ") dizinin elemanlarını aralarına virgül ve boşluk koyarak birleştirir: ["Uzun Kılıç x2", "Yakut Kristali x1"] dizisi "Uzun Kılıç x2, Yakut Kristali x1" metni olur. dukkan.js'te kılıcın ve kristalin etkileri eşyayı envantere de koyuyor. Kılıcın tek satırlık etki'si artık iki iş yaptığı için süslü parantezli, çok satırlı bir gövdeye dönüştü:

dukkan.js
    {        isim: "Uzun Kılıç",        fiyat: 90,        aciklama: "+4 güç",        etki: (oyuncu) => {            oyuncu.guc = oyuncu.guc + 4            oyuncu.envanter.push("Uzun Kılıç")        }    },    {        isim: "Yakut Kristali",        fiyat: 120,        aciklama: "+25 en yüksek can",        etki: (oyuncu) => {            oyuncu.maxHp = oyuncu.maxHp + 25            oyuncu.hp = oyuncu.hp + 25            oyuncu.envanter.push("Yakut Kristali")        }    }

Sayma kısmını tek başına, sınır durumlarıyla dene: boş envanter, tek eşya, tekrarlı eşyalar:

deneme.js
function envanterYazisi(envanter) {    if (envanter.length === 0) {        return "Eşyalar: yok"    }    const sayilar = envanter.reduce((sayac, esya) => {        sayac[esya] = (sayac[esya] ?? 0) + 1        return sayac    }, {})    const parcalar = Object.entries(sayilar).map(([esya, adet]) => `${esya} x${adet}`)    return "Eşyalar: " + parcalar.join(", ")} console.log(envanterYazisi([]))console.log(envanterYazisi(["Uzun Kılıç"]))console.log(envanterYazisi(["Uzun Kılıç", "Yakut Kristali", "Uzun Kılıç"]))
Çıktı
Eşyalar: yokEşyalar: Uzun Kılıç x1Eşyalar: Uzun Kılıç x2, Yakut Kristali x1

Oyunda denemek için kayit.json'a elle {"isim":"Ahri","gold":300} yaz ve node index.js ile aç. Önce Durum'a bak, sonra dükkânda sırayla 2, 2, 3 ve 0 seç, en son yine Durum'a bak. İki kılıç ve bir kristal tam 300 altın eder; altın tam sıfıra iner. Aşağıdaki çıktıda uzun dükkân listelerini ... ile kısalttık:

Terminal — PowerShell
PS C:\kod\metin-rpg> node index.js=== Sihirdar Vadisi: Metin RPG ===Kayıt bulundu. Tekrar hoş geldin, Ahri! 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Ahri | Seviye 1 | XP 0/50Can 100/100 | Güç 12 | Altın 300 | İksir 1Eşyalar: yok 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 2...Ne alacaksın? 2Uzun Kılıç aldın. Kalan altın: 210...Ne alacaksın? 2Uzun Kılıç aldın. Kalan altın: 120...Ne alacaksın? 3Yakut Kristali aldın. Kalan altın: 0...Ne alacaksın? 0 1) Savaş  2) Dükkân  3) Durum  4) Kaydet  5) ÇıkSeçimin: 3Ahri | Seviye 1 | XP 0/50Can 125/125 | Güç 20 | Altın 0 | İksir 1Eşyalar: Uzun Kılıç x2, Yakut Kristali x1...

Kaydedip yeniden açınca envanter kayit.json'dan geri gelir. Envanteri olmayan eski kayıt Eşyalar: yok ile açılır, çünkü kayit.envanter ?? [] boş bir dizi verir.

Kendini sına

Soru 1

Oyuncu 7 yazarsa ekrana ne çıkar?

JavaScript
const seviye = await rl.question("Seviye? ")console.log(seviye * 2, seviye + 2)
Cevabı göster

14 72. question metin döndürür, yani seviye aslında "7". * metni sayıya çevirip çarpar, ama + bir tarafta metin görünce yapıştırır. Hesap yapacaksan önce Number(seviye).

Soru 2

JSON.stringify(new Oyuncu("Jinx")) ne yazar?

JavaScript
class Oyuncu {    constructor(isim) {        this.isim = isim        this.hp = 0    }     get canliMi() {        return this.hp > 0    }}
Cevabı göster

{"isim":"Jinx","hp":0}. Yalnız objenin kendi alanları yazılır. canliMi bir getter; sınıfta durur, objede değil. Kayda girmez ve girmesine gerek de yoktur: yüklerken new Oyuncu(...) onu geri getirir.

Soru 3

Bu yukle ile oyun, kayıt varken açılıyor ama menüde 3'e basınca çöküyor. Neden? Kayıt dosyası hiç yoksa ne olur?

JavaScript
export async function yukle() {    const metin = await readFile("kayit.json", "utf8")    return JSON.parse(metin)}
Cevabı göster

JSON.parse düz bir obje döndürür; içinde durum metodu yoktur. 3'e basınca TypeError: oyuncu.durum is not a function çıkar. Doğrusu new Oyuncu(veri.isim, veri) döndürmek.

İkinci sorun: kayıt dosyası yoksa readFile ENOENT hatası fırlatır ve yakalayan olmadığı için oyun hiç açılmaz. try/catch içinde hata.code === "ENOENT" ise null döndürmek gerekir. Bozuk bir kayıtta da JSON.parse'ın SyntaxError'ı oyunu açılışta çökertir; onu da yakalamak gerekir.

Soru 4

Mesajı oku: sorun ne, nasıl düzelir?

Hata çıktısı
Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'C:\kod\metin-rpg\savas' imported from C:\kod\metin-rpg\index.jsDid you mean to import "./savas.js"?
Cevabı göster

index.js, savas.js'i import ederken uzantıyı unutmuş: from "./savas". ES modüllerinde uzantı zorunlu; doğrusu from "./savas.js". Mesajın ikinci satırı doğrusunu zaten öneriyor.

Soru 5

kayit.json'u neden .gitignore'a ekledik? Hangi durumda bir kayıt dosyasını depoya koyardın?

Cevabı göster

Kayıt kişisel ve sürekli değişen bir dosya. Her kaydedişte commit geçmişini gereksiz değişikliklerle doldurur ve depoyu indiren herkes senin kaydınla başlar. Okura örnek göstermek istersen asıl kaydı değil, ornek-kayit.json gibi ayrı ve sabit bir dosyayı koyarsın.

Soru 6

"Ahri", "3", "5" | node index.js komutunda eski girdi.js neden 3 ile 5'i kaybetti? Yenisi neden kaybetmiyor?

Cevabı göster

Borudan üç satır aynı anda geldi. question yalnız beklediği satırı (Ahri) aldı; 3 ve 5 geldiğinde bekleyen bir soru yoktu ve readline onları bir kenara attı. Yeni girdi.js'teki satır sırası (rl[Symbol.asyncIterator]()) gelen her satırı saklar, her next() çağrısı sıradakini verir. Girdi bitince de done: true ile haber verir; program cevabı hiç gelmeyecek bir soruda takılıp uyarıyla kapanmaz.

Soru 7

savas.js'te const dusman = { ...sablon } yerine const dusman = sablon yazsaydık ne olurdu?

Cevabı göster

dusman ile şablon aynı objeyi gösterirdi. Savaşta düşmanın canı azalınca DUSMANLAR içindeki şablonun canı da azalırdı. Aynı tür düşman bir daha çıktığında canı 0 olurdu: savaş başlamadan "yenildi" yazar ve bedava altın verirdi. Yayma (...) her savaşa yeni bir kopya verir.

Kayıt noktası

Blok 2 burada bitiyor. Bir sonraki derste Blok 3'e, web'e geçiyorsun: tarayıcı ile sunucu nasıl konuşur, adres çubuğuna bir adres yazınca neler olur ve bir sitenin içine geliştirici araçlarıyla nasıl bakılır.

İşaret yalnızca bu tarayıcıda saklanır, kimseye gönderilmez.