Tutorial Pasukan Lucee MY

Bina permainan Sudoku web dengan CFML dan MySQL

Tutorial membina permainan Sudoku dalam CFML dan MySQL: penjana satu penyelesaian, API JSON, semakan jawapan, papan pendahulu dan perangkap Lucee.

Bina permainan Sudoku web dengan CFML dan MySQL
0
Kandungan

Membina Permainan Sudoku Web dengan CFML (Lucee 7.1) dan MySQL

Banyak teka-teki Sudoku dalam laman web bergantung kepada skrip pihak ketiga. Kawalan sukar. Iklan muncul. Data pemain tidak semestinya selamat. Masa muat juga bergantung kepada pembekal luar. Jika teka-teki perlu diubah suai, kita terpaksa menunggu vendor. Jika vendor menutup servis, permainan boleh rosak. Kita juga tidak boleh buang iklan sepenuhnya.

Tutorial ini membina permainan Sudoku sendiri dengan CFML pada Lucee 7.1 dan MySQL. Server menjana teka-teki, menyemak jawapan, menyimpan skor, dan memaparkan papan pendahulu. Front-end kekal ringan. Semua peraturan penting berada di server. Projek ini sesuai untuk pembangun CFML pertengahan yang sudah tahu <cfquery>, komponen, dan JavaScript asas. Hasilnya ialah alat ringkas yang boleh dikawal dari segi reka bentuk, privasi, dan prestasi.

Apa yang kita bina

Kita membina satu halaman permainan. Fail utama ialah views/alat/sudoku.cfm, assets/css/sudoku.css, dan assets/js/sudoku.js. Logik server berada dalam services/SudokuService.cfc dan handlers/alat.cfc. Aliran utama mudah: pemain minta teka-teki, mengisi grid, menyemak, dan menghantar skor.

Ciri-ciri:

  • Tiga tahap: mudah, sederhana, susah.
  • Pemasa berjalan di klien dan dihantar sebagai saat.
  • Kiraan kesilapan disimpan dan dihantar sebagai kesilapan.
  • Tiga petunjuk dibenarkan dan dihantar sebagai bantuan.
  • Mod nota untuk calon angka.
  • Butang skrin penuh.
  • Papan pendahulu mengikut tingkat.

Grid dibina dengan CSS. Setiap sel boleh dipilih. Nota dipaparkan sebagai teks kecil dalam sel. JavaScript mengurus interaksi, tetapi keputusan akhir dibuat oleh server. Pendekatan ini mengelakkan cheat mudah.

Pangkalan data

Dua jadual cukup. permainan menyimpan teka-teki asal dan penyelesaian. skor menyimpan rekod pemain. Hubungan ON DELETE CASCADE memastikan skor padam jika permainan dipadam. Lajur tingkat disalin ke skor supaya papan pendahulu boleh ditapis tanpa join. Indeks idx_skor (tingkat, saat) menyokong susunan pantas.

Sebab setiap lajur:

  • permainan.id: kunci utama untuk rujukan API.
  • permainan.teka_awal: grid awal 81 aksara.
  • permainan.penyelesaian: grid sah 81 aksara.
  • permainan.tingkat: tahap permainan.
  • permainan.dicipta_pada: tarikh ciptaan untuk audit.
  • skor.id: kunci utama auto increment.
  • skor.permainan_id: foreign key ke permainan.
  • skor.nama: nama pemain.
  • skor.saat: masa tamat.
  • skor.kesilapan: bilangan jawapan salah.
  • skor.bantuan: bilangan petunjuk.
  • skor.tingkat: salinan tahap untuk pertanyaan pantas.

CHAR(81) dipilih kerana grid tetap. VARCHAR(10) cukup untuk nama tahap. TINYINT UNSIGNED memadai untuk kesilapan dan bantuan. NOT NULL mengelak nilai hilang pada medan wajib. Foreign key menggunakan CHAR(36) supaya sepadan dengan id permainan. Walaupun createUUID() Lucee memulangkan 35 aksara, lajur ini masih diterima.

SQL penuh:

CREATE TABLE permainan (
  id CHAR(36) NOT NULL,
  teka_awal CHAR(81) NOT NULL,
  penyelesaian CHAR(81) NOT NULL,
  tingkat VARCHAR(10) NOT NULL,
  dicipta_pada DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
) ENGINE = InnoDB;

