So-Long

So Long

So Long is a simple 2D game where the player must collect all collectibles and find an exit to complete the level. The game is developed in C using MLX42, and adaptation of the MiniLibX graphical library, with a focus on handling textures, sprites, and basic gameplay mechanics.


so_long


How to Run

  1. Clone this repository:
    git clone https://github.com/dracudev/So-Long
    
  2. Navigate into the cloned directory and run make. This will compile all the source files and create the game.

    cd So-Long
    make
    
  3. Run the game with a .ber map file:
    ./so_long /map/map1.ber
    
  4. You can use the provided maps or create your own custom maps following the map guidelines.

  5. Additional Makefile Commands:


Project Summary

This project is designed to enhance your skills in C programming and graphical management using MLX42. You will work with event handling, window management, and the display of 2D textures to create a small game. The game involves navigating a character through a maze, collecting items, and reaching an exit.


Features


Gameplay

The goal of the game is to collect every collectible on the map and reach the exit with the least amount of moves possible. The player cannot move through walls and must follow the correct path to complete the level.

Controls:


Map Requirements

The map file passed as an argument to the game must adhere to the following rules:


Bonus Features

Not implemented yet


Credits


Implementation

Error Handling

ft_error

The ft_error function displays an error message prefixed with [ERROR] and terminates the program with a non-zero exit status.

void	ft_error(char *msg)
{
	ft_printf("[ERROR] %s\n", msg);
	exit(1);
}

Parameters:

Functionality:


ft_error_clean

The ft_error_clean function displays an error message prefixed with [ERROR], performs cleanup of allocated resources, and terminates the program with a non-zero exit status.

void	ft_error_clean(char *msg, t_game *game)
{
	ft_printf("[ERROR] %s\n", msg);
	clean_up(game);
	exit(1);
}

Parameters:

Functionality:


Utility

clean_up

The clean_up function releases resources and memory allocated within the game structure, ensuring no memory leaks when the game exits.

void	clean_up(t_game *game)
{
	delete_images(game);
	delete_textures(game);
	if (game->mlx)
		mlx_terminate(game->mlx);
	if (game->map.array || game->map.info)
		free_map(game);
	if (game->fd > 0)
		close(game->fd);
}

Parameters:

Functionality:


line_len

The line_len function calculates the length of a given string until the newline character (\n) is encountered, replacing it with a null terminator (\0) if found.

int	line_len(char *str)
{
	int	i;

	i = 0;
	while (str[i])
	{
		if (str[i] == '\n')
			str[i] = '\0';
		i++;
	}
	return (i);
}

Parameters:

Functionality:


check_args

The check_args function verifies that the correct number of arguments is provided and that the map file has the .ber extension.

void	check_args(int argc, char **argv)
{
	int	map_len;

	if (argc != 2)
		ft_error("Invalid number of arguments.");
	map_len = ft_strlen(argv[1]);
	if (ft_strnstr(&argv[1][map_len - 4], ".ber", 4) == NULL)
		ft_error("Invalid map extension.");
}

Parameters:

Functionality:


init_value

The init_value function initializes the fields of the game structure, setting default values for the game’s starting state and ensuring pointers are NULL before allocation.

void	init_value(t_game *game)
{
	game->position.x = 0;
	game->position.y = 0;
	game->position.move = 0;
	game->map.rows = 0;
	game->map.columns = 0;
	game->map.collect = 0;
	game->map.exit = 0;
	game->map.player = 0;
	game->map.walls = 0;
	game->map.floor = 0;
	game->map.info = NULL;
	game->map.exit_found = 0;
	game->count = 0;
	game->finish_game = 0;
	game->map.array = NULL;
	game->txt = NULL;
	game->img = NULL;
	game->mlx = NULL;
}

Parameters:

Functionality:


Map Parsing

map_calloc

The map_calloc function allocates memory for the map.array in the game structure, ensuring it can store up to 270 rows of map data.

