42/42cursus

[so_long] 1. 맵 파싱하기

jaemjung 2021. 9. 27. 23:16

1. C언어 죽어

과제를 시작하며 일단 mlx라이브러리의 사용법부터 익혀보려고 하였으나...

대략 난감한 오류들이 뭉탱이로 쏟아져 나와서 나중에 다른 사람들의 도움을 받아보기로 하고 일단 맵 파싱을 담당하는 부분부터 완성하기로 했다. 

 

먼저 맵에는 5가지의 요소들이 있는데,

1 == 벽. 플레이어는 벽으로 움직일 수 없다. 또한 맵은 반드시 벽으로 둘러싸여 있어야 한다.

0 == 빈 공간. 플레이어는 빈 공간을 자유롭게 움직일 수 있다.

C == 플레이어가 먹을 수 있는 수집품. 플레이어는 맵에 있는 수집품을 모두 수집하여야 한다.

E == 탈출구. 플레이어는 수집품을 모두 먹고 탈출구로 움직여 게임을 끝낼 수 있다.

P == 플레이어의 시작 위치. 벽에서 시작할 수 없음.

 

따라서 화면을 초기화 하기 전에 먼저 맵 파일을 읽어와 맵이 유효한가를 먼저 체크해야 할 것이다.

일단 맵 파일을 읽어오는 것부터 머리가 아파오기 시작했다.

 

처음에는

1. get_next_line 함수를 이용해 파일에서 한 줄씩 읽어옴
2. 읽어온 함수 2차원 배열에 저장
3. 해당 2차원 배열을 돌며 맵 유효성 체크

라는 식으로 접근하려고 했는데, 생각해보니 C언어에서는 2차원 배열을 malloc 하려면 [sizeof(char *) * 열의 개수] 만큼을 구해야 했고... 이는 곧 파일의 열의 개수를 알아야 한다는 뜻이었다... 그런데 그걸 알아오려면 어쨌든 한 번은 파일 전체를 읽어 열의 개수를 카운트해야 하므로 파일을 한번 열어서 끝까지 탐색하여 \n의 개수를 세고 어쩌고... 그리고 또 그걸 2차원 배열 malloc 한 후 파일을 다시 열어서 get_next_line으로 한 줄씩 읽어가며 유효성을 판단하고 잘못되면 싹 다 free 하고 어쩌고... 저쩌고...

 

생각이 왕창 복잡해지던 찰나에 그냥 연결리스트에 임시로 저장하는 방식으로 파싱을 하기로 했다. 그렇게 하면

1. get_next_line으로 한 줄 읽어오기
2. 해당 라인의 유효성 검사
3. 유효하다면 연결리스트에 저장, 아니라면 에러 출력 후 종료.

처럼 보다 직관적이고 머리가 덜 아픈 순서로 맵을 파싱 할 수 있었다.

맵을 받아온 후 추후에 보다 편리한 맵 조작을 위해 이걸 다시 2차원 배열로 옮겨주기만 하면 되지 않을까?라는 생각으로 접근해 보았다. 

 

요렇게 해주기 위해 일단 맵을 파싱하며 정보를 담아줄 t_map_info 구조체를 만들어주었다.

typedef struct		s_map_info
{
	t_list		*temp_map; //유효성 검사를 끝낸 맵이 담길 연결리스트
	int		height; // 맵의 높이
	int		width; // 맵의 너비
	int		number_of_collectibles; // 맵에 있는 수집품의 갯수
	int		number_of_exit; // 맵에 있는 출구의 갯수
	int		number_of_player; // 맵에 있는 플레이어의 갯수
}			t_map_info;

 

2. 첫 째줄, 중간 줄, 마지막 줄로 나누기

맵이 유효한지 확인하기 위해서는 몇 가지 사항들을 체크해야 하는데,

1. 맵이 '1'로 완전히 둘러 싸여 있는가? -> 맵의 첫째 줄과 마지막 줄이 모두 1이며, 중간 줄의 시작과 마지막이 '1'인가?
2. 맵이 사각형인가? -> 맵의 첫째줄부터 마지막 줄 까지 너비가 모두 같은가?
3. 맵에 플레이어와 수집품과 탈출구가 모두 있는가? -> 'P'는 1개만, 'E'도 1개만, 'C'는 1개 이상 있는가?

와 같은 사항을 체크하여야 했다.

 

순간 뇌까리가 아찔했으나 곰곰이 생각해보니 첫째줄과 마지막 줄을 검사하는 함수와, 중간줄을 검사하는 함수를 나누어 작성한다면 직관적이고 쉽게 문제를 해결할 수 있겠다는 생각이 들었다.

