[UE5] 노노그램 플레이 중 피드백에 관해서 - 4
이전 글 마지막에서 적었던 약간 아쉬운 부분을 보완하겠다.
https://mstone8370.tistory.com/9
[UE5] 노노그램 플레이 중 피드백에 관해서 - 3
이전 글에서 힌트 피드백이 어떻게 작동해야하는지를 정했다. https://mstone8370.tistory.com/8 [UE5] 노노그램 플레이 중 피드백에 관해서 - 2 이전 글에서 이어진다. https://mstone8370.tistory.com/7 [UE5] 노노그
mstone8370.tistory.com
다음과 같은 예시가 있다.
힌트는 [ 1, 4, 2 ] 이다.
이런 상황에 아래와 같이 줄을 채웠다고 가정한다.
이런 상황에 블럭은 [[1, [2, 1], 2] 이렇게 되어있다.
상황을 분석해보면 힌트는 3개, 블럭은 4개 존재하고, 첫번째 블럭과 네번째 블럭은 각각 첫번째 힌트와 세번째 힌트와 맞아서 그 결과를 힌트에 표현해주고 있다.
그리고 두번째 블럭과 세번째 블럭은 둘 다 두번째 힌트에 해당하겠지만, 두 블럭 다 힌트와 크기가 다른 상황이다.
그렇다면 힌트는 어느 블럭에 해당되어도 틀린 상황이니 위와 같이 표현해주는데에는 문제가 없다.
그리고 아래처럼 두 블럭이 서로 이어질 가능성이 있다.
이렇게 되면 두번째 힌트가 두번째 블럭에 해당하게 되지만 블럭의 길이가 힌트와 달라서 틀렸다고 표시된다.
하지만 아래의 경우에는 상황을 다르게 이해해야한다.
이 경우에는 1번 예시에서 두번째 블럭을 X로 막은 경우다.
이전 글에서 작성했던 검사 방식에 따르면 이런 결과가 나오는게 맞고, No2g에서도 동일한 결과를 보여준다.
하지만 개인적인 의견으로는 블럭을 X로 막은 것은 플레이어의 의도가 들어있다고 판단해야한다.
그리고 블럭이 힌트보다 많다면, 서로 이어져서 하나가 될 블럭들이 있다는 뜻이다.
하지만 서로 이어져야 할 블럭들이 막혀있다면 그 블럭들은 이어질 가능성이 없어진다.
따라서 위의 경우에는 하나의 힌트를 위해 두개의 블럭이 존재하는게 확실해진 상황이므로, 모두 틀렸다고 처리하는게 옳은 방법이라고 생각한다.
추가로 이런 상황이 있을 수도 있다.
1번 예시에서 양쪽이 막혀있지만 줄의 어느쪽 끝에도 연결되지 않은 블럭이 추가된 경우다.
하지만 블럭이 막혀있어서 플레이어의 의도가 들어갔다고 이해할수도 있다.
이 경우는 현재의 검사 방식으로 구분하기엔 꽤 복잡한 상황이다.
현재 검사 방식으로는 줄의 양쪽 끝 어느쪽에도 연결되지 않은 블럭은 검사 대상이 아니며, 존재 여부만 판단하기 때문이다.
따라서 이런 경우도 구분하기 위해선 로직을 다시 짜야하며, 규칙도 다시 정의해야할 필요가 있다.
일단 이러한 경우는 앞으로 해결해야할 과제로 남겨둬야할 듯 하다.
추가된 규칙을 다시 한번 더 정리하고 구현으로 넘어가겠다.
블럭이 닫혀있다는 것은 블럭의 양쪽이 X로 막혀있다는 뜻이다.
한쪽만 막혀있지 않아도 열린 상태라고 정의한다.
한 줄에서 힌트보다 블럭의 개수가 많으면 몇몇 블럭들이 서로 이어져서 블럭의 개수가 줄어들 것을 기대해야한다.
그리고 이어질 가능성이 있는 두 블럭은 각각 줄의 다른쪽에 연결된 블럭들 중 마지막 블럭이어야 한다.
하지만 서로 이어질 것이라고 예상했던 블럭이 막힌 상태라면 다른 블럭과 이어질 수가 없다.
따라서 블럭의 개수가 힌트보다 많아지는 것은 확정된 일이므로, 이런 경우에는 모든 힌트를 틀린 것으로 처리한다.
이때 막힌 블럭을 판단하는 기준은 줄의 한쪽 끝과 연결된 경우에만 판단한다.
구현을 위해서는 한 줄의 정보를 블럭의 정보로 변환하는 과정에서 줄의 한쪽 끝과 이어진 블럭들이 닫혀있는지를 판단해야한다.
먼저 블럭 구조체에 닫혀있는상태인지를 저장할 변수를 하나 추가한다.
// 블럭 struct
struct FBlock
{
// 블럭 범위: [StartIdx, EndIdx]
int32 StartIdx = -1;
int32 EndIdx = -1;
int32 Length = -1;
bool bIsClosed = false;
bool bIsConnectedFromStart = false;
bool bIsConnectedFromEnd = false;
bool bStartOfLine = false;
bool bEndOfLine = false;
};
bIsClosed
가 그 역할을 한다.
다음으로 한 줄의 상태 정보를 블럭 정보로 변환하는 GetBlocks
함수에 약간의 과정을 더해줬다.
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 || CS == ECellState::ECS_NotSure)
{
FBlock NewBlock = FBlock();
NewBlock.Length = 1;
NewBlock.StartIdx = CellIdx;
NewBlock.bStartOfLine = (CellIdx == 0);
NewBlock.bIsConnectedFromStart = bConnectedWithLineStart;
NewBlock.bIsClosed = false; // <-- 추가됨.
if (!bConnectedWithOtherBlock)
{
// 다른 블럭과 연결되어있지 않으면 부모 블럭을 현재 블럭으로 지정.
ParentBlockIdx = OutBlocks.Num();
}
// 블럭의 끝을 찾을때까지 CellIdx++;
while (CellIdx < LineState.Num() - 1)
{
CellIdx++;
CS = LineState[CellIdx];
if (CS != ECellState::ECS_Filled && CS != ECellState::ECS_NotSure)
{
break;
}
}
// CS: 블럭 다음 Cell의 CellState.
if (CS == ECellState::ECS_Blank)
{
// Blank면 이미 연결이 끊어진 상태.
bConnectedWithOtherBlock = false;
bConnectedWithLineStart = false;
}
else
{
// Not || Invalid
bConnectedWithOtherBlock = true;
// 블럭의 다음 Cell이 빈칸이 아니면 닫힌 상태.
// 블럭이 Line의 Start와 연결되어있는 경우에만 닫힌 상태로 지정함.
NewBlock.bIsClosed = bConnectedWithLineStart; // <-- 추가됨.
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;
if (!OutBlocks[i].bIsClosed) // <-- 추가됨.
{
/*
* ParentBlockIdx가 0이고, 그 블럭이 Line의 Start에 해당하는 경우 StartIdx는 0이므로
* StartIdx - 1의 결과값은 -1이 나와서 인덱스 오류가 발생함.
* 하지만 해당 블럭이 bIsClosed가 false인 경우에만 해당 작업을 하므로 그러한 문제는 발생하지 않음.
*
* 인덱스 0의 블럭이 StartOfLine이려면 해당 블럭은 Start와 연결되어있는 것과 마찬가지임.
* 그러한 상황에 인덱스 0의 블럭이 마지막 블럭과 연결되어있으며,
* 마지막 블럭이 Line의 End와 연결되어있는 상황은
* Line에 빈 칸이 없어서 모든 블럭이 Line의 양쪽 모두에 연결된 상황이므로,
* 모든 블럭의 bIsClosed는 이미 true로 지정되어있을 것이기 때문.
*/
OutBlocks[i].bIsClosed = (LineState[OutBlocks[i].StartIdx - 1] == ECellState::ECS_Not);
}
}
}
}
추가된 부분은 주석으로 표시해놨다.
블럭을 찾는 과정에서 블럭의 다음 칸 까지 확인하므로, 그때 블럭이 막혀있는지를 쉽게 알 수 있다.
그리고 줄의 끝점과 연결된 블럭들을 확인하는 과정에서는 블럭이 시작되는 칸의 이전 칸을 검사해서 블럭이 닫혀있는지를 검사한다.
이때 조심해야하는 상황은 블럭들이 줄의 양쪽 끝에 연결되어있으며, 첫번째 블럭이 줄의 시작점에 해당하는 경우다.
이때에는 마지막 블럭은 첫번째 블럭과 연결되어있으며, ParentBlockIdx
에는 0이 저장되어있을 것이다.
그리고 첫번째 블럭이 시작하는 칸의 인덱스는 0일 것이며, 그 칸의 이전 칸은 인덱스가 -1으로 계산될 것이므로 에러가 발생한다.
이 경우를 구분하기 위해서 ParentBlockIdx
에서 부터 이어진 블럭들이 닫혀있는지를 확인한다.
연결된 대상 블럭이 이미 닫혀있는 상태라면 그 블럭은 줄의 시작점과 연결되어 있다.
그리고 그 블럭과 마지막 블럭까지 연결되어있으며, 마지막 블럭도 줄의 끝점과 연결되어있다는 것은 모든 블럭들이 줄의 양쪽 끝과 연결되어있다는 뜻이다.
이 경우를 걸러내면 첫번째 블럭이 줄의 시작점에 해당하는 경우는 부가적인 조건이므로 동시에 걸러지게 된다.
그리고 이미 모든 블럭들이 닫혀있는 상태로 지정되어있으니 또 다시 닫혀있는 상태로 지정할 필요도 없다.
다음으로 검사를해야한다.
블럭의 개수가 힌트의 개수보다 많은 경우에만 추가된 작업을 한다.
정방향 검사를 하는 중에 줄의 시작점과 연결된 마지막 블럭이면서 블럭의 길이가 힌트와 다르고 닫힌 상태인 블럭의 존재 여부를 bWrongAndClosedBlockExist
라는 변수에 저장한다.
정방향 검사가 끝나고 역방향 검사를 하는 중에 줄의 끝점과 연결된 마지막 블럭이면서 블럭의 길이가 힌트와 다른 블럭이 있다면 bWrongAndClosedBlockExist
를 확인해서 그러한 블럭이 정방향 검사에서도 발견했는지, 아니면 이 블럭이 막혀있는지 확인한다.
만약 연결되어야 할 두 블럭이 있지만, 둘중 한 블럭이라도 닫혀있으면 모두 틀린 것으로 지정한다.
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. 일부분만 정답인 경우.
// Line의 Start에 연결되어있는 마지막 블럭이지만, 길이가 Info와 다르고 닫혀있는 블럭이 존재하는지.
bool bWrongAndClosedBlockExist = false; // <-- 추가됨.
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와 다른 블럭이 있다면 다음 블럭들은 확인하지 않음.
// 현재 보고있는 블럭이 Line의 Start와 연결된 마지막 블럭이면서
// 길이가 Info와 다르고 닫혀있는지 확인. <-- 추가됨
bool bLastOfStart = (i + 1 >= Blocks.Num() || !Blocks[i + 1].bIsConnectedFromStart);
if (bLastOfStart && CurrentBlock.bIsClosed)
{
// 이 블럭은 다른 블럭과 이어져서 하나가 될 가능성이 없음.
bWrongAndClosedBlockExist = true;
}
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
{
if (Blocks.Num() > LineInfoNum) // <-- 추가됨.
{
// 만약 블럭의 개수가 Info의 개수보다 많다면
// 몇몇 블럭들이 이어져서 블럭의 개수가 줄어들 것을 기대해야 함.
// 이 블럭이 Line의 End와 연결된 마지막 블럭인지.
bool bLastOfEnd = (BlockIdx - 1 < 0 || !Blocks[BlockIdx - 1].bIsConnectedFromEnd);
if (bLastOfEnd)
{
if (CurrentBlock.bIsClosed || bWrongAndClosedBlockExist)
{
// 그러한 경우에 이 블럭이 닫혀있거나 이전의 그러한 블럭이 닫혀있다면
// 다른 블럭과 이어져서 블럭의 개수가 줄어들 가능성이 없음.
OutLineCheck.Fill(false, LineInfoNum);
}
}
}
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);
}
}
}
}
이렇게 하면 아래처럼 의도대로 작동한다.