static void	map_calloc(t_game *game)
{
	if (!game->map.array)
	{
		game->map.array = ft_calloc(270, sizeof(char *));
		if (!game->map.array)
			ft_error_clean("Memmory allocation for map failed.", game);
	}
}

Parameters:

Functionality:


map_len

The map_len function updates the column count of the map in the game structure based on the length of a given line and checks if the line exceeds the maximum allowed width.

static void	map_len(t_game *game, char *line, int i)
{
	int	len;

	len = line_len(line);
	if (i == 0)
		game->map.columns = len;
	if (len >= MAP_WIDTH / IMG_W)
		ft_error_clean("Map file too long.", game);
	return ;
}

Parameters:

Functionality:


map_parser

The map_parser function reads lines from a map file, allocates memory for each line, and populates the map.array in the game structure while ensuring the map adheres to defined dimensions.

void	map_parser(t_game *game)
{
	char	*line;
	int		i;

	line = get_next_line(game->fd);
	if (line == NULL)
		ft_error_clean("Map file is empty.", game);
	i = 0;
	while (line && (line[0] != '\n'))
	{
		if (i >= MAP_HEIGHT / IMG_H)
			ft_error_clean("Map file too high.", game);
		if (game->map.array == NULL)
			map_calloc(game);
		game->map.array[i] = malloc(sizeof(char) * (line_len(line) + 1));
		if (!game->map.array[i])
			ft_error_clean("Error allocating map rows", game);
		map_len(game, line, i);
		ft_strlcpy(game->map.array[i], line, game->map.columns + 1);
		i++;
		free(line);
		line = get_next_line(game->fd);
	}
	game->map.rows = i;
	free(line);
	close(game->fd);
}

Parameters:

Functionality:


copy_map

The copy_map function allocates memory for a copy of the map information and fills it with the data from the original map array.

static void	copy_map(t_game *game)
{
	int	i;

	if (!game->map.info)
	{
		game->map.info = (char **)malloc(sizeof(char *) * (game->map.rows + 1));
		if (!game->map.info)
			ft_error_clean("Allocation failed for map rows info.", game);
		i = 0;
		while (i < game->map.rows)
		{
			game->map.info[i] = malloc(sizeof(char) * (game->map.columns + 1));
			if (!game->map.info[i])
				ft_error_clean("Allocation failed for map columns info.", game);
			ft_strlcpy(game->map.info[i], game->map.array[i], \
					game->map.columns + 1);
			i++;
		}
		game->map.info[i] = NULL;
	}
}

Parameters:

Functionality:


Map Checking

is_rectangular

The is_rectangular function checks whether the map in the game structure is rectangular by verifying that each row has the same length as the first row.

static int	is_rectangular(t_game *game)
{
	size_t	len;
	int		i;

	if (!game->map.array || !game->map.array[0])
		ft_error("Map is empty or invalid.");
	len = ft_strlen(game->map.array[0]);
	i = 1;
	while (game->map.array[i])
	{
		if (ft_strlen(game->map.array[i]) != len)
			ft_error("Map is not rectangular.");
		i++;
	}
	return (0);
}

Parameters:

Functionality:


is_surrounded_by_walls

The is_surrounded_by_walls function checks if the map is correctly enclosed by walls ('1') on all sides, ensuring that the first and last rows, as well as the first and last columns of each row, are bounded by wall characters.

static int	is_surrounded_by_walls(t_game *game)
{
	int	i;
	int	last_row;

	if (!game->map.array || !game->map.array[0])
		ft_error("Map is empty or invalid.");
	last_row = 0;
	while (game->map.array[last_row])
		last_row++;
	last_row--;
	i = 0;
	while (game->map.array[0][i] && game->map.array[last_row][i])
	{
		if (game->map.array[0][i] != '1' || game->map.array[last_row][i] != '1')
			ft_error("Top or bottom row not enclosed by walls.");
		i++;
	}
	i = 0;
	while (game->map.array[i])
	{
		if (game->map.array[i][0] != '1'
				|| game->map.array[i][ft_strlen(game->map.array[i]) - 1] != '1')
			ft_error("Left or right not enclosed by walls.");
		i++;
	}
	return (0);
}

