Od Wordla do 64rdla: Kako zgraditi svojo različico legendarne igre (2. del)

27.07.2026

V drugem delu tega članka bomo osnovno različico igre Wordle nadgradili v izziv 64rdle, kjer igralec hkrati rešuje kar 64 iskanih besed. Naučili se boste, kako ustrezno prilagoditi podatkovno strukturo in nadgraditi uporabniški vmesnik za sočasni prikaz vseh igralnih plošč. Za tekočo uporabniško izkušnjo bomo rešili težave s padcem hitrosti delovanja, pri čemer bomo uporabili napredno virtualizacijo seznama s knjižnico TanStack Virtual.

V prejšnjem članku smo izdelali svojo različico igre Wordle. Tokrat bomo projekt nadgradili v 64rdle, kjer igralec hkrati rešuje kar 64 različnih Wordlov.

Priprava podatkov

Najprej moramo prilagoditi podatke, da bodo hranili vseh 64 iskanih besed.
Spremenljivko todaysWord preimenujemo v todaysWords in inicializacijo spremenimo tako:

const [ todaysWords ] = useState<string[]>(() => {
        let wordSet = new Set<string>();
        let words : string[] = [];
        let rnd = RandomInt(seed);
        while(words.length < 64) {
            let word = answerWords[rnd() % answerWords.length];
            if(wordSet.has(word)) continue;
            wordSet.add(word);
            words.push(word);
        }
        return words;
});

S tem zagotovimo, da dobimo 64 različnih naključno izbranih besed.

Izris 64 igralnih plošč

CSS razred gametable preimenujemo v game in v našem css dodamo:

.gametable {
    box-sizing: border-box;
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    max-height: calc(100vh - 62px - 120px - 50px);
    overflow: auto;
    margin-top: 25px;
}
.game {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
    gap: 0px;
    max-width: 340px;
    > div {
        box-sizing: border-box;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 28px;
        height: 28px;
        font-size: 100%;
        text-transform: uppercase;
        text-align: center;
        border: 2px solid lightgrey;
        &:empty {
            border: 2px solid grey;
        }
        &:not(:empty):has(~ div:empty) {
            animation: bounce 0.25s;
        }
        &.correct {
            background-color: green;
        }
        &.missplaced {
            background-color: orange;
        }
    }
}

Nato v datoteki App.tsx spremenimo logiko za izris igre. Namesto ene igralne plošče bomo zdaj izrisali vseh 64.
<div className='gametable'>
            {
                todaysWords.map(gameWord => {
                    let wordCC: { [key: string]: number } = {}; // todays character counter
                    for (let char of gameWord) {
                        wordCC[char] = wordCC[char] && wordCC[char] + 1 || 1;
                    }
                    return <>
                        <div className='game'>
                            {
                                Array.from({length: 70}, (_, i) => i).map((i) => {
                                    let tmpWord;
                                    if (i < history.length) {
                                        tmpWord = history[i];
                                        let tmpTodaysCC = { ...wordCC };
                                        for (let j = 0; j < 5; j++) {
                                            let char = tmpWord[j];
                                            if (gameWord[j] === char) {
                                                let num = (tmpTodaysCC[char] || 0) - 1;
                                                tmpTodaysCC[char] = num;
                                            }
                                        }
                                        return [...tmpWord].map((char, j) => {
                                            let className = '';
                                            if (char === gameWord[j]) className = 'correct';
                                            else if (gameWord.indexOf(char) >= 0 && tmpTodaysCC[char] > 0) className = 'missplaced'
                                            return <div className={className}>{char}</div>;
                                        });
                                    }
                                    else if (i == history.length) {
                                        tmpWord = word;
                                        return [...tmpWord, null, null, null, null, null].map((char, j) => {
                                            if (j >= 5) return;
                                            if (!char) return <div></div>;
                                            return <div>{char}</div>;
                                        })
                                    }
                                    return <>
                                        <div></div>
                                        <div></div>
                                        <div></div>
                                        <div></div>
                                        <div></div>
                                    </>;
                                })
                            }
                        </div>
                    </>
                })
            }
        </div>

Ko je igre konec moramo zamenjati pogoj za zmago in sicer s to linijo kode
todaysWords.filter((word) => history.includes(word)).length == 64
Za preverjanje konca igre pa 
const gameFinished = useMemo(() => history.length >= 70 || history.length > 0 && todaysWords.filter((word) => history.includes(word)).length == 64, [history]);

64rdle končan

Zdaj smo naredili 64rdle in ga lahko igramo, a pride do manjšega problema. Hitrost delovanja igre ni sprejemljiva, saj za vsak klik na tipkovnici mora posodobiti vseh 64 iger, 4480 vrstic, 22400 polj. Ta problem bomo rešili z virtualizacijo seznama. V tem projektu bom uporabil TanStack Virtual za virtualizacijo.

Virtualizacija seznama

