創作內容

24 GP

JS網頁遊戲-客製化Nonogram

作者:熾炎之翼│2021-09-17 15:37:12│巴幣:48│人氣:1001


遊戲連結:https://skylining823.github.io/nonogram/index.html


前言:

度度度 一直說要做的Nonogram
最初會想做這個遊戲
不只是當作練習HTML+CSS+Javascript
其實主要是因為我自己在玩其他Nonogram的時候被各種廣告煩到很不爽
而且不能自訂行列 生命值也沒辦法自己決定
再加上我其實想要玩看看隨機版面
所以就乾脆自己動手做了
過程其實很辛苦 畢竟JS跟CSS學不到幾個禮拜
而且開學以後比較忙 所以就被我擱置了
直到昨天12點不知道發生什麼事直接肝到3點
今天早上沒啥課就直接調整得差不多了

實際做好真的很有成就感


規則:

首先介紹一下Nonogram規則

就是利用行列的提示來找到黑塊
4代表有一組4格黑塊 33代表有兩組3格黑塊 1111就是有四組1格黑塊
而數字順序即代表出現的順序
比如10格中出現54即代表
■■■■■X■■■■
以此類推 算是不難上手


遊戲介面:

一開始遊戲介面是這樣

版面配置:決定行列 不過請注意數字不要寫太大一來是版面容易跑掉(可以靠縮放解決),二來是你瀏覽器可能會跑到當掉XD
難度:分為1~3(填小於1會歸類在1,填大於3會歸類在3),數字越大離散度越高,總體難度越難
生命值:預設3,無上限(至少1),點錯到紅塊會扣1,剩餘0則遊戲結束

以下以5x5開局來介紹:

設定好後 按「確認/重置」按鈕可以開始/換新一局遊戲

點擊灰塊提示按鈕可以得到完整提示
(在過大的行列容易出現數字溢出框的情況,這功能就很重要了)


所有黑塊找出來即可獲勝

反之要是一直點錯 生命值歸0就會重置一局遊戲

最後 如果版面亂掉調整視窗即可


遊戲主軸目前差不多就這樣
簡單又有可玩性 很需要動腦來解題

技術方面:

下面來講解技術細節
一樣先只講解JS的核心概念
有空再補剩下的
畢竟感覺要打好多好多...QQ

HTML
<!DOCTYPE html>
<html>

<head>
    <link rel="stylesheet" type="text/css" href="style.css">
    <title>Nonogram</title>
</head>

<body>
    <div id="settingBoard">
        <b>版面配置:</b>
        <input type="text" id="row" class="settingItem" placeholder="Rows">
        <b>×</b>
        <input type="text" id="col" class="settingItem" placeholder="Cols">
    </div>
    <div id="difficultBoard">
        <b>難度:</b>
        <input type="text" id="difficult" placeholder="1~3">
        <b>生命值:</b>
        <input type="text" id="life" value="3">
    </div>
    <div id="confirmBtnBoard">
        <button id="confirmBtn">確定 / 重置</button>
    </div>
    <div id='warningBoard'>
        <b>提示: 如果版面亂掉請重新縮放頁面大小,點擊灰框可以得到完整提示</b>
    </div>
    <div id="nowLifeBoard">
        <b id="nowLifeText">當前生命值:</b>
        <div id="nowLife">0</div>
    </div>
    </div>
    <div id="container">
        <div id="Board">
        </div>
    </div>
    <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous">
    </script>
    <script src="script.js"></script>
</body>

</html>


CSS
#settingBoard,
#difficultBoard,
#confirmBtnBoard,
#warningBoard,
#nowLifeBoard {
    display: flex;
    justify-content: center;
    position: relative;
    flex-direction: row;
    margin: 10px;
}

#nowLifeText {
    font-size: 20px;
    margin-top: 5px;
}

#nowLife {
    font-size: 30px;
    color: red;
    margin-left: 10px;
}

.settingItem,
#difficult,
#life {
    width: 35px;
    margin-left: 10px;
    margin-right: 10px;
}

#container {
    position: relative;
    margin: auto;
    margin-right: 50px;
    display: flex;
    justify-content: center;
}

#Board {
    position: relative;
    width: 500px;
    height: 500px;
}

.hiddenBtn,
.pressBtn,
.hintBtn {
    -webkit-appearance: none;
    width: 50px;
    height: 50px;
    margin: 15px;
    float: left;
    border-width: 1px;
    border-radius: 0%;
    margin: 0px;
}