Parameters:

Functionality:


invalid_components

The invalid_components function checks the components of the map at a specific position to ensure they are valid, and it tracks the player’s position if found.

static void	invalid_components(t_game *game, int i, int j)
{
	if (game->map.array[i][j] != 'E' && game->map.array[i][j] != 'P'
			&& game->map.array[i][j] != 'C' && game->map.array[i][j] != 'E'
			&& game->map.array[i][j] != '1' && game->map.array[i][j] != '0')
		ft_error("Invalid components.");
	if (game->map.array[i][j] == 'P')
	{
		game->map.player++;
		game->position.x = j;
		game->position.y = i;
	}
}

Parameters:

Functionality:


validate_map_components

The validate_map_components function checks the components of the map for validity, ensuring the correct number of exits, players, and collectibles are present.

static int	validate_map_components(t_game *game)
{
	int	i;
	int	j;

	i = 0;
	while (game->map.array[i])
	{
		j = 0;
		while (game->map.array[i][j])
		{
			if (game->map.array[i][j] == 'E')
				game->map.exit++;
			if (game->map.array[i][j] == 'C')
				game->map.collect++;
			invalid_components(game, i, j);
			j++;
		}
		i++;
	}
	if (game->map.exit != 1 || game->map.player != 1 || game->map.collect < 1)
		ft_error("Invalid number of components.");
	return (0);
}

Parameters:

Functionality:


flood_fill

The flood_fill function recursively marks reachable areas of the map starting from a given position, checking for collectibles and the exit.

void	flood_fill(t_game *game, int x, int y, int *collect)
{
	if (x < 0 || y < 0 || x >= game->map.columns || y >= game->map.rows
		|| game->map.info[y][x] == '1' || game->map.info[y][x] == 'V')
		return ;
	if (game->map.info[y][x] == 'C')
		(*collect)--;
	if (game->map.info[y][x] == 'E')
		game->map.exit_found = 1;
	game->map.info[y][x] = 'V';
	flood_fill(game, x + 1, y, collect);
	flood_fill(game, x - 1, y, collect);
	flood_fill(game, x, y + 1, collect);
	flood_fill(game, x, y - 1, collect);
}

Parameters:

Functionality:


pathfinding

The pathfinding function initiates the process to find a valid path in the map, ensuring all collectibles and the exit are reachable.

int	pathfinding(t_game *game)
{
	int	collect;

	collect = game->map.collect;
	copy_map(game);
	flood_fill(game, game->position.x, game->position.y, &collect);
	if (collect > 0 || !game->map.exit_found)
		ft_error("Not all collectibles or exit are reachable.");
	return (0);
}

Parameters:

Functionality:


map_checker

The map_checker function validates the map structure and its components, ensuring that it adheres to the required specifications.

int	map_checker(t_game *game)
{
	if (is_rectangular(game) == 1)
		return (1);
	if (is_surrounded_by_walls(game) == 1)
		return (1);
	if (validate_map_components(game) == 1)
		return (1);
	if (pathfinding(game) == 1)
		return (1);
	return (0);
}

Parameters:

Functionality:


Map Cleanup

free_map_array

The free_map_array function deallocates memory for a 2D array representing the map.

static void	free_map_array(char **map, int columns)
{
	int	i;

	if (!map || !*map)
		ft_error("Map data not found or invalid.");
	i = 0;
	while (i < columns && map[i] != NULL)
	{
		free(map[i]);
		i++;
	}
	free(map);
}

Parameters:

Functionality:


free_map_info

The free_map_info function deallocates memory for a 2D array containing map information.

static void	free_map_info(char **vector)
{
	int	i;

	if (!vector || !*vector)
		ft_error("Invalid map data.");
	i = 0;
	while (vector[i] != NULL)
	{
		free(vector[i]);
		i++;
	}
	free(vector);
}

