안녕하세요 이전에 카드 메모리 게임 자동화 스크립트를 만들었던 킹암살입니다. 

단순히 귀찮아서 혼자 쓰려고 만든 스크립트를 공유해드렸는데 많은 관심 주셔서 감사했습니다. 

유튜브에 소개해주신 갓냥이님도 감사드립니다.


(12/13)

5스테이지에서 층수가 증가하는 것을 확인하여 이에 맞게 코드를 수정했습니다.
다만 이후 스테이지에서도 동일하게 층수가 늘어나는지, 혹은 다른 기믹이 추가되는지는 아직 확실하지 않아 댓글을 통해 빠르게 확인해보려 합니다.


현재 버전은 스테이지별로 층수를 자동으로 감지하도록 구현되어 있어, 이후 층수가 증가하더라도 문제없이 사용할 수 있습니다.
추가로 게임 플레이 과정까지 완전 자동화를 시도해보았으나, 의도적으로 제한된 것인지 혹은 제 스크립트의 한계인지 플레이 전후로 페이지 새로고침이 발생하여 해당 방식으로는 구현이 어려웠습니다.


이 부분은 제 구현 능력이 부족한 탓도 있는 만큼, 더 나은 개선 코드나 조언이 있다면 공유해주시면 감사하겠습니다.


(12/15)

9스테이지부터 문이 5개가 되는 것을 확인하여 코드를 수정했습니다.

아직 9스테이지에서 확인해 본 것은 아니나, 자동 층수, 문 개수 감지 코드로 이전 스테이지들까지 문제 없이 실행되는 것을 확인하여 코드를 공유드립니다. 버그 발생시 알려주시면 감사하겠습니다.


해당 스크립트는 웹페이지에서 실행하여야 정상적으로 작동합니다.

https://lostark.game.onstove.com/Promotion/Mission/251210


사용 방법 (마이크로소프트 엣지 기준)

  1. F12 키를 눌러 개발자 도구를 엽니다.
  2. 상단 탭에서 ‘Sources’를 선택한 뒤 ‘New snippet’을 클릭합니다.
  3. 아래 코드를 그대로 복사해서 붙여넣고 저장합니다.
  4. 게임플레이를 눌러 문들이 모두 보이는 상태에서 만든 스니펫을 오른쪽 클릭 → Run 하면 됩니다.

요청 딜레이로 인한 버그들이 많아 이들을 방지하려다 보니 한층 한층 플레이가 오래걸립니다. 

더 개선하고 싶었으나 토큰이 부족하여 우선은 작동시 문제가 없었던 버전을 공유드립니다.

사용 중 오류나 비정상 동작이 발견되면 댓글로 알려주시면 감사하겠습니다.


수정 사항 

(12/12)

1. 층을 인식하는 방식과 문을 찾는 방식을 더 단순하고 안정적으로 바꿨습니다.

2. 정답/오답을 기록하는 로직을 최소한으로 정리하여 속도를 올렸습니다.

3. 자동 클릭 흐름을 간단하게 개선해 불필요한 시도를 줄였습니다.


(12/13)

1. 층을 자동으로 인식하게 하여 4층 ,5층에서도 문제가 없도록 변경하였습니다.


(12/15)

1. 문을 자동으로 인식하게 하여 9스테이지에서도 문제가 없도록 변경하였습니다.

2. 층수 확인 딜레이를 줄여 게임 속도가 빨라졌습니다.




