Доработать код в генерации филвордов

код не мой, но помогите сделать чтобы при генерации филвордов отображалось номер слова и номера букв в слове. Пример на картинки. Необязательно реализовывать так, можно например вывести ввведите сюда код консоль.

введите сюда описание изображения

Генератор филвордов из текстовых файлов

Слова для примера

ШОУ ЗЕЯ ЛЕВ УДАЛИТЬ

(function() {
    const textEncoder = new TextEncoder();
    const textDecoder = new TextDecoder();

    const canvas = document.getElementById('board');
    const fileInput = document.getElementById('file');
    const regenButton = document.getElementById('regenButton');
    const nextButton = document.getElementById('nextButton');

    let currentFile = undefined;
    let currentSentence = undefined;

    let currentField = [];
    let currentWords = [];

    let currentSelectedPath = [];
    const mouseState = {
        x: 0,
        y: 0,
        pressed: false,
    };

    async function parseSentence(reader) {
        let offset = undefined;
        let allText = '';

        while (true) {
            const { value: bytes, done } = await reader.read(new Uint8Array(512));

            if (bytes) {
                allText += textDecoder.decode(bytes);

                const endRegex = /[.!?]+/;
                const match = endRegex.exec(allText);

                if (match) {
                    const neededText = allText.substring(0, match.index);

                    offset = match.index + match[0].length;
                    offset = textEncoder.encode(allText.substring(0, offset)).length;

                    allText = neededText;
                    break;
                }
            }

            if (done) {
                break;
            }
        }

        const nonAbRegex = /[^a-zа-я]+/ig;

        const result = allText.split(nonAbRegex)
            .filter(txt => txt.length > 0)
            .map(txt => txt.toUpperCase());

        return { result, offset };
    }

    async function readSentence() {
        const reader = currentFile.stream().getReader();

        const { result, offset } = await parseSentence(reader);

        reader.releaseLock();

        if (offset) {
            currentFile = currentFile.slice(offset);
        } else {
            currentFile = undefined;
        }

        return result;
    }

    function calcFieldSize(minSize) {
        let i = 0;

        for (; i * i < minSize; ++i);

        return i;
    }

    // https://stackoverflow.com/a/12646864
    function shuffleArray(array) {
        for (let i = array.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));

            [array[i], array[j]] = [array[j], array[i]];
        }
    }

    const cellPrototype = {
        edges: function() {
            return [this.top, this.left, this.right, this.bottom].filter(x => x);
        },
    };

    function createEmptyField(fieldSize) {
        const field = [];

        for (let i = 0; i < fieldSize; ++i) {
            const row = [];

            for (let j = 0; j < fieldSize; ++j) {
                const cell = {
                    idx: i * fieldSize + j,
                    char: undefined,

                    top: undefined,
                    left: undefined,
                    right: undefined,
                    bottom: undefined,

                    __proto__: cellPrototype,
                };

                if (i > 0) {
                    cell.top = field[i - 1][j];
                    field[i - 1][j].bottom = cell;
                }

                if (j > 0) {
                    cell.left = row[j - 1];
                    row[j - 1].right = cell;
                }

                row.push(cell);
            }

            field.push(row);
        }

        return field;
    }

    function getEmptyFieldComponents(field) {
        const comps = [];

        for (let i = 0; i < field.length * field.length; ++i) {
            comps.push(undefined);
        }

        function dfs(v, comp) {
            if (v.char !== undefined || comps[v.idx] !== undefined) {
                return false;
            }

            comps[v.idx] = comp;

            for (const u of v.edges()) {
                dfs(u, comp);
            }

            return true;
        }

        let c = 0;
        for (const row of field) {
            for (const cell of row) {
                if (dfs(cell, c)) {
                    ++c;
                }
            }
        }

        return [c, comps];
    }

    function checkWordFit(field, wordLength) {
        const [compsAmount, comps] = getEmptyFieldComponents(field);

        const compSizes = [];
        for (let i = 0; i < compsAmount; ++i) {
            compSizes.push(comps.filter(x => x == i).length);
        }

        if (compSizes.every(x => x < wordLength)) {
            return false;
        }

        // TODO here we assume that word fit because of checking hardness
        return true;
    }

    function placeWordInField(field, word) {
        if (!checkWordFit(field, word.length)) {
            // TODO extend

            console.log('don\'t fit!!!', field, word);
            throw new Error(':(');
        }

        const neighbours = [];
        for (const row of field) {
            for (const cell of row) {
                if (cell.char !== undefined) {
                    neighbours.push(Infinity); // ignore busy cells
                } else {
                    neighbours.push(cell.edges().filter(x => x.char === undefined).length);
                }
            }
        }

        let firstCells = [];
        for (const row of field) {
            for (const cell of row) {
                if (neighbours[cell.idx] < 4) {
                    firstCells.push(cell);
                }
            }
        }

        if (firstCells.length === 0) {
            throw new Error('wtf');
        }

        const path = [];
        function dfs(v) {
            if (path.indexOf(v) !== -1) {
                return false;
            }

            if (v.char !== undefined) {
                return false;
            }

            path.push(v);
            if (path.length === word.length) {
                return true;
            }

            const us = v.edges();

            shuffleArray(us);
            us.sort((a, b) => neighbours[a.idx] - neighbours[b.idx]);

            for (const u of us) {
                if (dfs(u)) {
                    return true;
                }
            }

            path.pop();
            return false;
        }

        shuffleArray(firstCells);
        firstCells.sort((a, b) => neighbours[a.idx] - neighbours[b.idx]);

        for (const firstCell of firstCells) {
            if (dfs(firstCell)) {
                break;
            }
        }

        if (path.length !== word.length) {
            // TODO is this could happen?
            throw new Error(':(');
        }

        const wordObject = {
            word,
            chars: [],
            guessed: false,
        };

        for (let i = 0; i < word.length; ++i) {
            const charObject = {
                value: word[i],
                word: wordObject,
                cell: path[i],
            };

            path[i].char = charObject;
            wordObject.chars.push(charObject);
        }

        for (const cell of path) {
            if (cell.char === undefined) {
                throw Error('wtf');
            }
        }

        return wordObject;
    }

    function generateField(words) {
        const wordsLength = words.map(s => s.length).reduce((a, b) => a + b, 0);
        const fieldSize = calcFieldSize(wordsLength);

        const wordsPermutation = [...words];
        shuffleArray(wordsPermutation);

        const field = createEmptyField(fieldSize);
        const placedWords = [];

        for (const word of wordsPermutation) {
            placedWords.push(placeWordInField(field, word));
        }

        console.log(field.map(row => row.map(cell =>
            cell.char ? cell.char.value : '#').join(' ')).join('\n'))

        return { field, words: placedWords };
    }

    function renderField(field, words) {
        const ctx = canvas.getContext('2d');
        const { width, height } = canvas;

        ctx.beginPath();

        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.clearRect(0, 0, width, height);

        const size = Math.min(width, height);

        let scale = 50;
        const mathScale = (size - 20) / field.length;
        if (scale > mathScale) {
            scale = mathScale;
        }

        const startX = (width - scale * field.length) / 2;
        const startY = (height - scale * field.length) / 2;

        ctx.strokeStyle = '#333';
        ctx.fillStyle = '#86dbf7';
        ctx.lineWidth = 1;

        ctx.rect(startX, startY, scale * field.length, scale * field.length);
        ctx.stroke();
        ctx.fill();

        const fontSize = Math.max(12, scale - 20);
        ctx.font = `bold ${fontSize}px sans-serif`;

        const mouseCellX = Math.floor((mouseState.x - startX) / scale);
        const mouseCellY = Math.floor((mouseState.y - startY) / scale);
        const mouseOnCell = !(mouseCellX < 0 || mouseCellX >= field.length
            || mouseCellY < 0 || mouseCellY >= field.length
            || field[mouseCellY][mouseCellX].char === undefined
            || field[mouseCellY][mouseCellX].char.word.guessed);

        const mouseCell = mouseOnCell ? field[mouseCellY][mouseCellX] : undefined;

        if (mouseOnCell) {
            canvas.style.cursor = 'pointer';
        } else {
            canvas.style.cursor = 'default';
        }

        if (mouseState.pressed) {
            if (mouseOnCell) {
                const indexInPath = currentSelectedPath.indexOf(mouseCell);

                if (indexInPath === -1) {
                    let canPush = currentSelectedPath.length === 0;

                    if (currentSelectedPath.length > 0) {
                        const lastCell = currentSelectedPath[currentSelectedPath.length - 1];

                        if (lastCell.edges().indexOf(mouseCell) !== -1) {
                            canPush = true;
                        }
                    }

                    if (canPush) {
                        currentSelectedPath.push(mouseCell);
                    }
                } else if (indexInPath === currentSelectedPath.length - 2) {
                    currentSelectedPath.pop();
                }
            }
        } else if (currentSelectedPath.length > 0) {
            const word = currentSelectedPath[0].char.word;

            if (word.chars.length === currentSelectedPath.length
             && currentSelectedPath.every((c, idx) => word.chars[idx] === c.char)) {
                word.guessed = true;

                if (currentWords.every(w => w.guessed)) {
                    generateLevel();
                }
            }

            currentSelectedPath = [];
        }

        let cellY = startY + scale / 2;

        for (const row of field) {
            let cellX = startX + scale / 2;

            for (const cell of row) {
                if (cell.char) {
                    if (cell.char.word.guessed) {
                        ctx.fillStyle = '#7cfc00';
                        ctx.fillRect(cellX - scale / 2, cellY - scale / 2, scale, scale);
                    } else if (currentSelectedPath.indexOf(cell) !== -1) {
                        ctx.fillStyle = '#ffa000';
                        ctx.fillRect(cellX - scale / 2, cellY - scale / 2, scale, scale);
                    } else if (cell === mouseCell) {
                        ctx.fillStyle = 'rgba(127, 127, 127, 0.2)';
                        ctx.fillRect(cellX - scale / 2, cellY - scale / 2, scale, scale);
                    }

                    const measure = ctx.measureText(cell.char.value);
                    const lineHeight = measure.actualBoundingBoxAscent
                        + measure.actualBoundingBoxDescent;

                    const x = cellX - measure.width / 2;
                    const y = cellY - lineHeight / 2 + measure.actualBoundingBoxAscent;

                    ctx.fillStyle = '#111';
                    ctx.fillText(cell.char.value, x, y);
                } else {
                    ctx.fillStyle = 'rgba(127, 127, 127, 0.4)';
                    ctx.fillRect(cellX - scale / 2, cellY - scale / 2, scale, scale);
                }

                cellX += scale;
            }

            cellY += scale;
        }

        ctx.beginPath();
        for (let i = 1; i < field.length; ++i) {
            ctx.moveTo(startX + i * scale, startY);
            ctx.lineTo(startX + i * scale, startY + field.length * scale);

            ctx.moveTo(startX, startY + i * scale);
            ctx.lineTo(startX + field.length * scale, startY + i * scale);
        }

        ctx.stroke();

        ctx.lineWidth = 2;
        ctx.stokeStyle = '#000';

        ctx.beginPath();
        for (const word of words) {
            if (!word.guessed) {
                continue;
            }

            for (const char of word.chars) {
                const { cell } = char;

                const j = cell.idx % field.length;
                const i = (cell.idx - j) / field.length;

                const x = startX + j * scale;
                const y = startY + i * scale;

                const edges = [
                    [cell.top, [x, y], [x + scale, y]],
                    [cell.left, [x, y], [x, y + scale]],
                    [cell.right, [x + scale, y], [x + scale, y + scale]],
                    [cell.bottom, [x, y + scale], [x + scale, y + scale]],
                ];

                for (const [v, move, line] of edges) {
                    if (v?.char === undefined || v.char.word !== cell.char.word) {
                        ctx.moveTo(...move);
                        ctx.lineTo(...line);
                    }
                }
            }
        }

        ctx.stroke();
    }

    async function generateLevel() {
        if (currentFile === undefined) {
            currentSentence = undefined;

            renderField([], []);
            currentField = [];
            currentWords = [];

            regenButton.disabled = true;
            nextButton.disabled = true;
            fileInput.value = '';
            return;
        }

        let sentence = [];

        do {
            sentence = await readSentence();
        } while (sentence.length === 0);

        currentSentence = sentence;

        // console.log(sentence);

        const { field, words } = generateField(sentence);

        renderField(field, words);
        currentField = field;
        currentWords = words;

        regenButton.disabled = false;
        nextButton.disabled = false;
    }

    function handleMouseEvent({ buttons, clientX, clientY }) {
        const boundingRect = canvas.getBoundingClientRect();
        const x = (clientX - boundingRect.left) / boundingRect.width * canvas.width;
        const y = (clientY - boundingRect.top) / boundingRect.height * canvas.height;

        mouseState.x = x;
        mouseState.y = y;
        mouseState.pressed = (buttons & 1) !== 0;

        renderField(currentField, currentWords);
    }

    canvas.onmousemove = (ev) => {
        handleMouseEvent(ev);
    };

    canvas.onmouseenter = (ev) => {
        handleMouseEvent(ev);
    };

    canvas.onmouseleave = (ev) => {
        handleMouseEvent(ev);
    };

    canvas.onmousedown = (ev) => {
        handleMouseEvent(ev);
    };

    canvas.onmouseup = (ev) => {
        handleMouseEvent(ev);
    };

    fileInput.onchange = ({ currentTarget: fileInput }) => {
        if (fileInput.files.length !== 1) {
            return;
        }

        currentFile = fileInput.files[0];

        generateLevel();
    };

    regenButton.onclick = () => {
        if (currentSentence === undefined) {
            return;
        }

        const { field, words } = generateField(currentSentence);

        renderField(field, words);
        currentField = field;
        currentWords = words;
    };

    nextButton.onclick = () => {
        generateLevel();
    };
})();
body {
  font-family: system-ui;
  background: #f06d06;
  color: white;
  text-align: center;
}
<html language="ru">
<head>
    <title>Филворды</title>

    <link rel="stylesheet" href="./style.css">
