PageSourceSearch

https://ball-labyrinth-game.web.app/js/vars/game.js

js ball-labyrinth-game.web.app collected 2026-10-03 12:18:02 UTC 28,233 bytes, 794 lines download raw bytes

1var game = {
2    boardContentId: '',
3    testing: false,
4    showHint: false,
5    currentHint: {},
6    dimention: 0,
7    cellSize: 0,
8    levelInfo: {},
9    level: {},
10    levelProgress: {},
11    mooves: [],
12    balls: [],
13    newBalls: [],
14    gameInterval: {},
15    frameInterval: 0,
16    frame: 0,
17    speed: 1,
18    lastRun: 0,
19
20    //cellMove: null,
21    minCellMove: 10,
22
23    distance: function () {
24        return game.frame * game.speed / game.cellSize
25    },
26
27    play: function(showHint = false) {
28        state.refreshLastPlayed();
29
30        if (state.firstPackagePlay() && !state.inductions['p' + state.package.current]) {
31            const inductionShown = inductionScreen.showPackageInduction(state.package.current)
32
33            if(inductionShown) {
34                state.inductions['p' + state.package.current] = true
35                return
36            }
37        }
38
39        game.fillTheBoard(showHint);
40    },
41
42    fillTheBoard: function(showHint = false) {
43        get('boardScreen').classList.remove(INDUCTION)
44
45        let newLevelInfo = new LevelInfo(state.mode.current, state.package.current, state.level.current);
46
47        if (get(BOARD_CONTENT_ID) && newLevelInfo.equal(game.levelInfo)) {
48            newLevelInfo = null
49            game.levelInfo = {};
50            game.animateCellsReset();
51            setTimeout(game.fillTheBoard, 200, showHint);
52            return;
53        } else {
54            game.levelInfo = newLevelInfo
55            newLevelInfo = null
56        }
57
58        let gameLevel = levels[state.mode.current][state.package.current].levels[state.level.current];
59        game.createBoard('board', BOARD_CONTENT_ID, gameLevel, game.cellMouseDown, showHint)
60        gameLevel = null
61
62        game.clearBalls();
63        game.addBall();
64    },
65
66    createBoard: function(parentId, id, lvl, ceilClickListener, showHint=false) {
67        game.dimention = lvl.map.length;
68        game.showHint = showHint;
69        game.hintPause = false;
70        game.boardContentId = id;
71        game.ceilClickListener = ceilClickListener
72
73        rm(HINT);
74        game.clearBoard(id);
75
76        let parent = get(parentId);
77        const {boardTop, boardLeft, boardSize} = this.getBoardSize(parent);
78
79        let boardContent  = document.createElement('div');
80        boardContent.id = id;
81        boardContent.style.width = boardContent.style.height = boardSize + "px";
82        boardContent.style.marginTop = boardTop + "px";
83        boardContent.style.marginLeft = boardLeft + "px";
84        boardContent.setAttribute('class', 'boardTable');
85
86        game.level = clone(lvl);
87        game.levelProgress = new LevelProgress(state.mode.current, state.package.current, state.level.current, game.level.solution, state.currentLevelBestMoves());
88        game.mooves = [];
89        game.frame = 0;
90        game.frameInterval = 0;
91
92        game.cellSize = Math.round(boardSize / game.dimention)
93        game.speed = round(game.cellSize / GAME_CEIL_DIVIDER_FOR_SPEED)
94
95        get('boardAdvice').style.height = (game.cellSize + boardTop) * 0.6 + "px"
96
97        for(let i = 0; i < game.dimention; i++){
98            for(let j = 0; j < game.dimention; j++){
99                let cellId = "cell-" + i + "-" + j;
100                rm(cellId);
101
102                let parentDiv = document.createElement("div");
103                parentDiv.id = cellId;
104                parentDiv.style.width = (game.cellSize-1) + "px";
105                parentDiv.style.height = (game.cellSize-1) + "px";
106                parentDiv.topLeft(i * game.cellSize, j * game.cellSize);
107                parentDiv.setAttribute('row', i);
108                parentDiv.setAttribute('col', j);
109                parentDiv.setAttribute('class', game.level.map[i][j]);
110                parentDiv.mousedown(ceilClickListener);
111
112                let contentDiv = document.createElement("div");
113                contentDiv.setAttribute('class', 'content');
114
115                let subContentDiv = document.createElement("div");
116                subContentDiv.setAttribute('class', 'subContent');
117
118                let fixedDiv = document.createElement("div");
119                fixedDiv.setAttribute('class', 'fixed');
120
121                subContentDiv.appendChild(fixedDiv);
122                contentDiv.appendChild(subContentDiv);
123                parentDiv.appendChild(contentDiv);
124                boardContent.appendChild(parentDiv);
125            }
126        }
127
128        parent.appendChild(boardContent);
129
130        setTimeout(fireEvent, 0, GAME_STATE_CHANGE, {status: READY});
131
132        game.showNextHint();
133    },
134
135    clearBoard: function(boardId) {
136        let boardContent = get(boardId)
137
138        if (!boardContent) return
139
140        let parentDivs = boardContent.childNodes;
141        for (let i = 0; i < parentDivs.length; i++) {
142            parentDivs[i].removeMousedown(game.ceilClickListener)
143            boardContent.removeChild(parentDivs[i])
144        }
145
146        rm(boardId)
147    },
148
149    getBoardSize: function(parent) {
150        let parentW = parent.clientWidth;
151        let parentH = window.innerHeight - get('boardTop').clientHeight - get('boardFooter').clientHeight;
152        let boardTop = 0;
153        let boardLeft = 0;
154        let boardSize = 0;
155
156        if (parentH === parentW) boardSize = parentW;
157        else if (parentW > parentH) {
158            boardSize = parentH;
159            boardLeft = Math.floor((parentW - parentH) / 2);
160        } else {
161            boardSize = parentW;
162            boardTop = Math.floor((parentH - parentW) / 2);
163        }
164
165        return {boardTop, boardLeft, boardSize}
166    },
167
168    showNextHint: function () {
169        if (!game.showHint) return;
170
171        if (game.hintPause) {
172            game.hintPause = false;
173            fireEvent(SOUND, {sound: BALL_ROLL_SOUND});
174        }
175
176        game.currentHint = game.levelProgress.nextHint();
177        if (!game.currentHint) {
178            rm(HINT);
179            return;
180        }
181
182        // add new one
183        let hintDiv = get(HINT);
184        if (!hintDiv) {
185            hintDiv = document.createElement("div");
186            hintDiv.id = HINT;
187            hintDiv.innerHTML = HINT_HTML;
188            hintDiv.style.width = (game.cellSize - 1) + "px";
189            hintDiv.style.height = (game.cellSize - 1) + "px";
190            get('boardScreen').appendChild(hintDiv);
191        }
192
193        hintDiv.topLeft(game.currentHint.top, game.currentHint.left);
194
195        get(HINT).setAttribute('class', game.currentHint.distance > 0 ? HIDDEN : '');
196        setTimeout((cls)=>{get(HINT).classList.add(cls)}, 500, game.currentHint.type);
197    },
198
199    refreshBoard: function() {
200        for(let i = 0; i < game.dimention; i++){
201            for(let j = 0; j < game.dimention; j++){
202
203                let parentDiv = getCell(i, j);
204                parentDiv.topLeft(i * game.cellSize, j * game.cellSize)
205                parentDiv.setAttribute('class', game.level.map[i][j]);
206            }
207        }
208
209        get(game.boardContentId).classList.remove(MOVE_IN_PROGRESS)
210    },
211
212    addBall: function(ball) {
213        if (!ball) {
214            ball = new Ball(BOARD_CONTENT_ID, "ball", RED, game.cellSize, game.cellSize, game.cellSize, RIGHT, game.speed);
215        }
216
217        game.newBalls.unshift(ball);
218    },
219
220    clearBalls: function() {
221        for (let i=0; i<game.balls.length; i++) {
222            rm(game.balls[i].id);
223        }
224
225        for (let j=0; j<game.newBalls.length; j++) {
226            rm(game.newBalls[j].id);
227        }
228
229        game.balls = new Array();
230        game.newBalls = new Array();
231    },
232
233    startOrSpeedUp: function() {
234        if (game.frameInterval === 0) {
235            // start
236            game.start(NORMAL_GAME_INTERVAL);
237
238            fireEvent(SOUND, {sound: BALL_ROLL_SOUND});
239            fireEvent(GAME_STATE_CHANGE, {status: STARTED});
240        } else if (game.frameInterval > FAST_GAME_INTERVAL) {
241            // speed up
242            game.setGameInterval(FAST_GAME_INTERVAL);
243
244            fireEvent(SOUND, {sound: BALL_ROLL_FAST_SOUND});
245            fireEvent(GAME_STATE_CHANGE, {status: FAST});
246        }
247    },
248
249    start: function(interval) {
250        game.setGameInterval(interval);
251        game.showNextHint();
252    },
253
254    setGameInterval: function (interval) {
255        if (game.gameInterval) cancelAnimationFrame(game.gameInterval);
256        game.frameInterval = interval;
257        game.lastRun = 0
258        game.gameInterval = requestAnimationFrame(game.gameFrame);
259    },
260
261    pauseResume: function () {
262        if (game.gameInterval) {
263            cancelAnimationFrame(game.gameInterval)
264            game.gameInterval = 0;
265            get('boardScreen').classList.add(PAUSED);
266            fireEvent(VOLUME_CHANGE, {volume: 0});
267        } else {
268            game.gameInterval = requestAnimationFrame(game.gameFrame);
269            get('boardScreen').classList.remove(PAUSED);
270            fireEvent(VOLUME_CHANGE, {volume: 1});
271        }
272    },
273
274    gameFrame: function (timestamp) {
275        game.gameInterval = requestAnimationFrame(game.gameFrame);
276
277        if (timestamp < (game.lastRun + game.frameInterval)) {
278            return
279        } else {
280            game.lastRun = timestamp
281        }
282
283        if (game.hintPause) {
284            return;
285        }
286
287        if (game.showHint && game.currentHint && game.currentHint.distance > 0 && game.currentHint.distance <= game.distance()) {
288            get(HINT).classList.remove(HIDDEN);
289            game.hintPause = true;
290
291            fireEvent(SOUND_OFF, {sound: BALL_ROLL_SOUND});
292
293            return;
294        }
295
296        game.frame++;
297
298        for(let b=0; b<game.balls.length; b++) {
299            if (game.balls[b].mooving) {
300                let continueLoop = game.balls[b].moove();
301                if (!continueLoop) break;
302            }
303        }
304
305        if (game.newBalls.length) {
306            for (let i=0; i<game.newBalls.length; i++) {
307                game.newBalls[i].show()
308                game.balls.unshift(game.newBalls[i])
309
310                setTimeout((b)=>{
311                    b.moove()
312                }, 20, game.newBalls[i])
313            }
314            game.newBalls = []
315        }
316    },
317
318    ballInPlace: function(row, col) {
319        fireEvent(SOUND, {sound: BALL_IN_PLACE_SOUND});
320
321        // mark ceil as achieved
322        game.level.map[row][col] += ' ' + ACHIEVED
323        getCell(row, col).classList.add(ACHIEVED)
324
325        for (let i=0; i<game.balls.length; i++) {
326            // if there are at least one mooving - continue
327            if (game.balls[i].mooving) return;
328        }
329
330        // otherwise - game over successfully
331        game.over(true);
332    },
333
334    getSecondTeleportPosition: function (ceil) {
335        const row = +ceil.getAttribute('row')
336        const col = +ceil.getAttribute('col')
337
338        for(let i = 0; i < game.dimention; i++){
339            for(let j = 0; j < game.dimention; j++){
340                if (game.level.map[i][j].indexOf(TELEPORT) >= 0 && !(i === row && j === col)) {
341                    return {row: i, col: j}
342                }
343            }
344        }
345
346        return {row, col}
347    },
348
349    screenStateChange: function(event) {
350        let oldState = get('boardScreen').getAttribute('state')
351        let newState = event.detail.status;
352
353        if (oldState) {
354            get('boardScreen').classList.remove(oldState)
355        }
356
357        if (newState) {
358            get('boardScreen').setAttribute('state', newState)
359            get('boardScreen').classList.add(newState)
360        }
361
362        app.log('state:' + oldState + ' -> ' + newState)
363    },
364
365    over: function(success) {
366        cancelAnimationFrame(game.gameInterval)
367
368        get(game.boardContentId).classList.add(success ? FINISHED : FAILED)
369
370        fireEvent(SOUND, {sound: success ? LEVEL_COMPLETED_SOUND : LEVEL_FAILED_SOUND});
371
372        setTimeout(fireEvent, 1000, GAME_OVER, {success: success, time: new Date()});
373    },
374
375    overHandler: function(e) {
376        rm(HINT);
377
378        let lvlState = game.levelProgress.getLevelState(true);
379
380        if (e.detail.success) {
381            const awardState = state.levelAwardState(lvlState)
382            const starsBefore = state.userStars();
383            const solution = state.levelSolved(lvlState);
384
385            boardScreen.overHandler(e)
386            boardScreen.showEarnings(starsBefore, solution, awardState, lvlState);
387        } else {
388            boardScreen.overHandler(e)
389        }
390    },
391
392    starCollectHandler: function(e) {
393        game.levelProgress.starCollected();
394    },
395
396    boardMoveHandler: function(e) {
397        game.mooves.push(e.detail);
398        game.levelProgress.nextMove();
399    },
400
401    cellMouseDown: function(e) {
402        let cell = e.currentTarget;
403
404        if (!cell.classList.contains(CELL) || cell.classList.contains(FIXED)) return;
405
406        let data = e.touches ? e.touches[0] : e;
407
408        game.cellMovement = new CellMove(
409            +cell.getAttribute("row"),
410            +cell.getAttribute("col"),
411            data.screenX,
412            data.screenY,
413            game.distance()
414        );
415
416        get('boardScreen').mousemove(game.cellMouseMove);
417        get('boardScreen').mouseup(game.cellMouseUp);
418
419        e.preventDefault();
420        return false;
421    },
422
423    cellMouseMove: function(e) {
424        let data = e.touches ? e.touches[0] : e;
425
426        let deltaX = data.screenX - game.cellMovement.startX;
427        let deltaY = data.screenY - game.cellMovement.startY;
428
429        if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > game.minCellMove) {
430            game.cellMovement.direction = deltaX > 0 ? RIGHT : LEFT;
431            game.cellMove();
432        } else if (Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaY) > game.minCellMove) {
433            game.cellMovement.direction = deltaY > 0 ? DOWN : UP;
434            game.cellMove();
435        }
436    },
437
438    cellMouseUp: function(e) {
439        get('boardScreen').removeMousemove(game.cellMouseMove);
440        get('boardScreen').removeMouseup(game.cellMouseUp);
441    },
442
443    cellMove: function() {
444        game.cellMouseUp();
445
446        const deniedCeils = game.ballAffected(game.cellMovement) || game.noPlaceToMove(game.cellMovement)
447        if (deniedCeils) {
448            if (Array.isArray(deniedCeils)) {
449                game.highlightDeniedMovement(deniedCeils)
450
451                if (deniedCeils.length == 1 && deniedCeils[0].classList.contains('fixed')) {
452                    setTimeout(boardScreen.showAdvice, 0, translation('fixedAdvice'))
453                } else {
454                    setTimeout(boardScreen.showAdvice, 0, translation('ballAdvice'))
455                }
456            }
457            return;
458        }
459
460        fireEvent(SOUND, {sound: CEIL_MOVE_SOUND});
461        game.animateCellsMove(game.cellMovement);
462
463        let movementIsRight = true;
464
465        // in case hints are turned on, user should move the right cell
466        if (game.showHint && game.currentHint) {
467            movementIsRight = (
468                (game.cellMovement.vertical() && game.cellMovement.col === game.currentHint.col) ||
469                (game.cellMovement.horizontal() && game.cellMovement.row === game.currentHint.row)
470            ) && game.cellMovement.direction === game.currentHint.type;
471        }
472
473        if (movementIsRight) {
474            setTimeout(game.rearrangeCellsAfterAnimation, 200);
475        } else {
476            setTimeout(game.refreshBoard, 200);
477        }
478    },
479
480    highlightDeniedMovement: function(ceils) {
481        fireEvent(SOUND, {sound: DENIED_SOUND});
482
483        ceils.map(ceil => ceil.classList.add(MOVE_DENIED))
484
485        setTimeout(game.unHighlightDeniedMovement, 1500, ceils)
486    },
487
488    unHighlightDeniedMovement: function(ceils) {
489        ceils.map(ceil => ceil.classList.remove(MOVE_DENIED))
490    },
491
492    undo: function () {
493        if (game.showHint && game.currentHint) {
494            return false
495        }
496
497        let undoSet = game.levelProgress.undo()
498        let undoCell;
499
500        if (undoSet) {
501            for (let i=0; i<undoSet.length; i++) {
502                undoCell = undoSet[i]
503                game.level.map[undoCell.row][undoCell.col] = undoCell.classes;
504            }
505
506            game.refreshBoard();
507            return true
508        } else {
509            return false
510        }
511    },
512
513    getCellsToMove: function(cellMovement) {
514        // identify set of cells to move
515        let cellsToMove = [getCell(cellMovement.row, cellMovement.col)];
516        let cell = null;
517
518        let horizontal = game.cellMovement.horizontal();
519        let start = horizontal ? game.cellMovement.col : game.cellMovement.row;
520        let end = game.dimention;
521
522        for (let i=start+1; i<end; i++) {
523            cell = horizontal ? getCell(game.cellMovement.row, i) : getCell(i, game.cellMovement.col);
524
525            if (game.cellIsMovable(cell)) {
526                cellsToMove.push(cell);
527            } else {
528                break;
529            }
530        }
531
532        for (let i=start-1; i>=0; i--) {
533            cell = horizontal ? getCell(game.cellMovement.row, i) : getCell(i, game.cellMovement.col);
534
535            if (game.cellIsMovable(cell)) {
536                cellsToMove.push(cell);
537            } else {
538                break;
539            }
540        }
541
542        return cellsToMove;
543    },
544
545    cellIsMovable: function(cell) {
546        return cell && cell.classList.contains(CELL) && !cell.classList.contains(FIXED);
547    },
548
549    animateCellsMove: function(){
550        get(game.boardContentId).classList.add(MOVE_IN_PROGRESS)
551
552        let cellsToMove = game.getCellsToMove(game.cellMovement);
553        let vertical = game.cellMovement.vertical();
554
555        for (let i=0; i<cellsToMove.length; i++) {
556            cellsToMove[i].classList.add(ANIMATED_CELL)
557            if (vertical) {
558                let row = +cellsToMove[i].getAttribute('row');
559                cellsToMove[i].top(row * game.cellSize + (game.cellSize * (game.cellMovement.direction == DOWN ? 1 : -1)));
560            } else {
561                let col = +cellsToMove[i].getAttribute('col');
562                cellsToMove[i].left(col * game.cellSize + (game.cellSize * (game.cellMovement.direction == RIGHT ? 1 : -1)));
563            }
564        }
565    },
566
567    rearrangeCellsAfterAnimation: function(){
568        let cellsToMove = game.getCellsToMove(game.cellMovement);
569        let newClasses = "";
570
571        for (let i=0; i<cellsToMove.length; i++) {
572            cellsToMove[i].classList.remove(ANIMATED_CELL)
573        }
574
575        let undo = [];
576
577        switch(game.cellMovement.direction) {
578            case UP:
579                for (let i=0; i<game.dimention; i++) {
580                    if (!(cellsToMove.includes(getCell(i, game.cellMovement.col)) || cellsToMove.includes(getCell(i+1, game.cellMovement.col)))) continue;
581
582                    newClasses = (i === (game.dimention-1)) ? "" : game.level.map[i+1][game.cellMovement.col];
583                    if (newClasses.indexOf(CELL) < 0 || newClasses.indexOf(FIXED) >= 0) newClasses = "";
584
585                    undo.push(new CellData(i, game.cellMovement.col, game.level.map[i][game.cellMovement.col]));
586                    game.level.map[i][game.cellMovement.col] = newClasses;
587                }
588                break;
589            case DOWN:
590                for (let i=game.dimention-1; i>=0; i--) {
591                    if (!(cellsToMove.includes(getCell(i, game.cellMovement.col)) || cellsToMove.includes(getCell(i-1, game.cellMovement.col)))) continue;
592
593                    newClasses = (i === 0) ? "" : game.level.map[i-1][game.cellMovement.col];
594                    if (newClasses.indexOf(CELL) < 0 || newClasses.indexOf(FIXED) >= 0) newClasses = "";
595
596                    undo.push(new CellData(i, game.cellMovement.col, game.level.map[i][game.cellMovement.col]));
597                    game.level.map[i][game.cellMovement.col] = newClasses;
598                }
599                break;
600            case LEFT:
601                for (let j=0; j<game.dimention; j++) {
602                    if (!(cellsToMove.includes(getCell(game.cellMovement.row, j)) || cellsToMove.includes(getCell(game.cellMovement.row, j+1)))) continue;
603
604                    newClasses = (j === (game.dimention-1)) ? "" : game.level.map[game.cellMovement.row][j+1];
605                    if (newClasses.indexOf(CELL) < 0 || newClasses.indexOf(FIXED) >= 0) newClasses = "";
606
607                    undo.push(new CellData(game.cellMovement.row, j, game.level.map[game.cellMovement.row][j]));
608                    game.level.map[game.cellMovement.row][j] = newClasses;
609                }
610                break;
611            case RIGHT:
612                for (let j=game.dimention-1; j>=0; j--) {
613                    if (!(cellsToMove.includes(getCell(game.cellMovement.row, j)) || cellsToMove.includes(getCell(game.cellMovement.row, j-1)))) continue;
614
615                    newClasses = (j === 0) ? "" : game.level.map[game.cellMovement.row][j-1];
616                    if (newClasses.indexOf(CELL) < 0 || newClasses.indexOf(FIXED) >= 0) newClasses = "";
617
618                    undo.push(new CellData(game.cellMovement.row, j, game.level.map[game.cellMovement.row][j]));
619                    game.level.map[game.cellMovement.row][j] = newClasses;
620                }
621                break;
622        }
623
624        game.levelProgress.addUndo(undo);
625        game.refreshBoard();
626
627        fireEvent(BOARD_MOVE, game.cellMovement);
628
629        if (game.showHint) {
630            game.showNextHint();
631        }
632    },
633
634    animateCellsReset: function() {
635        let boardShouldBe = clone(levels[state.mode.current][state.package.current].levels[state.level.current]).map;
636        for (let i=0; i<game.level.map.length; i++) {
637            for (let j=0; j<game.level.map[i].length; j++) {
638                let parentDiv = getCell(i, j);
639
640                let result = game.findCellResetPosition(parentDiv.getAttribute('class'), boardShouldBe);
641
642                boardShouldBe = result.boardShouldBe;
643
644                parentDiv.classList.add(ANIMATED_CELL);
645                parentDiv.top(result.newi * game.cellSize);
646                parentDiv.left(result.newj * game.cellSize);
647            }
648        }
649    },
650
651    findCellResetPosition: function(classes, targetArray) {
652        let cls1 = game.clearClasses(classes)
653        for (let i=0; i<targetArray.length; i++) {
654            for (let j=0; j<targetArray[i].length; j++) {
655                let cls2 = game.clearClasses(targetArray[i][j])
656
657                if (cls1 === cls2) {
658                    targetArray[i][j] = "-";
659                    return {newi: i, newj: j, boardShouldBe: targetArray}
660                }
661            }
662        }
663    },
664
665    clearClasses: function(classes) {
666        return game.clearClass(
667            game.clearClass(
668                game.clearClass(classes, 'star'), 'involved'), '-')
669    },
670
671    clearClass: function(classes, cls) {
672        return classes
673            .replace(' ' + cls,'')
674            .replace(cls + ' ','')
675            .replace(cls,'')
676    },
677
678    ballAffected: function(cellMove) {
679        let isAffected = false
680
681        const allBalls = [...game.balls, ...game.newBalls];
682        for (let b=0; b<allBalls.length; b++) {
683            const ball = allBalls[b]
684            if (ball.finished) continue
685
686            const hoverRowCols = ball.hoverRowCols()
687
688            const emptyInBetween = game.emptyBetweenClickAndBall(cellMove, hoverRowCols)
689
690            if (cellMove.direction == UP || cellMove.direction == DOWN) {
691                isAffected = !emptyInBetween && game.isBallCol(+cellMove.col, hoverRowCols);
692            } else {
693                isAffected = !emptyInBetween && game.isBallRow(+cellMove.row, hoverRowCols);
694            }
695
696            if (isAffected) return isAffected
697        }
698
699        return isAffected
700    },
701
702    isBallCol: function(col, rowCols) {
703        const allBalls = [...game.balls, ...game.newBalls];
704        for (let b=0; b<allBalls.length; b++) {
705            let ball = allBalls[b]
706            if (ball.finished) continue;
707
708            //let rowCols = ball.hoverRowCols();
709            for (let c=0; c<rowCols.cols.length; c++) {
710                if (rowCols.cols[c] === col) return ball.hoverCells(rowCols).cells;
711            }
712        }
713
714        return false;
715    },
716
717    isBallRow: function(row, rowCols) {
718        const allBalls = [...game.balls, ...game.newBalls];
719        for (let b=0; b<allBalls.length; b++) {
720            let ball = allBalls[b]
721            if (ball.finished) continue;
722
723            //let rowCols = ball.hoverRowCols();
724            for (let r=0; r<rowCols.rows.length; r++) {
725                if (rowCols.rows[r] === row) return ball.hoverCells(rowCols).cells;
726            }
727        }
728
729        return false;
730    },
731
732    emptyBetweenClickAndBall: function (cellMove, hoverRowCols) {
733        if (cellMove.direction == UP || cellMove.direction == DOWN) {
734            let from = Math.min(hoverRowCols.rows[0], cellMove.row);
735            let to = Math.max(hoverRowCols.rows[0], cellMove.row);
736
737            if (from === to) return false;
738
739            for (let row=from+1; row<to; row++) {
740                let cellClasses = game.level.map[row][cellMove.col];
741                if (cellClasses.indexOf(CELL) < 0 || cellClasses.indexOf(FIXED) > 0) return true;
742            }
743
744            return false;
745        } else {
746            let from = Math.min(hoverRowCols.cols[0], cellMove.col);
747            let to = Math.max(hoverRowCols.cols[0], cellMove.col);
748
749            if (from === to) return false;
750
751            for (let col=from+1; col<to; col++) {
752                let cellClasses = game.level.map[cellMove.row][col];
753                if (cellClasses.indexOf(CELL) < 0 || cellClasses.indexOf(FIXED) > 0) return true;
754            }
755
756            return false;
757        }
758    },
759
760    noPlaceToMove: function(cellMove) {
761        if (cellMove.direction == UP) {
762            for (let i=cellMove.row-1; i>=0; i--) {
763                if (!getCell(i, cellMove.col)) return false;
764                else if (getCell(i, cellMove.col).classList.contains('fixed')) return [getCell(i, cellMove.col)];
765                else if (!getCell(i, cellMove.col).classList.contains('cell')) return false;
766            }
767        } else if (cellMove.direction == DOWN) {
768            for (let i=cellMove.row+1; i<game.dimention; i++) {
769                if (!getCell(i, cellMove.col)) return false;
770                else if (getCell(i, cellMove.col).classList.contains('fixed')) return [getCell(i, cellMove.col)];
771                else if (!getCell(i, cellMove.col).classList.contains('cell')) return false;
772            }
773        } else if (game.cellMovement.direction == RIGHT) {
774            for (let j=cellMove.col+1; j<game.dimention; j++) {
775                if (!getCell(cellMove.row, j)) return false;
776                else if (getCell(cellMove.row, j).classList.contains('fixed')) return [getCell(cellMove.row, j)];
777                else if (!getCell(cellMove.row, j).classList.contains('cell')) return false;
778            }
779        } else {
780            for (let j=cellMove.col-1; j>=0; j--) {
781                if (!getCell(cellMove.row, j)) return false;
782                else if (getCell(cellMove.row, j).classList.contains('fixed')) return [getCell(cellMove.row, j)];
783                else if (!getCell(cellMove.row, j).classList.contains('cell')) return false;
784            }
785        }
786
787        return true
788    }
789};
790
791window.addEventListener(GAME_OVER, game.overHandler);
792window.addEventListener(GAME_STATE_CHANGE, game.screenStateChange);
793window.addEventListener(STAR_COLLECTED, game.starCollectHandler);
794window.addEventListener(BOARD_MOVE, game.boardMoveHandler);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.