(function () {
    'use strict';

    if (window.__snowmanAutoV6_Final) return;
    window.__snowmanAutoV6_Final = true;

    const sleep = ms => new Promise(r => setTimeout(r, ms));

    function detectFloors() {
        const stage = document.querySelector('.stage');
        if (!stage) return [];
        return Array.from(stage.children)
            .filter(el => {
                const name = el.className;
                if (!name.startsWith('floor')) return false;
                const num = name.slice(5);
                return num !== '' && !isNaN(Number(num));
            })
            .map(el => Number(el.className.slice(5)))
            .sort((a, b) => a - b);
    }

    function getFloorDoors(floor) {
        const el = document.querySelector('.floor' + floor);
        if (!el) return [];
        return Array.from(el.querySelectorAll('button.door'));
    }

    function floorFromIndex(idx) {
        for (const f of FLOORS) {
            const doors = getFloorDoors(f);
            if (doors.some(d => Number(d.dataset.idx) === idx)) return f;
        }
        return null;
    }

    let FLOORS = detectFloors();
    const snowCorrect = {};
    const snowWrong = {};
    let stopFlag = false;
    let autoRunning = false;

    function initMemory() {
        FLOORS.forEach(f => {
            snowCorrect[f] = null;
            snowWrong[f] = new Set();
        });
    }

    function resetMemory() {
        FLOORS = detectFloors();
        initMemory();
    }

    initMemory();

    function detectFloorByCharacter() {
        for (const f of FLOORS) {
            const el = document.querySelector('.floor' + f);
            if (el && el.querySelector('.obj_character')) return f;
        }
        return null;
    }

    function detectFloorByActiveDoor() {
        for (const f of FLOORS) {
            if (getFloorDoors(f).some(b => !b.disabled)) return f;
        }
        return null;
    }

    function detectFloor() {
        return detectFloorByCharacter() ||
               detectFloorByActiveDoor() ||
               FLOORS[0];
    }

    async function waitFloorReady(floor, timeout = 1500) {
        const start = Date.now();
        while (Date.now() - start < timeout) {
            const charFloor = detectFloorByCharacter();
            const doors = getFloorDoors(floor).filter(b => !b.disabled);
            if (charFloor === floor && doors.length > 0) return true;
            await sleep(40);
        }
        return false;
    }

    function clickDoor(idx) {
        const btn = document.querySelector('button.door[data-idx="' + idx + '"]');
        if (btn && !btn.disabled) {
            btn.click();
            return true;
        }
        return false;
    }

    function scanPassedDoors() {
        FLOORS.forEach(f => {
            const el = document.querySelector('.floor' + f);
            if (!el) return;
            const passed = el.querySelector('button.door--passed');
            if (passed) {
                const idx = Number(passed.dataset.idx);
                if (!isNaN(idx)) snowCorrect[f] = idx;
            }
        });
    }

    function processResponse(clickedIdx, json) {
        if (typeof clickedIdx !== 'number') return;
        const floor = floorFromIndex(clickedIdx);
        if (!floor) return;

        if (json.isCorrect) {
            snowCorrect[floor] = clickedIdx;
        } else {
            snowWrong[floor].add(clickedIdx);
        }

        if (json.isComplete) resetMemory();
    }

    (function hookXHR() {
        if (window.__snowmanXHRHooked) return;
        window.__snowmanXHRHooked = true;

        const OriginalXHR = window.XMLHttpRequest;

        function WrappedXHR() {
            const xhr = new OriginalXHR();
            let url = null;
            let body = null;

            const open = xhr.open;
            xhr.open = function (m, u) {
                url = u;
                return open.apply(xhr, arguments);
            };

            const send = xhr.send;
            xhr.send = function (data) {
                body = data;
                xhr.addEventListener('load', () => {
                    try {
                        if (!url || url.indexOf('SetDoor') === -1) return;
                        let idx = null;
                        if (typeof body === 'string') {
                            const p = body.indexOf('index=');
                            if (p !== -1) idx = Number(body.slice(p + 6));
                        }
                        processResponse(idx, JSON.parse(xhr.responseText));
                    } catch {}
                });
                return send.apply(xhr, arguments);
            };
            return xhr;
        }

        WrappedXHR.prototype = OriginalXHR.prototype;
        window.XMLHttpRequest = WrappedXHR;
    })();

    async function playOneStep() {
        if (stopFlag) return;

        scanPassedDoors();

        const floor = detectFloor();
        await waitFloorReady(floor);

        const doors = getFloorDoors(floor);

        if (snowCorrect[floor] != null) {
            if (clickDoor(snowCorrect[floor])) return;
            snowCorrect[floor] = null;
        }

        for (const btn of doors) {
            const idx = Number(btn.dataset.idx);
            if (snowWrong[floor].has(idx)) continue;
            if (clickDoor(idx)) return;
        }
    }

    async function autoLoop() {
        if (autoRunning) return;
        autoRunning = true;

        while (!stopFlag) {
            try { await playOneStep(); } catch {}
            await sleep(120);
        }
        autoRunning = false;
    }

    window.addEventListener('keydown', e => {
        if (e.key === 'Escape') stopFlag = true;
    });

    autoLoop();
})();