Pred return dodamo
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
    count: 8,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 2361
});
rowVirtaulizer bo virtualiziral vrste, ker vemo, da bo 8 vrst, mu lahko to tudi povemo. Damo mu tudi reference na element, na katerem bo scroll in estimateSize, ki je višina elementov znotraj.

gametable elementu dodamo ref
<div 
            ref={parentRef}
            className='gametable'>

zdaj pa objamemo celo logiko za izris posameznih iger v virtualiziran seznam
<div
    style={{
         height: `${rowVirtualizer.getTotalSize()}px`,
          width: `100%`,
          position: 'relative',
      }}
>{
    rowVirtualizer.getVirtualItems().map((virtualRow) => (
        <div
            key={virtualRow.key}
            data-index={virtualRow.index}
            id={`tableRow_${virtualRow.key}`}
            className={`tableRow ${virtualRow.index == 0 && `first` || ``}`}
            ref={rowVirtualizer.measureElement}
            style={{
                display: 'inline-flex',
                position: 'absolute',
                top: 0,
                left: 0,
                width: `100%`,
                transform: `translateY(${virtualRow.start || 0}px)`,
                gap: '5px',
                justifyContent: 'center',
                padding: '5px 0px'
            }}
        >{
            [0, 1, 2, 3, 4, 5, 6, 7].map((virtualColumn) => {
                let index = (virtualRow.key as number) * 8 + virtualColumn;
                if (index >= todaysWords.length) return;
                    const gameWord = todaysWords[index];
                    let wordCC: { [key: string]: number } = {}; // todays character counter
                    for (let char of gameWord) {
                        wordCC[char] = wordCC[char] && wordCC[char] + 1 || 1;
                    }
                    let wasSolved = false;
                    return <>
                        <div>
                            <h2>{index}</h2>
                            <div className='game'>{
                                Array.from({length: 70}, (_, i) => i).map((i) => {
                                    let tmpWord;
                                    if(wasSolved) return null;
                                    if (i < history.length) {
                                        tmpWord = history[i];
                                        let tmpTodaysCC = { ...wordCC };
                                        for (let j = 0; j < 5; j++) {
                                            let char = tmpWord[j];
                                            if (gameWord[j] === char) {
                                                let num = (tmpTodaysCC[char] || 0) - 1;
                                                tmpTodaysCC[char] = num;
                                            }
                                        }
                                        wasSolved = tmpWord == gameWord;
                                        return [...tmpWord].map((char, j) => {
                                            let className = '';
                                            if (char === gameWord[j]) className = 'correct';
                                            else if (gameWord.indexOf(char) >= 0 && tmpTodaysCC[char] > 0) className = 'missplaced';
                                            return <div className={className}>{char}</div>;
                                        });
                                    }
                                    else if (i == history.length) {
                                        tmpWord = word;
                                        return [...tmpWord, null, null, null, null, null].map((char, j) => {
                                            if (j >= 5) return;
                                            if (!char) return <div></div>;
                                            return <div>{char}</div>;
                                        })
                                    }
                                    return <>
                                        <div></div>
                                        <div></div>
                                        <div></div>
                                        <div></div>
                                        <div></div>
                                    </>;
                                })
                            }
                        </div>
                    </div>
                </>
            })
        }
 </div>))
}
</div>

Za preverjanje zmage predstavimo novo spremenljivo ValidGuesses, ki sledi pravilnim ugibanjem

const [validGuesses, setValidGuesses] = useState<Set<string>>(() => {
    let validGuesses = new Set<string>();
    for(let word of history) {
        if(todaysWords.includes(word)) validGuesses.add(word);
    }
    return validGuesses;
});

V submitWord dodamo

if(todaysWords.includes(word)) {
    let newValidGuess = new Set(validGuesses);
    newValidGuess.add(word);
    setValidGuesses(newValidGuess);
}

Zares konec

Sedaj smo končali z 64dle. Če želite še kakšen izziv lahko naredite, da na tipkovnici prikazujete stanja iger, na primer vsako črko na tipkovnico razdelite na 64 kvadratkov in potem pobarvate glede na stanje posamične igre. Če želite igralcem pri igri pomagati, lahko naredite, ko ima določena iskana beseda najdene črke na 4 mestih in ima igralec eno črko na napačnem mestu, se le ta označi z rdečo barvo.

Končni projekt lahko najdete tukaj (Xnet/64rdle at master · LenartSvetek/Xnet).

Lenart Svetek
Lenart Svetek
Front - end programer, študent
lenart.svetek@kompas-xnet.si

Imate dodatna vprašanja?

Za več informacij smo vam vedno z veseljem na voljo. Pišite nam na info@kompas-xnet.si ali nas pokličite 01 5136 990.

Kontaktirajte nas

Novice

Naročite se na Xnet novice in ostanite na tekočem glede novih tečajev, seminarjev, možnosti pridobitve novih certificiranj in akcijskih cen.

Katero področje novic vas zanima?

Potrebuješ pomoč? bot icon
Potrebuješ pomoč?