따라서 다음과 같이 3개의 함수들을 작성해주었다.

int	check_map_first_line(char *line, t_map_info *map_info)
{
	int	i;

	i = 0;
	while (line[i] != '\0')
	{
		if (line[i] != '1') // 하나라도 1이 아니면 바로 에러 출력 후 종료
			error_handler("Wrong map format.");
		i++;
	}
	if (i == 0) // 파일이 비어있을 경우도 에러 출력 후 종료
		error_handler("Wrong map format.");
	else
	{
		map_info->width = i; // 맵의 너비를 저장해주고
		map_info->temp_map = ft_lstnew(line); // 연결리스트 초기화 하여 저장
		if (map_info->temp_map == NULL)
			error_handler("Failed to create temp_map.");
	}
	return (0);
}

int	check_map_middle_line(char *line, t_map_info *map_info)
{
	int		i;
	t_list	*tmp;

	i = 0;
	while (line[i] != '\0')
	{
		if (i == 0 && line[i] != '1') // 중간라인에서는 첫번째가 '1'인지 확인
			error_handler("Wrong map format.");
		if (line[i] == 'E')
			map_info->number_of_exit++;
		else if (line[i] == 'C')
			map_info->number_of_collectibles++;
		else if (line[i] == 'P')
			map_info->number_of_player++; // 그 외에는 각 경우에 따라 +1
		else if (i != 0 && !(line[i] == '0' || line[i] == '1'))
			error_handler("Wrong map format."); // 5가지 문자 외에 다른 문자가 있으면 에러출력 후 종료
		i++;
	}
	if (line[i - 1] != '1' || i != map_info->width)
		error_handler("Wrong map format."); // 마지막 글자가 1이 아니거나 맵의 너비가 다를경우 에러 출력 후 종료
	tmp = ft_lstnew(line);
	if (tmp == NULL)
		error_handler("Failed to create temp_map.");
	ft_lstadd_back(&map_info->temp_map, tmp); // line 연결리스트에 추가
	return (0);
}

int check_map_last_line(char *line, t_map_info *map_info)
{
	int i;
	t_list *tmp;
	
	i = 0;
	while (line[i] != '\0')
	{
		if (line[i] != '1')
			error_handler("Wrong map format.");
		i++;
	}
	if (i != map_info->width)
		error_handler("Wrong map format.");
	tmp = ft_lstnew(line);
	ft_lstadd_back(&map_info->temp_map, tmp);
	return (0);
}

그리고 최종적으로 맵을 검사하여 p, c, e의 갯수를 체크하는 함수도 만들어주었다.

int	check_for_last(t_map_info *map_info)
{
	if (map_info->number_of_player > 1 || // 플레이어의 갯수가 1개보다 많거나
		map_info->number_of_collectibles < 1 || //수집품이 0개거나
		map_info->number_of_exit > 1) // 탈출구의 갯수가 1개보다 많으면 에러
		error_handler("Wrong map format.");
	map_info->height = ft_lstsize(map_info->temp_map); // 마지막으로 리스트의 사이즈 == 맵의 높이
    						  	 // info에 저장
	return (0);
}

 

뭔가 중복되는 라인이 많아 보이지만 나는 중복보다 복잡한 게 더 싫다... 가독성이 극악으로 변하고 나중에 설명할 때 너무 힘들어지기 때문에... ㅠㅠ

 

이렇게 작성된 함수들을 이용해 check_map_validity라는 함수를 만들어 최종적으로 완성된 맵을 연결 리스트에 저장할 수 있도록 해줬다.

static int	check_map_validity(int map_fd, t_map_info *map_info)
{
	char	*line;
	int		gnl_check;
	int		first_ln_done;

	first_ln_done = 0;
	gnl_check = get_next_line(map_fd, &line);
	while (gnl_check > 0)
	{
		if (!first_ln_done)
		{
			check_map_first_line(line, map_info);
			first_ln_done = 1;
		}
		else
			check_map_middle_line(line, map_info);
		gnl_check = get_next_line(map_fd, &line);
	}
	if (gnl_check < 0)
		error_handler("Failed to read from map file");
	check_map_last_line(line, map_info);
	check_for_last(map_info);
	return (0);
}

함수 실행이 정상적으로 끝나면 map_info->tmp_map에 유효성 검사가 끝난 맵이 연결리스트 형태로 저장되게 된다. 추후에 화면에 맵을 그리기 위해 사용할때는 2차원 배열으로 옮겨서 사용하려고 계획중이다... 일단은... ㅠㅠ

 

요 다음에는 mlx라이브러리를 사용하는 법부터 차근차근 알아나가야겠다.