CREATE TABLE skor (
  id INT UNSIGNED NOT NULL AUTO_INCREMENT,
  permainan_id CHAR(36) NOT NULL,
  nama VARCHAR(40) NOT NULL,
  saat INT UNSIGNED NOT NULL,
  kesilapan TINYINT UNSIGNED NOT NULL,
  bantuan TINYINT UNSIGNED NOT NULL,
  tingkat VARCHAR(10) NOT NULL,
  dicipta_pada DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  KEY idx_skor (tingkat, saat),
  CONSTRAINT fk_skor_permainan
    FOREIGN KEY (permainan_id)
    REFERENCES permainan(id)
    ON DELETE CASCADE
) ENGINE = InnoDB;

Menjana teka-teki yang ada satu penyelesaian sahaja

Kita guna backtracking. Mula dengan grid penuh yang sah. Kemudian buang sel satu demi satu. Setiap kali sel dibuang, kiraPenyelesaian(teks, had) menghitung jumlah penyelesaian sehingga had. Jika jumlah bukan satu, sel dikembalikan. Dengan cara ini, teka-teki akhir ada satu penyelesaian sahaja. Backtracking mencuba nilai secara mendalam. Jika buntu, ia undur dan cuba nilai lain. Semakan keunikan dijalankan selepas setiap pembuangan.

Fungsi sah(teks) menyemak baris, lajur, dan kotak 3x3. Ia tidak menyelesaikan keseluruhan grid. Ia hanya menolak calon yang mustahil. Ini mempercepatkan carian. Had 2 cukup untuk tahu unik atau tidak. Jika kiraan sudah mencapai 2, fungsi berhenti.

services/SudokuService.cfc mempunyai jana(tingkat), sah(teks), kiraPenyelesaian(teks, had), dan semak(teks, penyelesaian). Contoh di bawah menunjukkan teras penjanaan. Fungsi gridRawak() dan pilihPosKosong() ialah helper dalam projek sebenar. Contoh ini memudahkan pemilihan posisi; projek sebenar menggunakan senarai posisi rawak.

component {
  public struct function jana(tingkat) {
    var lubang = tingkat == "susah" ? 54 : tingkat == "sederhana" ? 48 : 40;
    var penyelesaian = gridRawak();
    var teka = penyelesaian;
    for (var i = 1; i <= lubang; i++) {
      var pos = pilihPosKosong(teka);
      var asal = mid(teka, pos, 1);
      teka = left(teka, pos - 1) & "0" & right(teka, 81 - pos);
      if (kiraPenyelesaian(teka, 2) != 1) {
        teka = left(teka, pos - 1) & asal & right(teka, 81 - pos);
      }
    }
    return {teka: teka, penyelesaian: penyelesaian, lubang: lubang};
  }
  public numeric function kiraPenyelesaian(teks, had) {
    var pos = find("0", teks);
    if (!pos) return 1;
    var count = 0;
    for (var n = 1; n <= 9; n++) {
      var calon = left(teks, pos - 1) & toString(n) & right(teks, 81 - pos);
      if (sah(calon)) {
        count += kiraPenyelesaian(calon, had - count);
        if (count >= had) return count;
      }
    }
    return count;
  }
}

API dalam CFML

API diurus oleh handlers/alat.cfc. Tindakan tersedia ialah sudoku, sudokuBaru, sudokuSemak, sudokuPetunjuk, sudokuSelesai, dan sudokuPapan. Setiap POST memerlukan token CSRF portal. Respons dibina sebagai JSON manual. Ini mengelak masalah kunci huruf besar daripada serializeJSON(). JSON manual menggunakan petikan berganda yang di-escape.

Bentuk JSON:

  • POST /alat/sudoku/baru dengan tingkat (mudah|sederhana|susah) → {ok, id, teka, tingkat, lubang}.
  • POST /alat/sudoku/semak dengan id, teks (81 aksara) → {ok, salah:[indeks], penuh, selesai}.
  • POST /alat/sudoku/petunjuk dengan id, indeks → {ok, indeks, nilai}.
  • POST /alat/sudoku/selesai dengan id, nama, saat, kesilapan, bantuan → {ok, pangkat, tingkat}.
  • GET /alat/sudoku/papan → {ok, papan:{mudah:[],sederhana:[],susah:[]}}.

Peraturan server:

  • Nama 2 hingga 40 aksara huruf, ruang, titik, sempang dan apostrof.
  • Masa 5 hingga 7,200 saat, kesilapan 0 hingga 99, bantuan 0 hingga 3.
  • Permainan yang sama tidak boleh dihantar dua kali. Server memulangkan 409.