</head>

<body>
    <div class="MainPanel">
        <div class="MainPanel-File">
            Выберите текстовый файл:
        <input type="file" id="file">
        </div>

        <hr>

        <canvas id="board" width="200" height="200" class="MainPanel-Board"></canvas>

        <div class="MainPanel-Controls">
            <button type="button" id="regenButton" disabled>Заново</button>
        </div>
    </div>

    <script type="text/javascript" src="./fillwords.js"></script>
</body>
</html>


Ответы (1 шт):

Автор решения: Arbery

Особой логики не требуется - достаточно в процессе подготовки данных в паре мест индексы слова и буквы в слове дальше передать, а затем вывести.

В функции function generateField(words) {} изменить способ перебора чтобы передать дальше индекс слова:

wordsPermutation.forEach((word, idx) => {
   placedWords.push(placeWordInField(field, word, idx));
})

и дальше в функции function placeWordInField(field, word, idx) {} записать его, а так же далее при переборе и индекс буквы

const wordObject = {
    word,
    chars: [],
    guessed: false,
    indexWord: idx
};

for (let i = 0; i < word.length; ++i) {
    const charObject = {
        value: word[i],
        word: wordObject,
        cell: path[i],
        indexChar: i
    };

    path[i].char = charObject;
    wordObject.chars.push(charObject);
}