Parameters:

Functionality:


free_map

The free_map function is responsible for freeing the allocated memory used for the map data in the t_game structure.

void	free_map(t_game *game)
{
	free_map_array(game->map.array, game->map.columns);
	if (game->map.info != NULL)
		free_map_info(game->map.info);
}

Parameters:

Functionality:


Graphics

init_textures

The init_textures function initializes the textures for the game by allocating memory and loading the necessary image files.

static int	init_textures(t_game *game)
{
	game->txt = ft_calloc(1, sizeof(t_txt));
	if (!game->txt)
		ft_error_clean("Failled allocation for textures", game);
	game->txt->floor = mlx_load_png("./assets/floor1.png");
	game->txt->wall = mlx_load_png("./assets/wall.png");
	game->txt->exit1 = mlx_load_png("./assets/exit1.png");
	game->txt->exit2 = mlx_load_png("./assets/exit2.png");
	game->txt->pnj = mlx_load_png("./assets/pnj.png");
	game->txt->collect = mlx_load_png("./assets/collect.png");
	if (!game->txt->floor || !game->txt->wall || !game->txt->exit1
		|| !game->txt->exit2 || !game->txt->pnj || !game->txt->collect)
		ft_error_clean("Failed loading textures", game);
	return (0);
}

Parameters:

Functionality:


init_images

The init_images function initializes the images for the game by allocating memory for the image structure and converting loaded textures into images.

static int	init_images(t_game *game)
{
	game->img = ft_calloc(1, sizeof(t_img));
	if (!game->img)
		ft_error_clean("Failed allocation for images", game);
	game->img->floor = mlx_texture_to_image(game->mlx, game->txt->floor);
	game->img->wall = mlx_texture_to_image(game->mlx, game->txt->wall);
	game->img->exit1 = mlx_texture_to_image(game->mlx, game->txt->exit1);
	game->img->exit2 = mlx_texture_to_image(game->mlx, game->txt->exit2);
	game->img->pnj = mlx_texture_to_image(game->mlx, game->txt->pnj);
	game->img->collect = mlx_texture_to_image(game->mlx, game->txt->collect);
	if (!game->img->floor || !game->img->wall || !game->img->exit1
		|| !game->img->exit2 || !game->img->pnj || !game->img->collect)
		ft_error_clean("Failed creating images from textures", game);
	delete_textures(game);
	return (0);
}

Parameters:

Functionality:


draw_map

The draw_map function is responsible for rendering the game map in the game window by drawing different images based on the map array.

static void	draw_map(t_game *game)
{
	int	x;
	int	y;

	y = 0;
	while (game->map.array[y])
	{
		x = 0;
		while (game->map.array[y][x])
		{
			mlx_image_to_window(game->mlx, game->img->floor, \
					x * IMG_W, y * IMG_H);
			if (game->map.array[y][x] == '1')
				mlx_image_to_window(game->mlx, game->img->wall, \
						x * IMG_W, y * IMG_H);
			if (game->map.array[y][x] == 'E')
			{
				mlx_image_to_window(game->mlx, game->img->exit2, \
						x * IMG_W, y * IMG_H);
				mlx_image_to_window(game->mlx, game->img->exit1, \
						x * IMG_W, y * IMG_H);
			}
			x++;
		}
		y++;
	}
}

Parameters:

Functionality:

Note: The drawing order is important to ensure that the floor is rendered first before walls or exits.


draw_items

The draw_items function is responsible for rendering special game items such as collectibles and the player character on the game map.

static void	draw_items(t_game *game)
{
	int	x;
	int	y;

	y = 0;
	while (game->map.array[y])
	{
		x = 0;
		while (game->map.array[y][x])
		{
			if (game->map.array[y][x] == 'C')
				mlx_image_to_window(game->mlx, game->img->collect, \
						x * IMG_W, y * IMG_H);
			if (game->map.array[y][x] == 'P')
				mlx_image_to_window(game->mlx, game->img->pnj, \
						x * IMG_W, y * IMG_H);
			x++;
		}
		y++;
	}
}

