ABOUT ME

개인 프로젝트의 진행 사항과 공부하면서 개인적으로 기억해둬야 하거나 나중에 필요할듯한 내용을 정리해서 올립니다.

  • [UE5] 노노그램 플레이 중 피드백에 관해서 - 3
    언리얼엔진/노노그램 2023. 10. 4. 17:50

    이전 글에서 힌트 피드백이 어떻게 작동해야하는지를 정했다.

    https://mstone8370.tistory.com/8

     

    [UE5] 노노그램 플레이 중 피드백에 관해서 - 2

    이전 글에서 이어진다. https://mstone8370.tistory.com/7 [UE5] 노노그램 플레이 중 피드백에 관해서 - 1 노노그램 게임들을 해보면 퍼즐을 푸는 중에 칸을 어떻게 채웠는지에 대한 다양한 방식의 피드백을

    mstone8370.tistory.com

    다음으로는 그 규칙에 맞게 작동하게 로직을 짜야한다.

     

     

     


     

     

     

    먼저 칸들을 블럭으로 구분해야한다.

    그리고 각 블럭이 가져야 할 정보는

    1. 블럭의 길이 (또는 시작점과 끝점).

    2. 줄의 어느쪽 끝에 연결되어있는지.

    3. 줄의 한쪽 끝에 해당하는지.

    이다.

     

    다시 한번 설명하면,

    블럭은 칸이 채워진 상태로 붙어있는 칸들을 묶은 것으로 길이가 1 이상이다.

    2번의 연결되어있다는 것은 무언가에서부터 현재 블럭 사이에 존재하는 칸들이 빈칸 없이 X로 막혀있는 경우를 뜻한다.

    3번은 블럭이 줄의 끝에 존재해서 블럭 자체가 줄의 시작점이나 끝점에 해당하는 경우를 확인하는 것이다.

     


     

    블럭은 간단하게 검사해야할 줄을 루프를 통해 칸을 하나씩 순환하면서 찾고, 그 정보를 저장하면 된다.

    만약 순환을 줄의 시작점부터 시작하면 줄의 시작점부터 연결된 블럭들을 확인할 수 있다.

    반대로 끝점부터 시작점까지 순환하면 줄의 끝점부터 연결된 블럭들을 확인할 수 있다.

    노노그램에서 한 줄의 칸들은 많으면 30~40칸 정도로 그렇게 큰 숫자는 아니어서 위 방식대로 두번 순환해도 시간이 오래걸리지는 않을 것으로 예상된다.

    하지만 칸의 상태가 바뀔 때마다 검사를 해야하고, 한 칸의 상태가 바뀌면 퍼즐의 행과 열을 같이 검사해야하고, 한번에 여러 칸의 상태가 바뀌는 경우도 있으니 한 줄의 칸들을 순환하는 것은 루프 한번으로 끝내기로 했다.

     

    그러기 위해서는 두가지 정보가 추가로 필요하다.

    1. 앞으로 찾을 블럭이 마지막으로 찾은 블럭과 연결되어있는지.

    2. 만약 그렇다면 어느 블럭에서부터 연결되어있는지.

     

    1번 정보를 알고있다면 순환이 끝났을때 마지막 블럭이 줄의 끝에서부터 연결되어있는지를 알 수 있다.

    앞으로 찾을 블럭이 마지막으로 찾은 블럭과 연결되어있는데 앞으로 찾을 블럭은 더이상 존재하지 않고 연결된 상태로 줄의 끝까지 왔다면 마지막으로 찾은 블럭은 줄의 끝에서부터 연결되어있다는 뜻이다.

    그리고 2번 정보를 알고있다면 마지막 블럭이 어느 블럭에서부터 연결되어있는지도 알 수 있으니 그 블럭에서부터 마지막 블럭까지는 순환이 끝나는 지점과 연결되어있음을 알 수 있다.

     

    그렇게 작성된 코드는 아래와 같다.

    // 블럭 struct
    
    struct FBlock
    {
        // 블럭 범위: [StartIdx, EndIdx]
        int32 StartIdx = -1;
        int32 EndIdx = -1;
        int32 Length = -1;
        
        bool bIsConnectedFromStart = false;
        bool bIsConnectedFromEnd = false;
    
        bool bStartOfLine = false;
        bool bEndOfLine = false;
    };
    
    
    
    // 칸의 상태
    
    UENUM(BlueprintType)
    enum class ECellState : uint8
    {
        ECS_Blank      UMETA(DisplayName = "Blank"),
        ECS_Filled     UMETA(DisplayName = "Filled"),
        ECS_Not        UMETA(DisplayName = "Not"),
    
        ECS_Invalid    UMETA(DisplayName = "Invalid")
    };
    
    
    
    // UBoardManager.cpp
    
    void UBoardManager::GetBlocks(const TArray<ECellState>& LineState, TArray<FBlock>& OutBlocks) const
    {
        OutBlocks.Empty();
    
        bool bConnectedWithLineStart = true; // 앞으로 검사할 블럭이 Line의 시작에서부터 연결되어있는지.
        bool bConnectedWithOtherBlock = true; // 앞으로 검사할 블럭이 Line의 시작이 아니더라도 다른 블럭과 연결되어있는지.
        int32 ParentBlockIdx = 0; // 마지막 블럭이 어느 블럭에 연결되어있는지.
    
        int32 CellIdx = 0;
        while (CellIdx < LineState.Num())
        {
            ECellState CS = LineState[CellIdx];
            if (CS == ECellState::ECS_Invalid)
            {
                break;
            }
            else if (CS == ECellState::ECS_Blank)
            {
                bConnectedWithLineStart = false;
                bConnectedWithOtherBlock = false;
            }
            else if (CS == ECellState::ECS_Filled)
            {
                FBlock NewBlock = FBlock();
                NewBlock.Length = 1;
                NewBlock.StartIdx = CellIdx;
                NewBlock.bStartOfLine = (CellIdx == 0);
                NewBlock.bIsConnectedFromStart = bConnectedWithLineStart;
                if (!bConnectedWithOtherBlock)
                {
                    // 다른 블럭과 연결되어있지 않으면 부모 블럭을 현재 블럭으로 지정.
                    ParentBlockIdx = OutBlocks.Num();
                }
    
                // 블럭의 끝을 찾을때까지 CellIdx++;
                while (CellIdx < LineState.Num() - 1)
                {
                    CellIdx++;
                    CS = LineState[CellIdx];
                    if (CS != ECellState::ECS_Filled)
                    {
                        break;
                    }
                }
    
                // CS: 블럭 다음 Cell의 CellState.
                if (CS == ECellState::ECS_Blank)
                {
                    // Blank면 이미 연결이 끊어진 상태.
                    bConnectedWithOtherBlock = false;
                    bConnectedWithLineStart = false;
                }
                else
                {
                    // Not || Invalid
                    bConnectedWithOtherBlock = true;
                    if (CS == ECellState::ECS_Invalid)
                    {
                        // 현재 블럭이 Line의 끝.
                        NewBlock.bEndOfLine = true;
                    }
                }
    
                NewBlock.EndIdx = CellIdx - 1;
                NewBlock.Length = CellIdx - NewBlock.StartIdx;
                OutBlocks.Add(NewBlock);
            }
            CellIdx++;
        }
    
        if (OutBlocks.Num() && bConnectedWithOtherBlock)
        {
            // Line의 끝이 마지막 블럭과 연결되어있으면, 연결이 시작된 블럭부터 마지막 블럭까지 연결됨으로 설정.
            for (int32 i = ParentBlockIdx; i < OutBlocks.Num(); i++)
            {
                OutBlocks[i].bIsConnectedFromEnd = true;
            }
        }
    }

    길이는 좀 길지만 간단한 작업이다.

    참고로 함수의 입력으로 들어가는 LineState 배열에는 해당 줄의 순환이 끝났음을 알리기 위해서 마지막에 Invalid를 하나 더 추가했다.

     

    칸의 정보를 블럭 정보로 바꿨으니 다음에는 블럭들이 힌트에 맞게 채워져있는지를 판단한다.

     

     

     


     

     

     

    이제 각 힌트에 해당하는 블럭을 찾아서 블럭의 길이가 힌트와 일치하는지 확인한다.

    중요한 것은 플레이어가 현재 보고있는 줄이 어떤 상황인지 쉽게 알 수 있게 결과를 전달해줘야 한다는 것이다.

     

    블럭을 검사하는 순서는 이전 글에 한번 나왔던 순서대로 진행한다.

     


     

    먼저 가장 우선으로 확인해야하는 것은 2개가 있다.

    1. 블럭들이 모두 힌트와 일치하게 채워져있는지.

    이때는 블럭들이 줄의 한쪽 끝과 연결되어있는지 여부와는 상관없이 모두 검사한다.

    만약 블럭이 힌트와 모두 일치한다면 모든 힌트를 맞은것으로 처리하고 검사를 끝낸다.

     

    2. 모든 블럭들이 줄의 양쪽에 연결되어있는데 블럭의 개수가 힌트의 개수보다 많은지.

    모든 블럭들이 줄의 양쪽에 연결되어있다는 것은 해당 줄에 빈 칸이 없다는 뜻이다.

    그런데 그러한 상황에서 블럭의 개수가 힌트의 개수보다 많으면 애초에 잘못 채워진 상황이므로 모두 틀린것으로 처리하고 검사를 끝낸다.

     


     

    위와 같은 경우가 아니라면 맞게 채워진 블럭과 잘못 채워진 블럭을 구분해서 플레이어에게 알려줘야한다.

    줄의 양쪽 끝을 시작점과 끝점으로 구분해서 두 지점에서부터 시작하는 검사를 한다.

    먼저 시작점부터 끝점 방향으로 블럭들을 검사한 뒤에 반대 방향으로도 한번 더 하면 된다.

    간단하게 정방향 검사, 역방향 검사라고 하겠다.

     

    정방향 검사의 경우 첫번째 블럭과 첫번째 힌트를 비교하면서 시작하면 된다.

    만약 보고있는 블럭이 시작점과 연결되었고, 블럭의 길이가 힌트과 같다면 해당 힌트는 맞은것으로 처리하고 다음 블럭과 다음 힌트로 넘어가서 반복한다.

    검사는 다음에 해당하는 경우에만 계속 반복한다.

    1. 블럭과 힌트가 존재하고,

    2. 블럭이 시작점과 연결되어있고,

    3. 블럭의 길이가 힌트와 같고,

    4. 블럭이 반대쪽 끝에 해당하지 않는 경우.

    그렇지 않으면 검사를 멈춘다.

    4번의 경우 블럭 자체가 줄의 반대쪽 끝이기 때문에 검사 대상이 아니다.

     

    역방향 검사도 마찬가지다.

    이 경우 마지막 블럭과 마지막 힌트를 비교하면서 시작하면 된다.

    대신에 이 때는 추가로 확인해야할 것들이 있다.

    1. 힌트는 더 존재하는데 블럭은 더이상 없는 경우가 있다.

    2. 블럭의 길이와 힌트가 일치하는데 그 힌트가 이미 맞은것으로 처리되어있는 경우가 있다.

     


     

    먼저 1번 경우를 보겠다.

    힌트는 더 있지만 블럭이 없다는 것은 블럭의 개수가 힌트보다 적은 상황이다.

    이 경우는 정방향 검사에서도 발생 할 수 있으며, 간단하게 검사를 끝내면 된다.

    문제는 역방향 검사에서 이런 경우가 발생하려면 해당 줄에는 빈칸이 없어서 모든 블럭들이 양쪽 끝에 연결되어야 하며, 검사는 블럭의 길이와 힌트가 일치하는 경우에만 계속 진행되므로 지금까지 모든 블럭들이 힌트와 일치했다는 뜻이다.

    이 경우는 힌트에 반복되는 패턴이 있고, 그 패턴에 일치하게 블럭들이 존재하는 상황이다.

    간단한 예시로 힌트가 [ 1, 1, 1, 1 ] 인데 블럭은 [1], [1] 과 같은상태로 존재하는 경우다.

    만약 위의 예시에서 블럭이 [1] 만 존재한다면 첫번째 힌트와 네번째 힌트는 맞은 것으로 처리될 것이고, 이 경우 결과를 그대로 전달해도 큰 문제는 없다.

    물론 모두 틀렸다고 처리할 수도 있다.

     

    문제는 블럭이 [1], [1] 또는 [1], [1], [1] 이렇게 존재하는 경우다.

    또는 힌트가 [ 2, 3, 2, 3 ] 인 상황에 블럭은 [2], [3] 과 같은 상태로 존재하는 경우가 있다.

    이 두 경우에는 정방향, 역방향 검사가 끝나면 모든 힌트가 맞은 것으로 처리될 것이다.

    하지만 블럭의 개수가 힌트의 개수보다 적으므로 잘못 채워진 것은 명확하다.

    그런 상황에 모든 힌트가 맞았다고 전달해주면 플레이어가 해당 줄을 채우는건 끝났다고 오해하게 되어 나중에 문제가 발생할 것이다.

    따라서 이런 경우에는 모든 힌트를 틀렸다고 해야한다.

    그리고 이런 경우를 모호한 경우라고 한다.

    블럭이 여러개의 힌트에 해당할 수 있으면 모호하다고 한다.

     

    추가로 [1], [1], [1] 의 경우에는 위에서의 1번과 2번 모두 해당된다.

    자세한건 2번 경우를 설명할때 하겠다.

     


     

    그 다음으로 2번 경우를 보겠다.

    블럭과 힌트를 순서대로 확인하는데 현재 보고있는 힌트가 이미 맞은 것으로 처리 되어있다는 것은 정방향 검사에서 이미 확인했으며, 그때 확인한 블럭과 길이가 일치했다는 것이다.

    이런 상황이 발생할 수 있는 경우를 따져보면 다음과 같다.

     

    (1) 블럭이 힌트보다 많음.

    힌트는 하나인데 블럭이 두개 있다고 가정해보면 두 블럭을 확인해야할 상황에 사용될 힌트는 하나 뿐이다.

    그리고 두 블럭의 길이가 같고, 힌트의 숫자와 같다면 이런 경우가 발생한다.

     

    조금 더 자세하게 힌트는 3개인 상황에 블럭은 4개 있는 경우를 가정해보자.

    블럭을 A, B, C, D로 구분하고 힌트를 x, y, z로 구분하겠다.

    블럭 A, B는 줄의 시작점에 연결되어있고, 블럭 C, D는 줄의 끝점에 연결되어있다.

    그리고 블럭 B와 C는 서로 떨어져있다.

    블럭 A의 길이는 힌트 x와 일치하고, 블럭 B의 길이는 힌트 y와 일치한다.

    블럭 C의 길이는 힌트 y와 일치하고, 블럭 D의 길이는 힌트 z와 일치한다.

    그리고 블럭 B와 C의 길이는 같다.

     

    이 경우 힌트 y는 정방향 검사에서 블럭 B를 검사할 때 사용되고, 그 뒤에 역방향 검사에서 블럭 C를 검사할 때 다시 사용된다.

    그리고 정방향 검사에서는 블럭 B의 길이가 힌트 y와 일치하므로 블럭 B는 힌트 y를 위해서 채워졌다고 판단할 것이고, 역방향 검사에서는 블럭 C의 길이가 힌트 y와 일치하므로 블럭 C는 힌트 y를 위해서 채워졌다고 판단할 것이다.

    힌트 하나를 위해 여러개의 블럭이 존재하는 상황이므로 잘못된 상황임은 명확하다.

    힌트가 여러개의 블럭에 해당할 수 있는 경우로 모호한 상황과는 반대된다.

     

    그런데 이 경우에는 검사 결과를 보면 모든 힌트가 맞은 것으로 처리된다.

    하지만 블럭 개수가 힌트 개수보다 많아 잘못 채운 것은 명확하고, 정말로 모든 힌트가 맞았다면 이전에 이미 확인 됐을 것이다.

    따라서 이런 경우에는 모든 힌트를 틀린 것으로 처리해야한다.

     

    그래도 맞은 부분은 맞았다고 처리를 해줄수도 있지만, 그러기 위해서는 블럭 B와 C의 두 길이 모두 힌트 y와 달라야 한다.

    두 블럭의 길이가 힌트와 다르다면 플레이어가 아직 칸을 채우는 과정중에 있다고 판단할 수 있고, 조금만 더 진행하면 정답에 맞게 채워질 여지가 있으므로 이때에도 모두 틀린 것으로 처리하면 플레이중에 혼란을 줄 수 있다.

    하지만 블럭 두개의 길이가 모두 힌트와 일치한다면 플레이어의 의도가 들어있는 상황임을 가정할 수 있고, 이 경우에는 약간의 수정으로는 정답에 맞게 채우기는 어려운 상황이므로 모두 틀린 것으로 처리한다.

     

    (2) 블럭의 개수가 힌트의 개수보다 적고 모든 블럭들이 양쪽 끝에 연결되어있음.

    그리고 힌트에는 반복되는 패턴이 있고, 블럭이 그 패턴과 일치하게 존재해야하며, 양쪽 끝에 블럭들을 대입했을 때 두 패턴은 서로의 영역을 침범해야한다.

    서로 겹치는 영역이 있으므로 하나의 힌트가 두개의 블럭에 해당되는 상황이 발생한다.

    위에 예시로 들었던 블럭  [1], [1], [1] 의 상황이다.

    이때는 하나의 힌트가 여러개의 블럭에 해당할 수 있는 모호한 상황과, 하나의 블럭이 여러개의 힌트에 해당할 수 있는 상황이 동시에 존재한다.

    이 경우에도 검사 결과는 모든 힌트가 맞은것으로 처리될 것이니 나중에 모두 틀린 것으로 바꿔야 한다.

     


     

    검사중에 발생할 수 있는 특수한 경우는 모두 확인했다.

    이제 마지막으로 모든 힌트가 맞은 것으로 처리되어있는지와 모호성 검사를 진행한다.

     

    검사중에 발생하는 특수한 경우를 제외해도 검사가 끝나면 모든 힌트가 맞은 것으로 되어있는 경우들이 있다.

    일단 검사를 진행 했다는 것은 모든 블럭들이 정답에 맞게 채워지지 않은 경우라는 뜻이므로, 검사가 끝났는데 모든 힌트가 맞은 것으로 처리되어있다면 중간에 검사를 하지 않은 블럭들이 존재한다는 뜻이다.

    연결된 블럭들의 개수는 힌트의 개수와 일치하고, 그 블럭들의 길이가 힌트와 일치하지만, 연결되지 않고 분리되어있어서 검사를 건너뛴 블럭들이 중간에 들어있는 경우이므로 모두 틀렸다고 처리해야한다.

    이 경우는 모호한 것과 반대되는 상황이므로 모호성은 확인하지 않는다.

     

    제일 마지막 과정인 모호성 검사다.

    이건 위의 역방향 검사에서 발생한 특수한 경우와 이어지는 과정이다.

    모호성이 발생하는 경우는 공통적으로 모든 블럭들이 줄의 양쪽에 연결된 경우에 발생하므로, 그때에만 확인하면 될 것이다.

    첫번째 블럭이나 마지막 블럭이 줄의 한쪽 끝에 해당하는 경우에는 어느 힌트에 해당하는지 명확하므로, 첫번째 힌트 또는 마지막 힌트와 비교해서 블럭의 길이와 일치하면 이곳에서 맞은 것으로 처리한다.

     

     

     


     

     

     

    진행과정은 다 정리되었고 다음은 코드로 어떻게 구현했는지를 정리한다.

    일단 구조체를 정의해야 한다.

    // 한 줄의 Info(힌트)들을 저장하는 구조체.
    
    USTRUCT(BlueprintType)
    struct FLineInfo
    {
        GENERATED_BODY()
    
        UPROPERTY(EditAnywhere, BlueprintReadWrite)
        TArray<int32> Infos;
    
        int32& operator[](int32 Idx)
        {
            return Infos[Idx];
        }
    
        int32 operator[](int32 Idx) const
        {
            return Infos[Idx];
        }
    
        void Clear()
        {
            Infos.Empty();
        }
    };
    
    
    
    // Info가 맞았는지, 틀렸는지를 저장하는 구조체.
    
    USTRUCT(BlueprintType)
    struct FLineCheck
    {
        GENERATED_BODY()
    
        UPROPERTY(BlueprintReadOnly)
        TArray<bool> MatchStates;
        UPROPERTY(EditAnywhere, BlueprintReadWrite)
        int32 MatchCount = 0;
    
        void Fill(bool Value, int32 Size)
        {
            Clear();
            if (Size > 0)
            {
                MatchStates.Init(Value, Size);
                if (Value)
                {
                    MatchCount = Size;
                }
                else
                {
                    MatchCount = 0;
                }
            }
        }
    
        void SetState(int32 index, bool bNewState)
        {
            if (index < 0)
            {
                index = MatchStates.Num() + index;
            }
            if (0 <= index && index < MatchStates.Num())
            {
                if (bNewState != MatchStates[index])
                {
                    MatchStates[index] = bNewState;
                    if (bNewState)
                    {
                        MatchCount++;
                    }
                    else
                    {
                        MatchCount--;
                    }
                }
            }
        }
    
        void Clear()
        {
            MatchCount = 0;
            MatchStates.Empty();
        }
    
    };

    소스코드에서 Info는 힌트를 의미한다.

    한 줄의 Info들은 정수형 값들을 저장하는 TArray로 관리한다.

     

    FLineInfo는 정수형 배열과 다를게 없다.

    그런데 굳이 구조체로 선언한 이유는 여러 줄의 Info들을 배열으로 관리하기 위해 2차원 배열을 만들어야하기 때문이다.

    언리얼 엔진의 배열인 TArray에는 TArray를 넣을 수 없기 때문에 구조체로 한번 감싼 다음에 넣어야 한다.

     

    FLineCheck는 Info가 맞았는지 틀렸는지를 저장하는 bool타입 배열을 가지고있다.

    Info가 몇개 맞았는지를 확인할 일이 있기 때문에 정수 타입 변수를 하나 더 포함시키기 위해 구조체로 만들었다.

     

    다음은 검사 과정이다.

    0. 전처리

     

    1. 초기 조건 확인

    1.1. 모두 정답인 경우

    1.2. 블럭이 Info보다 많은 경우

     

    2. 일부분 정답 확인

    2.1. 정방향 검사

    2.2. 역방향 검사

    2.3. 정답은 아니지만 검사된 블럭의 개수가 Info의 개수와 동일한 경우

    2.4. 모호성 검사

    // UBoardManager.cpp
    
    void UBoardManager::LineMatch(const FLineInfo& LineInfo, const TArray<FBlock>& Blocks, FLineCheck& OutLineCheck) const
    {
        // 0. 전처리
        const int32 LineInfoNum = LineInfo.Infos.Num();
        OutLineCheck.Fill(false, LineInfoNum);
    
        // Info가 0인 경우 간단하게 확인하고 return.
        if (Blocks.Num() == 0)
        {
            if (LineInfoNum == 1 && LineInfo[0] == 0)
            {
                OutLineCheck.Fill(true, 1);
            }
            return;
        }
    
        // 모든 블럭이 Line의 양쪽에 연결되어있는지, 빈 칸이 하나도 없는지 확인.
        // 모두 연결되어있는지는 블럭 하나만 보면 알 수 있음.
        const bool bAllConnected = (Blocks[0].bIsConnectedFromStart && Blocks[0].bIsConnectedFromEnd);
    
        // 1. 다른 경우보다 제일 우선되는 경우 확인.
        // 1.1. 모두 정답인지 확인.
        if (Blocks.Num() == LineInfoNum)
        {
            bool bAllMatch = true;
            for (int32 i = 0; i < LineInfoNum; i++)
            {
                if (Blocks[i].Length != LineInfo[i])
                {
                    bAllMatch = false;
                    break;
                }
            }
            if (bAllMatch)
            {
                OutLineCheck.Fill(true, LineInfoNum);
                return;
            }
        }
        // 1.2. 모두 연결되어있지만 블럭의 개수가 Info의 개수보다 많으면 모두 틀린 것으로 지정.
        if (Blocks.Num() > LineInfoNum && bAllConnected)
        {
            OutLineCheck.Fill(false, LineInfoNum);
            return;
        }
    
        // 2. 일부분만 정답인 경우.
        if (Blocks[0].bIsConnectedFromStart)
        {
            // 2.1. Line의 Start와 연결된 블럭들만 확인. (정방향 검사)
            for (int32 i = 0; i < LineInfoNum; i++)
            {
                if (i >= Blocks.Num())
                {
                    break;
                }
    
                const FBlock& CurrentBlock = Blocks[i];
                const int32 TargetLength = LineInfo[i];
                if (CurrentBlock.bEndOfLine)
                {
                    // Line의 End에 해당하는 블럭은 현재 방향의 검사 대상이 아니며, 다음에 확인할 예정임.
                    break;
                }
    
                if (CurrentBlock.bIsConnectedFromStart)
                {
                    if (CurrentBlock.Length == TargetLength)
                    {
                        OutLineCheck.SetState(i, true);
                    }
                    else
                    {
                        // 뒤에있는 블럭들이 Line의 Start에 연결되어있어도
                        // Info와 다른 블럭이 있다면 다음 블럭들은 확인하지 않음.
                        break;
                    }
                }
                else
                {
                    // Line의 End에 연결된 블럭들은 아래에서 확인함.
                    break;
                }
            }
        }
        if (Blocks.Last().bIsConnectedFromEnd)
        {
            // 2.2. Line의 End와 연결된 블럭들만 확인. (역방향 검사)
            for (int32 i = 0; i < LineInfoNum; i++)
            {
                const int32 BlockIdx = Blocks.Num() - 1 - i;
                if (BlockIdx < 0)
                {
                    if (bAllConnected)
                    {
                        /*
                        * 이 분기점은 현재 Line의 모든 칸이 채워져있지만, 블럭의 개수가 부족하고, 존재하는
                        * 블럭들은 다 힌트에 맞는 경우임.
                        * 그러기 위해서는 Info에 반복되는 패턴이 있고 그 패턴에 맞게 블럭이 존재해야함.
                        * 그리고 양쪽 끝에서 시작하는 패턴이 서로의 영역을 침범하지 않으면 이 분기점으로 옴.
                        *
                        * 만약 패턴이 짧고 Info가 충분히 길다면, 중간에 틀린것으로 표시될 Info가 있으므로,
                        * 사용자가 보기에 양쪽 끝의 패턴은 서로 구분되며, 두 패턴 다 정답으로 표시할 수도 있음.
                        *
                        * 하지만 양쪽 두 패턴으로 인해서 모든 Info가 정답으로 되었다면, 잘못된 결과가 전달됨.
                        * (e.g. Info는 4개인데 패턴의 길이가 2이고 블럭이 패턴에 일치하게 존재함.)
                        * 이 경우 모호성이 발생함.
                        * 일단 칸을 잘못 채운것은 분명하므로, 모든 Info를 틀린것으로 지정한 뒤에
                        * 마지막에 모호성 체크를 함.
                        */
                        if (OutLineCheck.MatchCount >= LineInfoNum)
                        {
                            OutLineCheck.Fill(false, LineInfoNum);
                        }
                    }
                    break;
                }
    
                const FBlock CurrentBlock = Blocks[BlockIdx];
                const int32 LineInfoIdx = LineInfoNum - 1 - i;
                const int32 TargetLength = LineInfo[LineInfoIdx];
                if (CurrentBlock.bStartOfLine)
                {
                    // Line의 Start에 해당하는 블럭은 현재 방향의 검사 대상이 아니며, 이미 확인되었음.
                    break;
                }
    
                if (CurrentBlock.bIsConnectedFromEnd)
                {
                    if (CurrentBlock.Length == TargetLength)
                    {
                        if (!OutLineCheck.MatchStates[LineInfoIdx])
                        {
                            OutLineCheck.SetState(LineInfoIdx, true);
                        }
                        else
                        {
                            /*
                            * 위에서 이미 확인한 블럭을 다시 확인하고 있음.
                            * 위에서 다른 블럭을 확인했을 때 해당하는 Info와 맞아서 true로 되어있지만,
                            * 이번에도 같은 Info를 보고 true라고 판단하고있음.
                            * 
                            * 이런 상황은 두가지 경우에서 발생함.
                            * (1) 블럭이 Info보다 많아서 이전에 확인된 Info가 다시 사용됨.
                            *     하나의 Info가 두 블럭에 매칭될 수 있는 경우인데, 그러기 위해서는 두 블럭의
                            *     크기가 같아야함.
                            *     
                            *     하지만 위에서 확인했을 때 이 블럭의 개수가 Info와 달라서 틀렸다면 이곳에서도 
                            *     틀릴것이며, 두개의 블럭이 이 Info를 위해 채워졌다고 보기 어렵기 때문에,
                            *     (e.g. 아직 칸을 채우고있는 중간과정인데 양쪽을 먼저 채운 다음에 중간을
                            *     채우고있는 경우.)
                            *     이 Info만 틀렸다고 표시되는 것은 문제가 없음.
                            *     
                            *     하지만 블럭의 크기가 둘 다 Info와 일치한다면 두 블럭들은 이 Info를 위해
                            *     채워졌다고 볼 수 있으며, 하나의 Info를 위해 두개의 블럭이 존재하는건
                            *     이 Line이 아예 잘못 채워진 상황임.
                            * 
                            * (2) 모든 블럭들이 Line의 양쪽에 연결되어있는데 블럭의 개수가 Info의 개수보다 적음.
                            *     그리고 Info에 반복되는 패턴이 있고 그 패턴에 맞게 블럭이 존재해야함.
                            *     이 분기점에 오기 위해서는 양쪽 끝에 존재하는 패턴이 서로의 영역을 침범해야함.
                            *     모호한 상황이므로 일단 모두 틀린것으로 표시하고 모호성 체크를 함.
                            * 
                            * 두 경우 어느것이든 이 Line은 잘못 채워진것은 명확하기에 모두 틀린것으로 지정함.
                            */
                            OutLineCheck.Fill(false, LineInfoNum);
                            break;
                        }
                    }
                    else
                    {
                        break;
                    }
                }
                else
                {
                    break;
                }
            }
        }
    
        // 2.3. 정답은 아니지만 Info가 모두 맞은 경우.
        if (OutLineCheck.MatchCount == LineInfoNum)
        {
            /*
            * 만약 블럭의 상태가 이 Line의 진짜 정답이었다면 함수의 처음부분에서 확인되었을 것임.
            * 그리고 이 분기점에 도달하려면 검사중 발생할 수 있는 특수 케이스는 아니고,
            * 연결된 블럭들이 Info와 모두 맞지만, 중간에 연결되지 않은 블럭이 한개 이상 존재하는 상태.
            * 이런 경우 모두 틀린것으로 지정함. (모호성 체크 안함.)
            */
            OutLineCheck.Fill(false, LineInfoNum);
            return;
        }
    
        // 2.4. 모호성 체크.
        if (bAllConnected)
        {
            // 만약 블럭이 Line의 Start거나 End라면, 어느 Info에 해당하는지는 명확하므로
            // 마지막으로 한번 더 확인함.
            if (Blocks[0].bStartOfLine && Blocks[0].Length == LineInfo[0])
            {
                if (!OutLineCheck.MatchStates[0])
                {
                    OutLineCheck.SetState(0, true);
                }
            }
            if (Blocks.Last().bEndOfLine && Blocks.Last().Length == LineInfo.Infos.Last())
            {
                if (!OutLineCheck.MatchStates.Last())
                {
                    OutLineCheck.SetState(-1, true);
                }
            }
        }
    }

     

    여기까지 하면 레퍼런스로 정했던 모바일게임인 No2g의 피드백과 거의 동일하게 작동한다.

     

     

     


     

     

     

    이 정도만 해도 피드백은 이해하기 쉽고, 퍼즐을 푸는데 충분히 도움이 된다.

    하지만 로직을 분석하는 과정에서 약간 아쉬운 부분을 발견했다.

    그러한 상황은 발생할 가능성이 낮아서 큰 문제는 되지 않겠지만, 조금만 수정하면 해결될 수 있을듯 해서 다음에는 이 부분에 관해서 정리해 보겠다.

Designed by Tistory.