.hintBtn {
    background-color: gray;
    text-align: left;
}

.hiddenBtn {
    background-color: rgba(0, 0, 0, 0);
    border-width: 0px;
    margin: 0px;
}


Javascript
const row = document.getElementById("row")
const col = document.getElementById("col")
const difficult = document.getElementById("difficult")
const life = document.getElementById("life")
const confirmBtn = document.getElementById("confirmBtn")
const Board = document.getElementById('Board')


function colCount(rowNum, colNum, ansArray) {
    for (let i = 0; i < colNum; i++) {
        let numArray = new Array();
        let colArray = new Array();
        count = 0;
        t = 0;
        for (let j = 0; j < rowNum; j++)
            colArray.push(ansArray[j][i])
        for (let j = 0; j < colArray.length; j++) {
            if (colArray[j] == 1) {
                count += 1;
            } else {
                if (count == 0)
                    continue;
                else {
                    numArray.push(count);
                    count = 0;
                    t++;
                }
            }
        }
        if (count != 0) {
            numArray.push(count);
            t++;
        }
        if (t == 0) {
            numArray.push(0);
        }
        for (let k = 0; k < numArray.length; k++) {
            document.getElementById(`Col${i+1}`).innerText += String(numArray[k]) + ".";
        }
    }
}

function rowCount(rowNum, colNum, ansArray) {
    for (let i = 0; i < rowNum; i++) {
        let numArray = new Array();
        count = 0;
        t = 0;
        for (let j = 0; j < colNum; j++) {
            if (ansArray[i][j] == 1) {
                count += 1;
            } else {
                if (count == 0)
                    continue;
                else {
                    numArray.push(count);
                    count = 0;
                    t++;
                }
            }
        }
        if (count != 0) {
            numArray.push(count);
            t++;
        }
        if (t == 0) {
            numArray.push(0);
        }
        for (let k = 0; k < numArray.length; k++) {
            document.getElementById(`Row${i+1}`).innerText += String(numArray[k]) + ".";
        }
    }

}

function check(rowNum, colNum, ansArray) {
    let s = 0;
    for (let i = 0; i < rowNum; i++) {
        for (let j = 0; j < colNum; j++) {
            if (ansArray[i][j] == 1) {
                s = 1;
                break;
            }
        }
    }
    if (s == 0) {
        alert("恭喜你找出所有黑塊!!!");
    }
}


confirmBtn.addEventListener("click", function() {
    rowNum = parseInt(row.value);
    colNum = parseInt(col.value);
    difficultNum = parseInt(difficult.value);
    lifeNum = parseInt(life.value);

    if (lifeNum < 1)
        lifeNum = 1;
    if (difficultNum < 1)
        difficultNum = 1;
    else if (difficultNum > 3)
        difficultNum = 3;

    document.getElementById("nowLife").innerText = lifeNum;
    let nowlife = document.getElementById("nowLife").innerText

    Board.innerHTML = ''
    for (let i = 0; i < rowNum + 1; i++) {
        for (let j = 0; j < colNum + 1; j++) {
            if (i == 0 && j == 0) {
                Board.innerHTML += `<button class="hiddenBtn"></button>`;
            } else if (i == 0) {
                Board.innerHTML += `<button class="hintBtn" id="Col${j}"></button>`;
            } else if (j == 0) {
                Board.innerHTML += `<button class="hintBtn" id="Row${i}"></button>`;
            } else {
                Board.innerHTML += `<button class="pressBtn" id="${i}x${j}"></button>`;
            }
        }
    }

    let ansArray = new Array(); //先宣告一維
    for (let i = 0; i < rowNum; i++) {
        ansArray[i] = new Array(); //宣告二維
        for (let j = 0; j < colNum; j++) {
            let randNum = Math.floor(Math.random() * 10);
            if (randNum <=
8 - difficultNum)
                ansArray[i][j] = 1;
            else
                ansArray[i][j] = 0;
        }
    }

    rowCount(rowNum, colNum, ansArray);
    colCount(rowNum, colNum, ansArray);

    let pressBtn = document.getElementsByClassName("pressBtn");
    let hintBtn = document.getElementsByClassName("hintBtn");

    for (let i = 0; i < pressBtn.length; i++) {
        pressBtn[i].addEventListener("click", function() {
            const btnRow = Math.floor(i / colNum);
            const btnCol = i % colNum;
            if (ansArray[btnRow][btnCol] == 1) {
                this.style.backgroundColor = "black";
                ansArray[btnRow][btnCol] = 0;
                check(rowNum, colNum, ansArray);
            } else {
                if (this.style.backgroundColor != "black") {
                    this.style.backgroundColor = "red";
                    this.innerHTML = "X"
                    nowlife -= 1;
                    document.getElementById("nowLife").innerText = nowlife;
                    if (nowlife == 0) {
                        alert("您的生命值已歸0!\n遊戲將重置");
                        confirmBtn.click();
                    }


                }
            }
        })
    }

    for (let i = 0; i < hintBtn.length; i++) {
        hintBtn[i].addEventListener("click", function() {
            alert(hintBtn[i].innerText);
        })
    }

    Board.style.cssText = `width: ${(colNum+1)*50}px; height: ${(rowNum+1)*50}px;`;
})