Parameters:

Functionality:

Note: This function should be called after the draw_map function to ensure that items are rendered on top of the floor and walls, maintaining the correct visual hierarchy in the game window.


init_graphics

The init_graphics function initializes all the previous functions.

void	init_graphics(t_game *game)
{
	init_textures(game);
	init_images(game);
	draw_map(game);
	draw_items(game);
}


delete_textures

The delete_textures function is responsible for deallocating memory and cleaning up the texture resources used in the game. This helps prevent memory leaks by ensuring that all allocated textures are properly freed when they are no longer needed.

void	delete_textures(t_game *game)
{
	if (game->txt)
	{
		if (game->txt->floor)
			mlx_delete_texture(game->txt->floor);
		if (game->txt->wall)
			mlx_delete_texture(game->txt->wall);
		if (game->txt->exit1)
			mlx_delete_texture(game->txt->exit1);
		if (game->txt->exit2)
			mlx_delete_texture(game->txt->exit2);
		if (game->txt->pnj)
			mlx_delete_texture(game->txt->pnj);
		if (game->txt->collect)
			mlx_delete_texture(game->txt->collect);
		free(game->txt);
		game->txt = NULL;
	}
}

Parameters:

Functionality:


delete_images

The delete_images function is responsible for deallocating the image resources used in the game. Properly managing image memory is crucial to avoid memory leaks and ensure efficient resource usage during the game’s lifecycle.

void	delete_images(t_game *game)
{
	if (game->img)
	{
		if (game->img->floor)
			mlx_delete_image(game->mlx, game->img->floor);
		if (game->img->wall)
			mlx_delete_image(game->mlx, game->img->wall);
		if (game->img->exit1)
			mlx_delete_image(game->mlx, game->img->exit1);
		if (game->img->exit2)
			mlx_delete_image(game->mlx, game->img->exit2);
		if (game->img->pnj)
			mlx_delete_image(game->mlx, game->img->pnj);
		if (game->img->collect)
			mlx_delete_image(game->mlx, game->img->collect);
		free(game->img);
		game->img = NULL;
	}
}

Parameters:

Functionality:


Hooks

move_functions

The movement functions (move_up, move_down, move_right, move_left) update the position of the player character in the game based on user input. Each function checks if the next tile in the respective direction is not a wall before moving the character.

static void	move_up(t_game *game, int y, int x)
{
	if (game->map.array[(y - IMG_W) / IMG_W][x / IMG_H] != '1')
	{
		game->img->pnj->instances[0].y -= IMG_W;
		game->position.move++;
		ft_printf("MOVES: %i\n", game->position.move);
	}
}

static void	move_down(t_game *game, int y, int x)
{
	if (game->map.array[(y + IMG_W) / IMG_W][x / IMG_H] != '1')
	{
		game->img->pnj->instances[0].y += IMG_W;
		game->position.move++;
		ft_printf("MOVES: %i\n", game->position.move);
	}
}

static void	move_right(t_game *game, int y, int x)
{
	if (game->map.array[y / IMG_W][(x + IMG_H) / IMG_H] != '1')
	{
		game->img->pnj->instances[0].x += IMG_H;
		game->position.move++;
		ft_printf("MOVES: %i\n", game->position.move);
	}
}

static void	move_left(t_game *game, int y, int x)
{
	if (game->map.array[y / IMG_W][(x - IMG_H) / IMG_H] != '1')
	{
		game->img->pnj->instances[0].x -= IMG_H;
		game->position.move++;
		ft_printf("MOVES: %i\n", game->position.move);
	}
}

Parameters:

Functionality:


my_key_hook

The my_key_hook function handles keyboard input to control the movement of the player character and manage game events. It checks for specific key presses and calls the appropriate movement functions or triggers game-related actions.