Jika id tidak dijumpai, server memulangkan ok false. Jika teks bukan 81 aksara, server menolak. Jika nama tidak sah, skor tidak disimpan. Semua semakan ini berlaku sebelum sebarang rekod ditulis.

Pengendali sudokuSemak membaca permainan mengikut id, kemudian memanggil semak(). Pengendali sudokuPetunjuk membaca nilai penyelesaian pada indeks diminta. Pengendali sudokuSelesai validasi semua medan sebelum insert. sudokuPapan memulangkan tiga senarai papan. Contoh pengendali sudokuBaru:

public void function sudokuBaru(struct params = {}) {
    if (cgi.request_method != "POST") jsonKeluar({ ok: false, ralat: "Kaedah tidak dibenarkan." }, 405);
    var tingkat = formTeks("tingkat", 12);
    if (!listFindNoCase("mudah,sederhana,susah", tingkat)) tingkat = "mudah";
    var hasil = new app.services.SudokuService().jana(tingkat);
    var id = createUUID();
    // Penyelesaian disimpan di pelayan untuk semakan kemudian.
    queryExecute(
        "INSERT INTO permainan (id, teka_awal, penyelesaian, tingkat) VALUES (:id, :t, :s, :k)",
        { id: { value: id, cfsqltype: "cf_sql_varchar" },
          t: { value: hasil.teka, cfsqltype: "cf_sql_char" },
          s: { value: hasil.selesai, cfsqltype: "cf_sql_char" },
          k: { value: tingkat, cfsqltype: "cf_sql_varchar" } },
        { datasource: "sudoku" }
    );
    jsonKeluar({ ok: true, id: id, teka: hasil.teka, tingkat: tingkat, lubang: hasil.lubang });
}

Memilih lajur dan menyemak jawapan

Client menghantar rentetan 81 aksara. Angka 1 hingga 9 ialah jawapan. Angka 0 ialah sel kosong. Server membandingkan setiap sel dengan penyelesaian. Indeks dalam salah bermula dari 0 supaya mudah dipetakan ke grid JavaScript.

penuh bermaksud tiada 0. selesai bermaksud grid penuh dan tiada sel salah. Jika ada satu sel salah, selesai kekal false walaupun grid penuh. Jika indeks 10 salah, array memulangkan 10. Jika semua sel betul tetapi masih ada 0, penuh false. Contoh:

public struct function semak(teks, penyelesaian) {
  if (!reFind("^[0-9]{81}$", teks)) {
    return {ok: false};
  }
  var salah = [];
  var penuh = true;
  for (var i = 1; i <= 81; i++) {
    var nilai = mid(teks, i, 1);
    if (nilai == "0") {
      penuh = false;
    } else if (nilai != mid(penyelesaian, i, 1)) {
      arrayAppend(salah, i - 1);
    }
  }
  var selesai = penuh && arrayLen(salah) == 0;
  return {ok: true, salah: salah, penuh: penuh, selesai: selesai};
}

Papan pendahulu yang adil

Papan pendahulu disusun berdasarkan masa penyelesaian, bukan berdasarkan jumlah kesilapan atau bantuan. Kod sebenar menyimpan nilai kesilapan dan bantuan supaya kedua-duanya boleh dipaparkan semula sebagai maklumat tambahan, tetapi nilai itu tidak digunakan untuk menentukan kedudukan. Susunan utama ialah saat ASC, manakala dicipta_pada ASC menjadi pemutus seri apabila dua catatan mempunyai masa yang sama. Dengan cara ini, catatan yang dicipta lebih awal akan muncul dahulu jika masanya seiras. Walaupun metrik kesilapan dan bantuan kekal kelihatan, ia tidak mengubah kedudukan senarai. Format tarikh yang dipaparkan juga membantu pengguna membezakan catatan tanpa mendedahkan butiran berlebihan. Prinsipnya mudah: masa menentukan kedudukan, manakala data sokongan hanya melengkapkan paparan. Ia turut mengurangkan kejutan apabila papan dikemas kini dengan data baharu. Ia juga memudahkan ujian automatik.

Blok SQL berikut menunjukkan bentuk pertanyaan yang digunakan:

SELECT nama, saat, kesilapan, bantuan,
       DATE_FORMAT(dicipta_pada, '%Y-%m-%d %H:%i') AS bila
FROM skor
WHERE tingkat = :t
ORDER BY saat ASC, dicipta_pada ASC
LIMIT 10;