本人網頁開發菜逼八 請鞭小力

程式主要圍繞於confirmBtn.addEventListener()
講白話就是點擊確認按鈕後即開始動作
程式會按照玩家輸入的Row數Col數開始生成按鈕
這邊其實生成的行列數都會各多一
因為要預留給提示方塊按鈕
為了維持版面 其實左上角的缺口也有個隱藏按鈕
只是被我從CSS用background-color: rgba(0, 0, 0, 0);調成透明的而已

接著宣告一個二維陣列ansArray來生成對應整個局面的地圖
我的邏輯就是1代表黑塊 0代表紅塊
程式會照版面亂數生成0,1
其實一開始0,1的生成機率是1:1
但是經過我本人和朋友們測試 這樣會讓離散度太高
實際玩起來會難到哭
而先前輸入的難度就用在這裡了
我事後運用了難度difficultNum來調節生成機率
黑紅生成機率 難度1是7:3 難度2是6:4 難度3是5:5
這樣就很大程度解決了這個問題
也進一步增加客製程度

然後用rowCount()colCount()兩個函式
根據ansArray()來偵測版面
並且把提示數字印到提示按鈕上

之後就用迴圈幫每個按鈕都掛上addEventListener()
也就是裝上各自該有的功能與邏輯判斷

每個隱藏的黑塊被點擊後除了現形之外
同時在ansArray上的值都會從1改成0
再去跑前面寫的check()函式做檢測
一旦整個數值地圖上都沒有1
則判斷玩家勝利

每個隱藏紅塊被點擊除了現形之外
會讓生命值扣1再判斷剩下的生命值
如果生命值歸0則判斷玩家敗北
再彈出提示以後即重新一局遊戲

順帶一提 紅塊的判斷有加上條件 this.style.backgroundColor != "black"
來避免點到已經找到的黑塊
因為數值地圖上該座標變成0
而被系統誤判斷採到紅塊的窘境

每個提示灰塊的功能就很簡單
點擊以後用彈出alert的方式告訴玩家提示數字

最後的Board.style.cssText = `width: ${(colNum+1)*50}px; height: ${(rowNum+1)*50}px;`;
就是在動態調整版面容器大小
讓版面可以維持成Row+1 x Col+1的格式

技術就差不多到這邊
剩下之後有空補

檢討要點:

這次總體來說完整度偏高

不過對 我到打這篇文的時候
才發現我忘記做手動打X功能 = =
這之後要補上

然後其實也能加上紀錄勝敗場的功能等等
現在只是基本功能齊全(除了手動打X = =)而已

還有一個問題雖然發生機率不太大但是確實存在
就是亂數的不可控 就算可以用難度來限制大致趨勢
但還是有機會出現難度1出現超難版面 難度3出現超智障版面的狀況
這部分目前也只能重置局面來解決了
還要再想想有啥限制的方式



度度度 打好多字
差不多就這樣 歡迎大家遊玩
有BUG或是建議請務必在留言告訴我

最後祝大家玩得開心~
感謝觀看


引用網址:https://home.gamer.com.tw/TrackBack.php?sn=5268027
All rights reserved. 版權所有,保留一切權利

相關創作

留言共 14 篇留言

只是個安弟
大佬

09-17 15:41

熾炎之翼
網頁開發我真的是菜逼巴 = =09-17 15:57
無鹽粄條
感謝分享
我第一場就手賤設太大 直接當掉XD

09-17 15:52

熾炎之翼
我相信一旦開放自訂 應該很多人就會設超大XD09-17 15:57
燃燄-ないない
之前玩過類似的手遊 還蠻好玩ㄉ

09-17 15:55