void	my_key_hook(mlx_key_data_t keydata, void *param)
{
	t_game	*game;
	int		*y;
	int		*x;

	game = param;
	y = &game->img->pnj->instances[0].y;
	x = &game->img->pnj->instances[0].x;
	if ((keydata.key == MLX_KEY_W && keydata.action == MLX_PRESS)
		|| (keydata.key == MLX_KEY_UP && keydata.action == MLX_PRESS))
		move_up(game, *y, *x);
	if ((keydata.key == MLX_KEY_D && keydata.action == MLX_PRESS)
		|| (keydata.key == MLX_KEY_RIGHT && keydata.action == MLX_PRESS))
		move_right(game, *y, *x);
	if ((keydata.key == MLX_KEY_S && keydata.action == MLX_PRESS)
		|| (keydata.key == MLX_KEY_DOWN && keydata.action == MLX_PRESS))
		move_down(game, *y, *x);
	if ((keydata.key == MLX_KEY_A && keydata.action == MLX_PRESS)
		|| (keydata.key == MLX_KEY_LEFT && keydata.action == MLX_PRESS))
		move_left(game, *y, *x);
	if (keydata.key == MLX_KEY_ESCAPE && keydata.action == MLX_PRESS)
		mlx_close_window(game->mlx);
	pick_collect(game, *y, *x);
	finish_game(game, *y, *x);
}

Parameters:

Functionality:


Logic

del_collect

The del_collect function removes a collectible item from the game when the player character interacts with it. It updates the collectible’s state to disabled, effectively making it no longer visible or collectible.

static void	del_collect(t_game *game, int y, int x)
{
	int	i;
	int	collects;

	i = 0;
	collects = game->map.collect;
	while (i < collects)
	{
		if (((game->img->collect->instances[i].y == y)
				&& (game->img->collect->instances[i].x == x))
			&& (game->img->collect->instances[i].enabled == true))
		{
			collects--;
			game->img->collect->instances[i].enabled = false;
			return ;
		}
		i++;
	}
}

Parameters:

Functionality:


pick_collect

The pick_collect function checks if the player character has collected a collectible item at its current position. If so, it updates the game state accordingly.

void	pick_collect(t_game *game, int y, int x)
{
	if (game->map.array[y / IMG_W][x / IMG_H] == 'C')
	{
		del_collect(game, y, x);
		game->map.array[y / IMG_W][x / IMG_H] = '0';
		game->count++;
		if (game->count == game->map.collect)
			game->img->exit1->instances->enabled = false;
	}
}

Parameters:

Functionality:


finish_game

The finish_game function checks if the player character has reached the exit point of the game. If the player has collected all necessary items, it triggers the end of the game.

void	finish_game(t_game *game, int y, int x)
{
	if (game->map.array[y / IMG_W][x / IMG_H] == 'E')
	{
		if (game->count == game->map.collect)
			mlx_close_window(game->mlx);
	}
}

Parameters:

Functionality:


Main

so_long

The so_long or main function serves as the entry point for the game application. It initializes the game, processes command-line arguments, and sets up the graphical interface.

int	main(int argc, char **argv)
{
	t_game	game;

	check_args(argc, argv);
	game.fd = open(argv[1], O_RDONLY);
	if (game.fd < 0 || game.fd == 0)
		ft_error("Failed to open file");
	init_value(&game);
	map_parser(&game);
	if (map_checker(&game) == 0)
	{
		mlx_set_setting(MLX_STRETCH_IMAGE, true);
		game.mlx = mlx_init(IMG_W * game.map.columns, \
				IMG_H * game.map.rows, "so_long", true);
		if (!game.mlx)
		{
			ft_error_clean("Fail initializing MLX", &game);
			return (1);
		}
		init_graphics(&game);
		mlx_key_hook(game.mlx, &my_key_hook, &game.mlx);
		mlx_loop(game.mlx);
	}
	clean_up(&game);
	return (0);
}

Parameters:

Functionality:

Theme  Moonwalk