Далее в функции рендера function renderField(field, words) {} вывести переданное:

ctx.fillText(`${cell.char.value}:[${cell.char.word.indexWord+1},${cell.char.indexChar+1}]` , x, y);

Ну и чтобы это влезло в ячейки:

// const fontSize = Math.max(12, scale - 20);
const fontSize = 10;

Т.е в рабочем виде так:

(function() {
    const textEncoder = new TextEncoder();
    const textDecoder = new TextDecoder();

    const canvas = document.getElementById('board');
    const fileInput = document.getElementById('file');
    const regenButton = document.getElementById('regenButton');
    const nextButton = document.getElementById('nextButton');

    let currentFile = undefined;
    let currentSentence = undefined;

    let currentField = [];
    let currentWords = [];

    let currentSelectedPath = [];
    const mouseState = {
        x: 0,
        y: 0,
        pressed: false,
    };

    async function parseSentence(reader) {
        let offset = undefined;
        let allText = '';

        while (true) {
            const { value: bytes, done } = await reader.read(new Uint8Array(512));

            if (bytes) {
                allText += textDecoder.decode(bytes);

                const endRegex = /[.!?]+/;
                const match = endRegex.exec(allText);

                if (match) {
                    const neededText = allText.substring(0, match.index);

                    offset = match.index + match[0].length;
                    offset = textEncoder.encode(allText.substring(0, offset)).length;

                    allText = neededText;
                    break;
                }
            }

            if (done) {
                break;
            }
        }

        const nonAbRegex = /[^a-zа-я]+/ig;

        const result = allText.split(nonAbRegex)
            .filter(txt => txt.length > 0)
            .map(txt => txt.toUpperCase());

        return { result, offset };
    }

    async function readSentence() {
        const reader = currentFile.stream().getReader();

        const { result, offset } = await parseSentence(reader);

        reader.releaseLock();

        if (offset) {
            currentFile = currentFile.slice(offset);
        } else {
            currentFile = undefined;
        }

        return result;
    }

    function calcFieldSize(minSize) {
        let i = 0;

        for (; i * i < minSize; ++i);

        return i;
    }

    // https://stackoverflow.com/a/12646864
    function shuffleArray(array) {
        for (let i = array.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));

            [array[i], array[j]] = [array[j], array[i]];
        }
    }

    const cellPrototype = {
        edges: function() {
            return [this.top, this.left, this.right, this.bottom].filter(x => x);
        },
    };

    function createEmptyField(fieldSize) {
        const field = [];

        for (let i = 0; i < fieldSize; ++i) {
            const row = [];

            for (let j = 0; j < fieldSize; ++j) {
                const cell = {
                    idx: i * fieldSize + j,
                    char: undefined,

                    top: undefined,
                    left: undefined,
                    right: undefined,
                    bottom: undefined,

                    __proto__: cellPrototype,
                };

                if (i > 0) {
                    cell.top = field[i - 1][j];
                    field[i - 1][j].bottom = cell;
                }

                if (j > 0) {
                    cell.left = row[j - 1];
                    row[j - 1].right = cell;
                }

                row.push(cell);
            }

            field.push(row);
        }

        return field;
    }

    function getEmptyFieldComponents(field) {
        const comps = [];

        for (let i = 0; i < field.length * field.length; ++i) {
            comps.push(undefined);
        }

        function dfs(v, comp) {
            if (v.char !== undefined || comps[v.idx] !== undefined) {
                return false;
            }

            comps[v.idx] = comp;

            for (const u of v.edges()) {
                dfs(u, comp);
            }

            return true;
        }

        let c = 0;
        for (const row of field) {
            for (const cell of row) {
                if (dfs(cell, c)) {
                    ++c;
                }
            }
        }

        return [c, comps];
    }

    function checkWordFit(field, wordLength) {
        const [compsAmount, comps] = getEmptyFieldComponents(field);

        const compSizes = [];
        for (let i = 0; i < compsAmount; ++i) {
            compSizes.push(comps.filter(x => x == i).length);
        }

        if (compSizes.every(x => x < wordLength)) {
            return false;
        }

        // TODO here we assume that word fit because of checking hardness
        return true;
    }

    function placeWordInField(field, word, idx) {
        if (!checkWordFit(field, word.length)) {
            // TODO extend

            console.log('don\'t fit!!!', field, word);
            throw new Error(':(');
        }

        const neighbours = [];
        for (const row of field) {
            for (const cell of row) {
                if (cell.char !== undefined) {
                    neighbours.push(Infinity); // ignore busy cells
                } else {
                    neighbours.push(cell.edges().filter(x => x.char === undefined).length);
                }
            }
        }

        let firstCells = [];
        for (const row of field) {
            for (const cell of row) {
                if (neighbours[cell.idx] < 4) {
                    firstCells.push(cell);
                }
            }
        }

        if (firstCells.length === 0) {
            throw new Error('wtf');
        }

        const path = [];
        function dfs(v) {
            if (path.indexOf(v) !== -1) {
                return false;
            }

            if (v.char !== undefined) {
                return false;
            }

            path.push(v);
            if (path.length === word.length) {
                return true;
            }

            const us = v.edges();

            shuffleArray(us);
            us.sort((a, b) => neighbours[a.idx] - neighbours[b.idx]);

            for (const u of us) {
                if (dfs(u)) {
                    return true;
                }
            }

            path.pop();
            return false;
        }

        shuffleArray(firstCells);
        firstCells.sort((a, b) => neighbours[a.idx] - neighbours[b.idx]);

        for (const firstCell of firstCells) {
            if (dfs(firstCell)) {
                break;
            }
        }

        if (path.length !== word.length) {
            // TODO is this could happen?
            throw new Error(':(');
        }

        const wordObject = {
            word,
            chars: [],
            guessed: false,
            indexWord: idx
        };

        for (let i = 0; i < word.length; ++i) {
            const charObject = {
                value: word[i],
                word: wordObject,
                cell: path[i],
                indexChar: i
            };

            path[i].char = charObject;
            wordObject.chars.push(charObject);
        }

        for (const cell of path) {
            if (cell.char === undefined) {
                throw Error('wtf');
            }
        }

        return wordObject;
    }

    function generateField(words) {
        const wordsLength = words.map(s => s.length).reduce((a, b) => a + b, 0);
        const fieldSize = calcFieldSize(wordsLength);
      
        const wordsPermutation = [...words];
        shuffleArray(wordsPermutation);

        const field = createEmptyField(fieldSize);
        const placedWords = [];


        wordsPermutation.forEach((word, idx) => {
            placedWords.push(placeWordInField(field, word, idx));
        })

        console.log(field.map(row => row.map(cell =>
            cell.char ? cell.char.value : '#').join(' ')).join('\n'))
      

        return { field, words: placedWords };
    }

    function renderField(field, words) {
        const ctx = canvas.getContext('2d');
        const { width, height } = canvas;

        ctx.beginPath();

        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.clearRect(0, 0, width, height);

        const size = Math.min(width, height);

        let scale = 50;
        const mathScale = (size - 20) / field.length;
        if (scale > mathScale) {
            scale = mathScale;
        }

        const startX = (width - scale * field.length) / 2;
        const startY = (height - scale * field.length) / 2;

        ctx.strokeStyle = '#333';
        ctx.fillStyle = '#86dbf7';
        ctx.lineWidth = 1;

        ctx.rect(startX, startY, scale * field.length, scale * field.length);
        ctx.stroke();
        ctx.fill();

        // const fontSize = Math.max(12, scale - 20);
        const fontSize = 10;
        ctx.font = `bold ${fontSize}px sans-serif`;

        const mouseCellX = Math.floor((mouseState.x - startX) / scale);
        const mouseCellY = Math.floor((mouseState.y - startY) / scale);
        const mouseOnCell = !(mouseCellX < 0 || mouseCellX >= field.length
            || mouseCellY < 0 || mouseCellY >= field.length
            || field[mouseCellY][mouseCellX].char === undefined
            || field[mouseCellY][mouseCellX].char.word.guessed);

        const mouseCell = mouseOnCell ? field[mouseCellY][mouseCellX] : undefined;

        if (mouseOnCell) {
            canvas.style.cursor = 'pointer';
        } else {
            canvas.style.cursor = 'default';
        }

        if (mouseState.pressed) {
            if (mouseOnCell) {
                const indexInPath = currentSelectedPath.indexOf(mouseCell);

                if (indexInPath === -1) {
                    let canPush = currentSelectedPath.length === 0;

                    if (currentSelectedPath.length > 0) {
                        const lastCell = currentSelectedPath[currentSelectedPath.length - 1];

                        if (lastCell.edges().indexOf(mouseCell) !== -1) {
                            canPush = true;
                        }
                    }

                    if (canPush) {
                        currentSelectedPath.push(mouseCell);
                    }
                } else if (indexInPath === currentSelectedPath.length - 2) {
                    currentSelectedPath.pop();
                }
            }
        } else if (currentSelectedPath.length > 0) {
            const word = currentSelectedPath[0].char.word;

            if (word.chars.length === currentSelectedPath.length
             && currentSelectedPath.every((c, idx) => word.chars[idx] === c.char)) {
                word.guessed = true;

                if (currentWords.every(w => w.guessed)) {
                    generateLevel();
                }
            }

            currentSelectedPath = [];
        }

        let cellY = startY + scale / 2;

        for (const row of field) {
            let cellX = startX + scale / 2;

            for (const cell of row) {
                if (cell.char) {
                    if (cell.char.word.guessed) {
                        ctx.fillStyle = '#7cfc00';
                        ctx.fillRect(cellX - scale / 2, cellY - scale / 2, scale, scale);
                    } else if (currentSelectedPath.indexOf(cell) !== -1) {
                        ctx.fillStyle = '#ffa000';
                        ctx.fillRect(cellX - scale / 2, cellY - scale / 2, scale, scale);
                    } else if (cell === mouseCell) {
                        ctx.fillStyle = 'rgba(127, 127, 127, 0.2)';
                        ctx.fillRect(cellX - scale / 2, cellY - scale / 2, scale, scale);
                    }

                    const measure = ctx.measureText(cell.char.value);
                    const lineHeight = measure.actualBoundingBoxAscent
                        + measure.actualBoundingBoxDescent;

                    const x = cellX - measure.width / 2;
                    const y = cellY - lineHeight / 2 + measure.actualBoundingBoxAscent;

                    ctx.fillStyle = '#111';
                    ctx.fillText(`${cell.char.value}:[${cell.char.word.indexWord+1},${cell.char.indexChar+1}]` , x, y);
                } else {
                    ctx.fillStyle = 'rgba(127, 127, 127, 0.4)';
                    ctx.fillRect(cellX - scale / 2, cellY - scale / 2, scale, scale);
                }

                cellX += scale;
            }

            cellY += scale;
        }

        ctx.beginPath();
        for (let i = 1; i < field.length; ++i) {
            ctx.moveTo(startX + i * scale, startY);
            ctx.lineTo(startX + i * scale, startY + field.length * scale);

            ctx.moveTo(startX, startY + i * scale);
            ctx.lineTo(startX + field.length * scale, startY + i * scale);
        }

        ctx.stroke();

        ctx.lineWidth = 2;
        ctx.stokeStyle = '#000';

        ctx.beginPath();
        for (const word of words) {
            if (!word.guessed) {
                continue;
            }

            for (const char of word.chars) {
                const { cell } = char;

                const j = cell.idx % field.length;
                const i = (cell.idx - j) / field.length;

                const x = startX + j * scale;
                const y = startY + i * scale;

                const edges = [
                    [cell.top, [x, y], [x + scale, y]],
                    [cell.left, [x, y], [x, y + scale]],
                    [cell.right, [x + scale, y], [x + scale, y + scale]],
                    [cell.bottom, [x, y + scale], [x + scale, y + scale]],
                ];

                for (const [v, move, line] of edges) {
                    if (v?.char === undefined || v.char.word !== cell.char.word) {
                        ctx.moveTo(...move);
                        ctx.lineTo(...line);
                    }
                }
            }
        }

        ctx.stroke();
    }

    async function generateLevel() {
        if (currentFile === undefined) {
            currentSentence = undefined;

            renderField([], []);
            currentField = [];
            currentWords = [];

            regenButton.disabled = true;
            nextButton.disabled = true;
            fileInput.value = '';
            return;
        }

        let sentence = [];

        do {
            sentence = await readSentence();
        } while (sentence.length === 0);

        currentSentence = sentence;


        const { field, words } = generateField(sentence);

        renderField(field, words);
        currentField = field;
        currentWords = words;

        regenButton.disabled = false;
        nextButton.disabled = false;
    }

    function handleMouseEvent({ buttons, clientX, clientY }) {
        const boundingRect = canvas.getBoundingClientRect();
        const x = (clientX - boundingRect.left) / boundingRect.width * canvas.width;
        const y = (clientY - boundingRect.top) / boundingRect.height * canvas.height;

        mouseState.x = x;
        mouseState.y = y;
        mouseState.pressed = (buttons & 1) !== 0;

        renderField(currentField, currentWords);
    }

    canvas.onmousemove = (ev) => {
        handleMouseEvent(ev);
    };

    canvas.onmouseenter = (ev) => {
        handleMouseEvent(ev);
    };

    canvas.onmouseleave = (ev) => {
        handleMouseEvent(ev);
    };

    canvas.onmousedown = (ev) => {
        handleMouseEvent(ev);
    };

    canvas.onmouseup = (ev) => {
        handleMouseEvent(ev);
    };

    fileInput.onchange = ({ currentTarget: fileInput }) => {
        if (fileInput.files.length !== 1) {
            return;
        }

        currentFile = fileInput.files[0];

        generateLevel();
    };

    regenButton.onclick = () => {
        if (currentSentence === undefined) {
            return;
        }
        const { field, words } = generateField(currentSentence);

        renderField(field, words);
        currentField = field;
        currentWords = words;
    };

    nextButton.onclick = () => {
        generateLevel();
    };
})();
body {
  font-family: system-ui;
  background: #f06d06;
  color: white;
  text-align: center;
}
<html language="ru">

<head>
  <title>Филворды</title>

  <link rel="stylesheet" href="./style.css">
</head>

<body>
  <div class="MainPanel">
    <div class="MainPanel-File">
      Выберите текстовый файл:
      <input type="file" id="file">
    </div>

    <hr>

    <canvas id="board" width="200" height="200" class="MainPanel-Board"></canvas>

    <button id="nextButton">Далее</button>
    <div class="MainPanel-Controls">
      <button type="button" id="regenButton" disabled>Заново</button>
    </div>
  </div>

  <script type="text/javascript" src="./fillwords.js"></script>
</body>

</html>

→ Ссылка