using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Algorithm
{
// 우선순위 큐를 만들어줌
class PriorityQueue<T> where T : IComparable<T>
{
List<T> _heap = new List<T>();
// O(logN)
public void Push(T data)
{
// 힙의 맨 끝에 새로운 데이터를 삽입한다.
_heap.Add(data);
int now = _heap.Count - 1;
// 도장 깨기
while (now > 0)
{
// 도장 깨기 시도
int next = (now - 1) / 2; // 부모의 인덱스
if (_heap[now].CompareTo(_heap[next]) < 0)
{
break; // 실패
}
// 두 값을 교체
T temp = _heap[now];
_heap[now] = _heap[next];
_heap[next] = temp;
// 검사 위치를 이동
now = next;
}
}
// O(logN)
public T Pop()
{
// 반환할 데이터를 따로 저장
T ret = _heap[0];
// 마지막 데이터를 루트로 이동한다.
int lastIndex = _heap.Count - 1;
_heap[0] = _heap[lastIndex];
_heap.RemoveAt(lastIndex);
lastIndex--;
// 역으로 내려가는 도장 깨기 시작
int now = 0;
while (true)
{
int left = 2 * now + 1;
int right = 2 * now + 2;
int next = now;
// 왼쪽 값이 현재 값보다 크면 왼쪽으로 이동
if (left <= lastIndex && _heap[next].CompareTo(_heap[left]) < 0)
{
next = left;
}
// 오른값이 현재값보다 크면(왼쪽 이동 포함), 오른쪽으로 이동
if (right <= lastIndex && _heap[next].CompareTo(_heap[left]) < 0)
{
next = right;
}
// 왼쪽 / 오른쪽 모두 현재값보다 작으면 종료
if (next == now)
{
break;
}
// 두 값을 교채한다.
T temp = _heap[now];
_heap[now] = _heap[next];
_heap[next] = temp;
// 검사 위치 이동
now = next;
}
return ret;
}
public int Count
{
get { return _heap.Count; }
}
}
class Knight : IComparable<Knight>
{
public int Id { get; set; }
public int CompareTo(Knight other)
{
if (Id == other.Id)
return 0;
return Id > other.Id ? 1 : -1;
throw new NotImplementedException();
}
}
}
// BFS와 다익스트라는 목적지를 모르는 상태로 만듬
// 모든 정점을 다지나서 가서 비효율 적임
// 반면 A*는 시작지점과 끝점을 다 알고있음
// 출구에 가까워질 수록 가산점이 들어감
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Algorithm
{
class Pos
{
public Pos(int y, int x) { Y = y; X = x; }
public int Y;
public int X;
}
struct PQNode : IComparable<PQNode>
{
public int F;
public int G;
public int Y;
public int X;
public int CompareTo(PQNode other)
{
if (F == other.F) { return F; }
return F < other.F ? 1 : -1;
}
}
class Player
{
public int PosY { get; private set; }
public int PosX { get; private set; }
//Random _random = new Random();
Board _board;
enum Dir // 반시계방향
{
Up = 0,
Left = 1,
Down = 2,
Right = 3
}
int _dir = (int)Dir.Up;
List<Pos> _points = new List<Pos>();
public void Initialize(int posY, int posX, Board board)
{
PosX = posX;
PosY = posY;
_board = board;
//BFS();
AStar();
}
void AStar()
{
// UP LEFT DOWN RIGHT UPLEFT, DOWNLEFT, DOWNRIGHT UPRIGHT
int[] deltaY = { -1, 0, 1, 0, -1, 1, 1, -1 };
int[] deltaX = { 0, -1, 0, 1, -1, -1, 1, 1 };
int[] cost = { 10, 10, 10, 10, 14, 14, 14, 14 };
// 점수 매기기
// F = G + H
// F = 최종 점수 (작을 수록 좋음, 경로에 따라 달라짐)
// G = 시작점에서 해당 좌표까지 이동하는데 드는 비용 (작을 수록 좋음, 경로에 따라 달라짐)
// H = 목적지에서 얼마나 가까운지 (작을 수록 좋음, 고정)
// (y,x) 이미 방문했는지 여부 (방문 = closed 상태)
bool[,] closed = new bool[_board.Size, _board.Size];
// 부모님 추적
Pos[,] parent = new Pos[_board.Size, _board.Size];
// (y,x)가는 길을 한 번이라도 발견했는지
// 발견 X -> MaxValue
// 발견 O -> F = G + H
int[,] open = new int[_board.Size, _board.Size];
for (int y = 0; y < _board.Size; y++)
{
for (int x = 0; x < _board.Size; x++)
{
open[y, x] = Int32.MaxValue;
}
}
// 오픈리스트에 있는 정보들 중에서, 가장 좋은 후보를 빠르게 뽑아오기 위한 도구
PriorityQueue<PQNode> pq = new PriorityQueue<PQNode>();
open[PosY, PosX] = 10 * (Math.Abs(_board.DestY - PosY) + Math.Abs(_board.DestX - PosX));
pq.Push(new PQNode() { F = 10* (Math.Abs(_board.DestY - PosY) + Math.Abs(_board.DestX - PosX)), G = 0, Y = PosY, X = PosX });
parent[PosY, PosX] = new Pos(PosY, PosX);
while (pq.Count > 0)
{
// 제일 좋은 후보를 찾는다
PQNode node = pq.Pop();
// 동일한 좌표를 여러 경로를 찾아서, 더 빠른 경로로 인해서 이미 방문된(closed) 경우 스킵
if (closed[node.Y, node.X]) continue;
// 방문한다.
closed[node.Y, node.X] = true;
// 목적지에 도착했으면 바로 종료
if (node.Y == _board.DestY && node.X == _board.DestX) break;
// 상화좌우등 이동할 수 있는 좌표인지 확인해서 예약(Open)한다.
for (int i = 0; i < deltaY.Length; i++)
{
int nextY = node.Y + deltaY[i];
int nextX = node.X + deltaX[i];
// 유효 범위를 벗어 났으면 스킵
if (nextX < 0 || nextX >= _board.Size || nextY < 0 || nextY >= _board.Size) continue;
// 벽으로 막혀서 갈 수 없으면 스킵
if (_board.Tile[nextY, nextX] == Board.TileType.Wall) continue;
// 이미 방문한 곳이면 스킵
if (closed[nextY, nextX]) continue;
// 비용 계산
int g = node.G + cost[i];
int h = 10 * (Math.Abs(_board.DestY - nextY) + Math.Abs(_board.DestX - nextX));
// 다른 경로에서 더 빠른길을 이미 찾았으면 스킵
if (open[nextY, nextX] < g + h) continue;
// 예약 진행
open[nextY, nextX] = g + h;
pq.Push(new PQNode() { F = g + h, G = g, Y = nextY, X = nextX });
parent[nextY, nextX] = new Pos(node.Y, node.X);
}
}
CalcPathFromParent(parent);
}
void CalcPathFromParent(Pos[,] parent)
{
int y = _board.DestY;
int x = _board.DestX;
while (parent[y, x].Y != y || parent[y, x].X != x)
{
_points.Add(new Pos(y, x));
Pos pos = parent[y, x];
y = pos.Y;
x = pos.X;
}
_points.Add(new Pos(y, x));
_points.Reverse();
}
void BFS()
{
int[] deltaY = { -1, 0, 1, 0 };
int[] deltaX = { 0, -1, 0, 1 };
bool[,] found = new bool[_board.Size, _board.Size];
// 어디서 부터 왔는지 기억해야함 부모님의 정보를 넣어줌
Pos[,] parent = new Pos[_board.Size, _board.Size];
// 미리 만들어준 Pos 사용
Queue<Pos> q = new Queue<Pos>();
q.Enqueue(new Pos(PosY, PosX)); // 시작점
found[PosY, PosX] = true;
parent[PosY, PosX] = new Pos(PosY, PosX); // 현재 지점을 저장해둠
while (q.Count > 0)
{
Pos pos = q.Dequeue();
//
int nowY = pos.Y;
int nowX = pos.X;
for (int i = 0; i < 4; i++)
{
int nextY = nowY + deltaY[i];
int nextX = nowX + deltaX[i];
if (nextY < 0 || nextY >= _board.Size || nextX < 0 || nextX >= _board.Size) // 범위를 벗어남
continue;
if (_board.Tile[nextY, nextX] == Board.TileType.Wall) // 벽이면
continue;
if (found[nextY, nextX]) // 이미 방문 했으면
continue;
q.Enqueue(new Pos(nextY, nextX));
found[nextY, nextX] = true; // 찾았다
parent[nextY, nextX] = new Pos(nowY, nowX); // 현재 지점을 저장해줌
}
}
// 도착 지점에서 출발 지점으로 거슬러 올라가며 경로 저장
int y = _board.DestY;
int x = _board.DestX;
// 부모님과 자신의 좌표가 같으면 빠져나옴
// 아니면 루프를 게속 돈다.
while (parent[y, x].Y != y || parent[y, x].X != x)
{
_points.Add(new Pos(y, x));
Pos pos = parent[y, x];// 값을 넣어줌
// 거꾸로감
y = pos.Y;
x = pos.X;
}
// 끝점에 도달하면 while문을 빠져나옴 따라서
// 시작점은 ADD를 안해준 상황
_points.Add(new Pos(y, x)); // 출발점 추가
_points.Reverse(); // 최단 경로로 정렬
}
public void RightHand()
{
// 현재 바라보고 있는 방향을 기존으로 좌표 변화를 나타냄
int[] frontY = new int[] { -1, 0, 1, 0 };
int[] frontX = new int[] { 0, -1, 0, 1 };
int[] rightY = new int[] { 0, -1, 0, 1 };
int[] rightX = new int[] { 1, 0, -1, 0 };
_points.Add(new Pos(PosY, PosX));
// 목표 지점에 도달할 때까지 뺑뺑이를 돌겠다
// 목적지 도착하기 전에는 계속 실행
// 실시간으로 로직 돌리기 전에, 렌더링도 하기 전에, 미리 길을 찾아 보는 것이다.
// 현재 바라보는 방향이 어디냐에 따라 오른손 위치가 다름.(왼쪽을 보고 있는 상태라면 오른손은 절대 기준으로 위쪽일것)
while (PosY != _board.DestY || PosX != _board.DestX)
{
// 1. 현재 바라보는 방향을 기준으로 오른쪽으로 갈 수 있는지 확인
if (_board.Tile[PosY + rightY[_dir], PosX + rightX[_dir]] == Board.TileType.Empty)
{
// 오른쪽으로 가기
// 1. 오른쪽 방향으로 90 도 회전
_dir = (_dir - 1 + 4) % 4;
// 2. 앞으로 한 보 전진
PosY = PosY + frontY[_dir];
PosX = PosX + frontX[_dir];
_points.Add(new Pos(PosY, PosX));
}
// 2. 현재 바라보는 방향을 기준으로 앞 쪽으로 갈 수 있는지
else if (_board.Tile[PosY + frontY[_dir], PosX + frontX[_dir]] == Board.TileType.Empty)
{
// 앞으로 한 보 전진
PosY = PosY + frontY[_dir];
PosX = PosX + frontX[_dir];
_points.Add(new Pos(PosY, PosX));
}
else
{
// 왼쪽 방향으로 90 도 회전 해주고 다음 반복 하러 (반시계방향으로 여러 방향 따져봄)
_dir = (_dir + 1 + 4) % 4;
}
}
}
const int MOVE_TICK = 10; // 10밀리세컨즈 = 0.01 초 마다 움직이게
int _sumTick = 0;
int _lastIndex = 0;
public void Update(int deltaTick)
{
if (_lastIndex >= _points.Count)
{
_lastIndex = 0;
_points.Clear();
_board.Initialize(_board.Size, this);
Initialize(1, 1, _board);
}
_sumTick += deltaTick;
if (_sumTick >= MOVE_TICK) // 이부분은 0.1초마다 실행
{
_sumTick = 0;
PosY = _points[_lastIndex].Y;
PosX = _points[_lastIndex].X;
_lastIndex++;
}
}
}
}
A* 알고리즘은 최단 경로를 찾는 데 사용되는 탐색 알고리즘입니다. 주어진 시작점에서 목표점까지 가는 최적의 경로를 찾으며, 다익스트라 알고리즘과 휴리스틱 함수를 조합하여 효율적으로 동작합니다.
G: 시작점에서 현재 노드까지의 이동 비용H: 현재 노드에서 목적지까지의 예상 비용(휴리스틱, Heuristic)F: F = G + H → 가장 작은 F 값을 가진 노드를 우선적으로 탐색| 알고리즘 | 탐색 방식 |
|---|---|
| 다익스트라 | F = G 기준으로 최단 거리 노드를 방문 |
| A* | F = G + H 기준으로 최적의 경로를 탐색 |
| 차이점 | A*는 H를 고려하여 목적지 방향으로 더 빠르게 탐색 |
Pos 클래스class Pos
{
public Pos(int y, int x) { Y = y; X = x; }
public int Y;
public int X;
}
(Y, X) 좌표 정보를 저장하는 클래스Player 클래스플레이어의 위치 및 A* 알고리즘을 통한 경로 탐색을 담당하는 클래스입니다.
public int PosY { get; private set; }
public int PosX { get; private set; }
Random _random = new Random();
Board _board;
PosY, PosX: 플레이어의 현재 좌표_random: 무작위 동작을 위한 랜덤 객체_board: 게임 맵을 저장하는 객체enum Dir { Up = 0, Left = 1, Down = 2, Right = 3 }
int _dir = (int)Dir.Up;
_dir: 플레이어가 현재 바라보는 방향List<Pos> _points = new List<Pos>();
PQNode 구조체 (우선순위 큐 노드)struct PQNode : IComparable<PQNode>
{
public int F;
public int G;
public int Y;
public int X;
public int CompareTo(PQNode other)
{
return F.CompareTo(other.F);
}
}
F: 총 비용(F = G + H)G: 시작점에서 해당 좌표까지 이동하는 비용Y, X: 좌표 정보CompareTo: F 값이 작은 노드가 우선순위 큐에서 먼저 꺼내짐AStar() 함수 (A* 알고리즘 실행)int[] deltaY = { -1, 0, 1, 0 };
int[] deltaX = { 0, -1, 0, 1 };
int[] cost = { 10, 10, 10, 10 };
deltaY, deltaX: 상하좌우 이동 시 좌표 변화량cost: 이동 비용 (모든 방향 동일)bool[,] closed = new bool[_board.Size, _board.Size];
int[,] open = new int[_board.Size, _board.Size];
Pos[,] parent = new Pos[_board.Size, _board.Size];
PriorityQueue<PQNode> pq = new PriorityQueue<PQNode>();
closed: 이미 방문한 노드 표시open: 최적 경로 비용(F) 저장parent: 각 노드의 부모 노드 저장 (경로 추적용)pq: 우선순위 큐 → F 값이 작은 노드가 우선 탐색됨open[PosY, PosX] = 10 * (Math.Abs(_board.DestY - PosY) + Math.Abs(_board.DestX - PosX));
pq.Push(new PQNode() { F = open[PosY, PosX], G = 0, Y = PosY, X = PosX });
parent[PosY, PosX] = new Pos(PosY, PosX);
G = 0, H = 맨해튼 거리(남은 타일 수 * 10)while (pq.Count > 0)
{
PQNode node = pq.Pop();
if (closed[node.Y, node.X]) continue;
closed[node.Y, node.X] = true;
if (node.Y == _board.DestY && node.X == _board.DestX) break;
F 값이 가장 작은 노드를 꺼냄for (int i = 0; i < deltaY.Length; i++)
{
int nextY = node.Y + deltaY[i];
int nextX = node.X + deltaX[i];
if (nextX < 0 || nextX >= _board.Size || nextY < 0 || nextY >= _board.Size) continue;
if (_board.Tile[nextY, nextX] == Board.TileType.Wall) continue;
if (closed[nextY, nextX]) continue;
int g = node.G + cost[i];
int h = 10 * (Math.Abs(_board.DestY - nextY) + Math.Abs(_board.DestX - nextX));
if (open[nextY, nextX] < g + h) continue;
open[nextY, nextX] = g + h;
pq.Push(new PQNode() { F = g + h, G = g, Y = nextY, X = nextX });
parent[nextY, nextX] = new Pos(node.Y, node.X);
}
G, H, F) 계산 후 우선순위 큐에 추가parent 배열)에 현재 노드를 저장 (경로 추적용)CalcPathFromParent() - 경로 추적void CalcPathFromParent(Pos[,] parent)
{
int y = _board.DestY;
int x = _board.DestX;
while (parent[y, x].Y != y || parent[y, x].X != x)
{
_points.Add(new Pos(y, x));
Pos pos = parent[y, x];
y = pos.Y;
x = pos.X;
}
_points.Add(new Pos(y, x));
_points.Reverse();
}