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.
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/promisesile 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/catchile yakalayabilirsin - Oyunun durumunu
node:fs/promisesve 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
.gitignoreyazıp projeyi GitHub'a göndermeye hazırlayabilirsin
Bu sayfada
- Proje planı
- Adım 1: klasör ve package.json
- Kullanıcıdan girdi almak
- Adım 2: girdi.js ve ana menü
- Adım 3: yanlış girdiye dayanmak
- Adım 4: Oyuncu sınıfı
- Adım 5: savaş
- Adım 6: dükkân
- Adım 7: kaydetmek ve yüklemek
- Adım 8: oyunu otomatik denemek
- Modüllere bölmek: geriye bakış
- README, .gitignore ve GitHub
- Genişletme fikirleri
- Görevler
- Kendini sına
- 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:
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.jsondosyası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.
| Dosya | Görevi | Dışa aktardıkları (export) |
|---|---|---|
package.json | Proje bilgisi, "type": "module", npm start | yok |
index.js | Giriş noktası: açılış ve ana menü döngüsü | yok |
girdi.js | Klavyeden satır okumak, seçimi denetlemek | sor, secimeCevir, secimIste, girdiyiKapat |
oyuncu.js | Oyuncu sınıfı: can, altın, seviye, iksir | Oyuncu |
savas.js | Rastgele düşman, tur tur savaş | savas |
dukkan.js | Eşya listesi, satın alma | dukkan |
kayit.js | kayit.json dosyasına yazmak ve okumak | kaydet, 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?
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:
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ı:
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):
{ "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,exportve fonksiyon dışındaawaitserbest olur."start": "node index.js":npm startyazı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:
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()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:processmodü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.rladı 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.questionbir Promise döndürür;awaitile 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.
SyntaxError: Cannot use import statement outside a moduleNe 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.
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.0Nası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:
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()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):
const seviye = Number(await rl.question("Kaçıncı seviyedesin? "))console.log(`Bir sonraki seviyen: ${seviye + 1}`)PS C:\kod\metin-rpg> node deneme.jsAdın ne? AhriMerhaba Ahri!Kaçıncı seviyedesin? 7Bir sonraki seviyen: 8Oyuncu sayı yerine abc yazarsa ne olacak? Number("abc") sonucu NaN. Bunu Adım 3'te ele alacağız. Önce readline'ın üç sık hatası:
[object Promise] görmekNe zaman çıkar? rl.question'ın önüne await yazmayı unutunca.
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()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).
AbortError: Aborted with Ctrl+CNe 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.
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.0Yığı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.
Error [ERR_USE_AFTER_CLOSE]: readline was closedNe zaman çıkar? rl.close()'dan sonra yeniden rl.question çağırınca.
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}`)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.0Nası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:
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ıtrimile atıp döndürür (Ders 9). Oyuncu yanlışlıkla" 5 "yazarsa"5"olur.girdiyiKapat()arayüzü kapatır.rldış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:
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()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!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çindekibreakdö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.sormetin döndürdüğü için seçimi"5"gibi bir metinle karşılaştırdık.
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.
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:
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")))33NaN02.5falseBoş 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:
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()}secimeCevirtek 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.secimIstedoğru bir seçim gelene kadar sorar.tryiçindekireturnbaşarılı olursa fonksiyondan hemen çıkılır. Hata olursacatchmesajı 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:
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()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ğilSondaki 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:
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:
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:
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 }}canliMivegerekenXpbirer 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.iksirIcdeMath.minile canın en yüksek değeri aşmasını engeller.iksirIciksir yoksafalsedöndürür. Savaş kodu bu cevaba bakıp "İksirin kalmadı!" diyecek.xpKazanbirwhileile çalışır: tek seferde çok XP gelirse birden fazla seviye atlanabilir. Kaç seviye atlandığını döndürür.durumekrana 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:
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)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 0Sı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ı:
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()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:
Error [ERR_MODULE_NOT_FOUND]: Cannot find moduleNe zaman çıkar? import satırındaki dosya yolunda .js uzantısını unutunca: from "./oyuncu".
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.0Nası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.
SyntaxError: 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.
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.0Nası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:
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.`) }}DUSMANLARher 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ızsavas.jsbilir.zar(enAz, enCok)iki sınır dahil rastgele bir tam sayı verir (Ders 6'dakiMath.randomveMath.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
continueturu 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çarsanreturnsavaşı 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:
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))12 13 8En küçük: 8 En büyük: 16zar(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.
Ne zaman çıkar? Düşmanı şablondan kopyalamadan kullanınca: const dusman = sablon. Hata mesajı yok, ama oyun bozuk:
# ... ö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:
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)Birinci kurt yenildi.İkinci kurdun canı: 0Nası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:
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()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 115 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.
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:
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}`) } }}etkialanı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çini + 1yazdık; seçimi diziye geri çevirirken desecim - 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
0diyene 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ç:
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:
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)){"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:
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 }}kaydetyazma 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.yukleiki ayrıtrykullanıyor, çünkü iki ayrı şey ters gidebilir: dosya hiç olmayabilir, ya da var olup bozuk olabilir.- Dosya yoksa Node'un hatasının
codealanında"ENOENT"yazar. Bu bizim için hata değil, "kayıt yok, yeni oyun" demek;nulldö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.parsemetin bozuksaSyntaxErrorfırlatır. İçindeisimolmayan 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.
TypeError: veri.durum is not a functionNe zaman çıkar? Dosyadan okuyup JSON.parse ile çevirdiğin objenin metodunu çağırınca.
const metin = '{ "isim": "Ahri", "hp": 86 }'const veri = JSON.parse(metin)console.log(veri.isim)console.log(veri.durum())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.0veri.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ı:
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 oyundanew Oyuncu("Ahri")yazıncakayitboş bir obje olur.kayit.seviye ?? 1(Ders 12): kayıtta seviye varsa onu, yoksa (undefined) 1'i kullanır.||değil??kullandık, çünkü0geçerli bir değer:xpya dagoldsı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üzdenmaxHpsatı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.
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:
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:
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:
{ "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:
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:
Error: ENOENT: no such file or directoryNe 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:
import { readFile } from "node:fs/promises" const metin = await readFile("kayit.json", "utf8")console.log(metin)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.0Hatanı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:
SyntaxError: Expected double-quoted property name in JSONNe zaman çıkar? JSON.parse'a JSON kurallarına uymayan bir metin verince. Buradaki metin yarıda kesilmiş:
const metin = '{ "isim": "Ahri", "seviye": 3,'const veri = JSON.parse(metin)console.log(veri)<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.0Mesaj, 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:
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:
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 yalnız beklediği satırı alır. Satır sırası ise gelen her satırı sırayla saklar.Warning: Detected unsettled top-level awaitNe 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:
PS C:\kod\metin-rpg> $LASTEXITCODE13Nası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:
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 kezoutputvermedik. 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.loggibi 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 bittiysedonetrueolur; o zaman boş birconsole.log()ile sorunun satırını bitiripnulldöndürüyoruz.stdin.isTTY: girdi klavyeden, yani bir terminalden geliyorsatrue(TTY, eski dilde terminal demek), borudan geliyorsaundefined. 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.secimIstedenullgörüncenulldö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:
const secim = await secimIste("Seçimin: ", 1, 5) if (secim === null || secim === 5) { break const secim = await secimIste("1) Saldır 2) İksir iç 3) Kaç\nSeçimin: ", 1, 3) if (secim === null) { return } const secim = await secimIste("Ne alacaksın? ", 0, URUNLER.length) if (secim === null || secim === 0) { returnsavas.js'teki üç satır yeni; index.js ve dukkan.js'te yalnız koşul uzadı. Aynı denemeyi tekrarla:
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:
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.jsherkesi 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,zarveURUNLERkendi 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,secimeCevirsayı ya da hata döndürüyor; ikisi de bu yüzden tek başına denenebildi.
Projenin son hâli, VS Code'da:
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()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!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:
# 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:
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.jsondosyasını koymak daha doğru.
Karar: kayit.json depoya girmesin. Proje klasöründe .gitignore adında yeni bir dosya oluştur (Ders 7):
kayit.jsonnode_modules/node_modules/ satırı şimdilik gereksiz, çünkü hiç paket kurmadık; ama ileride kurarsan hazır olsun (Ders 14). Yeniden bak:
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:
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.jswarning 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:
- GitHub'da sağ üstteki + simgesine tıkla ve New repository'yi seç.
- Repository name kutusuna
metin-rpgyaz, istersen kısa bir açıklama ekle ve deponun herkese açık (Public) olmasını seç. - README,
.gitignoreya 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. - Create repository'ye tıkla. Açılan sayfada deponun adresini kopyala.
C:\kod\metin-rpgklasöründe aşağıdaki üç komutu çalıştır. Adrestekidenizyilmazyerine kendi kullanıcı adın gelecek.
git remote add origin https://github.com/denizyilmaz/metin-rpg.gitgit branch -M maingit push -u origin maingit remote add origin …: deponaoriginadıyla bir uzak depo adresi ekler.git branch -M main: dalın adınımaster'danmain'e çevirir; GitHub'ın varsayılan dal adımain.git push -u origin main: commit'lerini GitHub'a gönderir.-uyerelmaindalını GitHub'dakine bağlar; sonraki gönderimlerde yalnızgit pushyazman yeter. İlk gönderimde tarayıcıda giriş penceresi açılabilir.
Adresin doğru eklendiğini git remote -v ile görebilirsin:
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 denemeile 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:utilmodülündekistyleTextile 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.jsonyap; yazarken elektrik kesilirse eski kayıt bozulmasın. - Testler: Ders 53'te
secimeCevirveOyuncuiç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ı.
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.
=== 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:
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}`) }} 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:
{ "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):
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.
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
"scripts": { "start": "node index.js", "deneme": "node index.js < deneme-girdi.txt" },Ahri321035Terminalde npm run deneme yazınca oyun kendi kendine oynar:
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:
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.
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:
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 }]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:
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))[ '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.
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.
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:
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:
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:
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:
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){ 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):
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.
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:
this.iksir = kayit.iksir ?? 1 this.envanter = kayit.envanter ?? [] } 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ü:
{ 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:
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ıç"]))Eşyalar: yokEşyalar: Uzun Kılıç x1Eşyalar: Uzun Kılıç x2, Yakut Kristali x1Oyunda 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:
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?
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?
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?
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?
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.