Untuk menentukan pangkat selepas penyisipan, kod menggunakan kiraan baris yang memenuhi syarat masa lebih kecil, atau masa sama dengan id tidak melebihi id terkini:

SELECT COUNT(*)
FROM skor
WHERE tingkat = :t
  AND (saat < :saat
       OR (saat = :saat AND id <= LAST_INSERT_ID()));

Pendekatan ini mengelakkan pergantungan pada nilai rawak dan mengekalkan keputusan yang konsisten. Jika anda ingin melihat contoh lain tentang penjanaan dan ralat, lawati penjana QR CFML dan ralat runtime CFML skop.

Perangkap sebenar di Lucee

  1. Tiada fungsi shuffle(). Lucee tidak menyediakan fungsi itu. Pembetulan: tulis Fisher-Yates sendiri dengan randRange().
  2. arraySlice() memerlukan dua argumen. Jika argumen kurang, ralat runtime muncul. Pembetulan: salinan array dibuat dengan gelung, bukan bergantung kepada satu panggilan.
  3. serializeJSON() menukar kunci struct kepada huruf besar. Ini merosakkan kontrak JSON seperti ok dan id. Pembetulan: bina JSON secara manual.
  4. Tanda # tidak berinterpolasi dalam cfscript. Jika ditulis dalam rentetan, ia kekal sebagai teks. Pembetulan: gabungkan rentetan dengan &.
  5. createUUID() dalam Lucee memulangkan 35 aksara, bukan 36. Formatnya 8-4-4-16. Pembetulan: corak regex mesti sepadan dengan 35 aksara.
  6. Grid CSS 1fr tidak mengecil bawah lebar kandungan. Sel boleh melimpah pada skrin sempit. Pembetulan: guna minmax(0,1fr).

Prestasi yang diukur

Ujian dijalankan pada Lucee 7.1.1.17 dan MySQL 8.4.

  • Bilangan lubang: mudah 40, sederhana 48, susah 54.
  • Masa jana: 116 ms untuk mudah, 1.3 s untuk sederhana, 1.1 s untuk susah.
  • Setiap teka-teki disahkan mempunyai tepat satu penyelesaian.
  • Halaman muat pada 320 px hingga 430 px lebar tanpa limpahan.

Angka ini menunjukkan penjanaan sederhana dan susah masih sesuai untuk permintaan web biasa. Masa jana tidak perlu berlaku setiap kali pemain memuat halaman jika cache digunakan. Namun projek ini menyimpan setiap permainan dalam pangkalan data supaya id boleh dirujuk semula. Masa susah lebih rendah daripada sederhana dalam ujian ini kerana variasi penjanaan rawak. Ia masih dalam lingkungan saat. Saiz grid tetap kecil. Tiada imej berat. CSS dan JS boleh dimampatkan.

Soalan lazim

Adakah JavaScript perlu menyimpan penyelesaian?

Tidak. Penyelesaian disimpan dalam jadual permainan. Client hanya menghantar teks 81 aksara. Server membuat perbandingan akhir. Ini juga memudahkan audit. Pendekatan ini juga membolehkan tukar teka-teki tanpa mengubah klien.

Bagaimana mod nota berfungsi?

Nota disimpan di klien sebagai lapisan UI. Ia tidak mengubah teks jawapan sebenar. Apabila pemain menghantar grid, hanya nilai tetap dinilai. Pendekatan ini mengelakkan skim kompleks. Jika nota perlu kekal selepas refresh, simpan dalam localStorage.

Bolehkah pemain menghantar skor palsu?

Server menyemak token CSRF, julat masa, nama, dan bilangan bantuan. Permainan yang sama juga ditolak dengan 409. Ini mengurangkan penghantaran palsu. Walau bagaimanapun, klien masih boleh cuba; server tetap sumber kebenaran. Log audit boleh ditambah jika perlu.

Mengapa tahap mengubah bilangan lubang?

Lubang menentukan berapa banyak sel kosong. Mudah ada 40 lubang. Sederhana ada 48. Susah ada 54. Lebih banyak lubang biasanya lebih sukar. Perbezaan ini juga mempengaruhi masa jana. Ia juga memberi profil masa jana berbeza.

Permainan ini lengkap dengan penjanaan unik, semakan server, dan papan pendahulu. Alat sebenar tersedia di /alat/sudoku. Kod boleh disesuaikan untuk alat lain. Pendekatan yang sama boleh dipakai untuk permainan logik lain.

0 komen

0 komen

Log masuk untuk menjawab atau memberi komen.

Log masuk