熾炎之翼
444 我也玩過
只不過我想要更多客製化的功能
所以就自己寫了一個09-17 15:58
無鹽粄條
突然想到 難度的部分是不是可以設定黑格的總數來避免你說的狀況

09-17 16:05

熾炎之翼
我想總數問題不大 問題是可能會太離散09-17 16:06
白鳥
你是先生成答案再反推數字的嗎?同樣數字pattern不見得都是唯一解唷

09-17 16:31

熾炎之翼
我是生成答案再去求數字沒錯
欸原來會有不同解ㄇ?09-17 17:01
熾炎之翼
我跟朋友測試都還沒發生這種狀況09-17 17:01
白鳥
像下面兩個都是對的,我剛剛有試出來

--| 1 | 1 |
1 | * | - |
1 | - | * |


--| 1 | 1 |
1 | - | * |
1 | * | - |

09-17 17:09

熾炎之翼
對欸…但是這算nonogram的缺點(?ㄇ
如果只有單一數字(多個數字就會有前後關係
大概就沒啥解了09-17 17:49
燃燄-ないない
剛剛玩了一場覺得能設很大的也很猛 雖然也是解得出來 不過會記的頭很痛

09-17 18:46

熾炎之翼
我朋友有解出20x20的XD09-17 19:07
燃燄-ないない
20x20的有些灰框會太小把字吃掉,要點才能看到完整的

09-17 19:20

熾炎之翼
這是真的 太離散就會這樣09-17 19:31
樂小呈
打了三還四場
https://i.imgur.com/zxpuaVN.jpg

難度的問題,隨機生成上可以試試以排為單位,不以單格
把不同分散狀態分類 (難1~2 中3~5 簡單6~8 免費9~10)
根據輸入的難度當權重,去抽當行的難度,你的控制度會更高
還可以固定送幾排,像初始就需要幾行 7 ~ 10 來讓玩家能開始推理

功能上可以添加鎖定行的功能,讓玩家鎖定特定排
還有就是,讓玩家可以在已選擇的正確選項上填數字,才不用重複看提示
版面越大的話這兩個功能效果會越明顯

就降,讚歐

09-17 19:29

熾炎之翼
果然遊戲方面還是你專業的多XDD
真的很感謝你的建議
讓我知道改進的方向在哪裡了 謝啦09-17 19:33
白鳥
https://theses.liacs.nl/pdf/2013-2014Lai-yeeLiu.pdf
可以看第四章"How to Create a Uniquely Solvable Nonogram?"
這其實比我想像中的還要複雜XD

09-17 20:17

熾炎之翼
哇XD09-17 20:27
白鳥
簡單來說就是生成完之後自動去解謎題,如果有超過一組解,就去更動原本的圖,反覆直到只有一組解為止,跟生成數獨題目有點像

09-17 20:22

熾炎之翼
喔我懂你的意思了
就是要直接換圖 看來這不簡單ㄋ…09-17 20:27
燃燄-ないない
解完20x20了 拿出小畫家標不是的方塊ww

09-17 21:28

熾炎之翼
抱歉 我應該早點開發打X的功能 [e3]09-17 21:31
泌尿器
想詢問一下
附圖這種case :
https://imgur.com/xka6UU1
紅塊那邊不應該是黑塊嗎?

03-04 18:19

熾炎之翼
我個人猜測應該是這樣

https://media.discordapp.net/attachments/856516846144192543/949251307213037578/IMG_5443.jpg03-04 18:26
熾炎之翼
他因為方格大小限制所以沒有顯示最後一個1. 您可以點擊一下灰色按鈕來看看完整資訊 這部分我沒設計好 不好意思@@03-04 18:27
泌尿器
原來如此, 感謝!

03-04 18:31

我要留言提醒:您尚未登入,請先登入再留言

24喜歡★h32902227 可決定是否刪除您的留言,請勿發表違反站規文字。

前一篇:2021/09/16 生... 後一篇:2021/09/17 N...

追蹤私訊切換新版閱覽

作品資料夾

Lobster0627全體巴友
大家可以多多來我的YT頻道看看哦(*´∀`)~♥https://www.youtube.com/@lobstersandwich看更多我要大聲說昨天18:34


face基於日前微軟官方表示 Internet Explorer 不再支援新的網路標準,可能無法使用新的應用程式來呈現網站內容,在瀏覽器支援度及網站安全性的雙重考量下,為了讓巴友們有更好的使用體驗,巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業系統版本才可使用)

face我們了解您不想看到廣告的心情⋯ 若您願意支持巴哈姆特永續經營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學】