minden/source/main.c
Juhani Krekelä c56f624f1e aaaaaa
2026-10-25 15:56:20 +02:00

740 lines
19 KiB
C

#include <assert.h>
#include <stdio.h>
#include <maxmod9.h>
#include <nds.h>
#include "background.h"
#include "sprite.h"
#include "soundbank.h"
#include "soundbank_bin.h"
#include "constants.h"
#include "units.h"
#include "types.h"
#include "ptrmath.h"
#include "credits.h"
constexpr int playfield_margin = 1 * tile_side;
constexpr struct rect playfield = {
{playfield_margin, playfield_margin},
SCREEN_WIDTH - 2 * playfield_margin,
SCREEN_HEIGHT - 2 * playfield_margin
};
constexpr int screen_height_chars = SCREEN_HEIGHT / character_side;
constexpr struct rect grid_rect = {
{0, 0},
SCREEN_WIDTH / tile_side - 2,
SCREEN_HEIGHT / tile_side - 2
};
enum bg_tiles grid[grid_rect.h][grid_rect.w];
int undeployed_units[PLAYERS_COUNT][UNIT_TYPES_COUNT] = {
[P1] = {
[UNIT_GRASS] = 0,
[UNIT_WALKER] = 2,
},
[P2] = {
[UNIT_GRASS] = 5,
[UNIT_WALKER] = 6,
},
};
enum unit_types unit_to_place;
int left_to_deploy(enum players player) {
int left = 0;
for (int i = 0; i < UNIT_TYPES_COUNT; i++) {
left += undeployed_units[player][i];
}
return left;
}
bool can_player_deploy(struct pos pos, enum players player, enum unit_types type) {
return (grid[pos.y][pos.x] == PLAYFIELD || grid[pos.y][pos.x] == player_playfield_tile[player]) &&
undeployed_units[player][type] > 0;
}
int mainBg, subBg;
u16 *arrows[9];
struct tile tiles[BG_TILES_COUNT];
int arrow_index_from_direction(struct move dir) {
auto clamped = move_clamp_to_1(dir);
return (clamped.dy + 1) * 3 + (clamped.dx + 1);
}
struct pos pos_from_touchPosition(touchPosition touch_pos) {
return (struct pos){touch_pos.px, touch_pos.py};
}
struct pos hit_tile(struct pos screen_point) {
auto playfield_point = inner_coords(playfield, screen_point);
return tile_from_point(playfield_point);
}
struct pos tile_on_screen(struct pos tile) {
auto on_playfield = tile_top_left(tile);
return outer_coords(playfield, on_playfield);
}
void sprite_frame(u16 *tile_memory, int frame_num) {
constexpr int frame_size = 16 * 16;
memcpy(tile_memory, offset_ptr(spriteTiles, frame_num * frame_size), frame_size);
}
u16 *allocate_sprite_graphics(void) {
return oamAllocateGfx(&oamMain, SpriteSize_16x16, SpriteColorFormat_256Color);
}
u16 *unit_sprites[UNIT_TYPES_COUNT];
void load_unit_sprites(void) {
for (int i = 0; i < UNIT_TYPES_COUNT; i++) {
unit_sprites[i] = allocate_sprite_graphics();
sprite_frame(unit_sprites[i], unit_types[i].sprite);
}
}
void init_graphics_sub(void) {
videoSetModeSub(MODE_0_2D);
vramSetBankC(VRAM_C_SUB_BG);
subBg = bgInitHiddenSub(0, BgType_Text8bpp, BgSize_T_256x256, 0, 1);
memcpy(BG_PALETTE_SUB, backgroundPal, backgroundPalLen);
memcpy(bgGetGfxPtr(subBg), backgroundTiles, backgroundTilesLen);
}
constexpr int pen_width = UNIT_TYPES_COUNT;
constexpr int pen_height = 8;
void draw_pen(u16 *map, struct pos pos, enum players player) {
constexpr enum bg_tiles tiles_base[PLAYERS_COUNT] = {
[P1] = P1_PEN_TILES_START,
[P2] = P2_PEN_TILES_START,
};
enum bg_tiles b = tiles_base[player];
put_tile_at(map, (struct pos){pos.x, pos.y}, tiles[b + TOP_LEFT_CORNER]);
for (int dx = 0; dx < pen_width; dx++) {
put_tile_at(map, (struct pos){pos.x + 1 + dx, pos.y}, tiles[b + TOP_EDGE]);
}
put_tile_at(map, (struct pos){pos.x + 1 + pen_width, pos.y}, tiles[b + TOP_RIGHT_CORNER]);
for (int dy = 0; dy < pen_height; dy++) {
put_tile_at(map, (struct pos){pos.x, pos.y + 1 + dy}, tiles[b + LEFT_EDGE]);
for (int dx = 0; dx < pen_width; dx++) {
if (undeployed_units[player][dx] >= dy + 1) {
put_tile_at(map, (struct pos){pos.x + 1 + dx, pos.y + 1 + dy}, tiles[unit_types[dx].sub_tiles[player]]);
} else {
put_tile_at(map, (struct pos){pos.x + 1 + dx, pos.y + 1 + dy}, tiles[b + EMPTY]);
}
}
put_tile_at(map, (struct pos){pos.x + 1 + pen_width, pos.y + 1 + dy}, tiles[b + RIGHT_EDGE]);
}
put_tile_at(map, (struct pos){pos.x, pos.y + pen_height}, tiles[b + BOTTOM_LEFT_CORNER]);
for (int dx = 0; dx < pen_width; dx++) {
put_tile_at(map, (struct pos){pos.x + 1 + dx, pos.y + pen_height}, tiles[b + BOTTOM_EDGE]);
}
put_tile_at(map, (struct pos){pos.x + 1 + pen_width, pos.y + pen_height}, tiles[b + BOTTOM_RIGHT_CORNER]);
}
void update_bg_sub(void) {
u16 *map = bgGetMapPtr(subBg);
for (int y = 0; y < screen_height_chars / 2; y++) {
for (int x = 0; x < bg_width_chars / 2; x++) {
put_tile_at(map, (struct pos){x, y}, tiles[SUB_SCREEN_BG]);
}
}
put_tile_at(map, (struct pos){1, 10 - pen_height}, tiles[P1_LABEL_LEFT]);
put_tile_at(map, (struct pos){2, 10 - pen_height}, tiles[P1_LABEL_RIGHT]);
put_tile_at(map, (struct pos){14, 10 - pen_height}, tiles[P2_LABEL_LEFT]);
put_tile_at(map, (struct pos){15, 10 - pen_height}, tiles[P2_LABEL_RIGHT]);
put_tile_at(map, (struct pos){7, 1}, tiles[TITLE_1]);
put_tile_at(map, (struct pos){8, 1}, tiles[TITLE_2]);
put_tile_at(map, (struct pos){7, 2}, tiles[TITLE_3]);
put_tile_at(map, (struct pos){8, 2}, tiles[TITLE_4]);
draw_pen(map, (struct pos){0, 11 - pen_height}, P1);
draw_pen(map, (struct pos){14 - pen_width, 11 - pen_height}, P2);
if (left_to_deploy(P1) > 0) {
put_tile_at(map, (struct pos){1 + unit_to_place, 11}, tiles[P1_PEN_OPEN_BOTTOM]);
}
}
void init_graphics(void) {
lcdMainOnBottom();
videoSetMode(MODE_0_2D);
vramSetBankA(VRAM_A_MAIN_BG);
vramSetBankB(VRAM_B_MAIN_SPRITE);
mainBg = bgInitHidden(0, BgType_Text8bpp, BgSize_T_256x256, 0, 1);
memcpy(BG_PALETTE, backgroundPal, backgroundPalLen);
memcpy(bgGetGfxPtr(mainBg), backgroundTiles, backgroundTilesLen);
for (int i = 0; i < BG_TILES_COUNT; i++) {
int y = i / (bg_width_chars / 2);
int x = i % (bg_width_chars / 2);
int top_left_character = 2 * y * bg_width_chars + 2 * x;
tiles[i] = get_tile(backgroundMap + top_left_character);
}
oamInit(&oamMain, SpriteMapping_1D_32, false);
memcpy(SPRITE_PALETTE, spritePal, spritePalLen);
load_unit_sprites();
for (int i = 0; i < 9; i++) {
arrows[i] = allocate_sprite_graphics();
sprite_frame(arrows[i], ARROWS_START + i);
}
}
void update_bg(void) {
for (int y = 0; y < screen_height_chars; y += 2) {
u16 *line = bgGetMapPtr(mainBg) + y * bg_width_chars;
for (int x = 0; x < bg_width_chars; x += 2) {
if (x == 0 || y == 0 || x == bg_width_chars - 2 || y == screen_height_chars - 2) {
put_tile(line + x, tiles[BORDER]);
} else {
int grid_x = x / 2 - 1;
int grid_y = y / 2 - 1;
put_tile(line + x, tiles[grid[grid_y][grid_x]]);
}
}
}
}
void set_sprite_attributes(int sprite_id, struct pos top_left, u16 *graphics, bool hidden) {
oamSet(&oamMain,
sprite_id,
top_left.x, top_left.y,
0, // Priority
0, // Palette index
SpriteSize_16x16, SpriteColorFormat_256Color,
graphics,
-1, false, // Affine info (not used)
hidden,
false, false, // H, V flip
false // Mosaic
);
}
void place_sprite(int sprite_id, struct pos top_left, u16 *graphics) {
set_sprite_attributes(sprite_id, top_left, graphics, false);
}
void hide_sprite(int sprite_id) {
set_sprite_attributes(sprite_id, (struct pos){}, NULL, true);
}
void play_effect_oneshot(mm_word effect, struct pos at) {
int panning = 256 * at.x / grid_rect.w;
mm_sfxhand h = mmEffect(effect);
mmEffectPanning(h, panning);
mmEffectVolume(h, 255);
mmEffectRate(h, 1024);
}
struct moving_unit {
struct pos on_screen;
struct move direction;
int dist_left;
int speed;
enum unit_types type;
enum players player;
};
constexpr int moving_units_max = 1;
struct moving_unit moving_units[moving_units_max];
void place_unit(struct pos tile, enum players player, enum unit_types type) {
grid[tile.y][tile.x] = unit_types[type].tiles[player];
}
bool is_unit_on_tile(enum bg_tiles tile, enum unit_types type) {
for (int player = 0; player < PLAYERS_COUNT; player++) {
if (tile == unit_types[type].tiles[player]) { return true; }
}
return false;
}
static bool is_tile_unoccupied(enum bg_tiles tile) {
return tile == P1_PLAYFIELD || tile == P2_PLAYFIELD || tile == PLAYFIELD;
}
void unit_has_arrived(struct pos to, enum unit_types type, enum players player) {
struct pos tile = hit_tile(to);
if (!is_tile_unoccupied(grid[tile.y][tile.x])) {
for (int i = 0; i < UNIT_TYPES_COUNT; i++) {
if (is_unit_on_tile(grid[tile.y][tile.x], i)) {
undeployed_units[player][i]++;
}
}
}
place_unit(tile, player, type);
}
int animate_moving_units(void) {
int units_still_moving = 0;
for (int i = 0; i < moving_units_max; i++) {
auto p = &moving_units[i];
if (p->dist_left > 0) {
units_still_moving++;
int to_move_this_frame = p->speed;
while (to_move_this_frame > 0 && p->dist_left > 0) {
p->on_screen = offset_pos(p->on_screen, p->direction);
p->dist_left--;
to_move_this_frame--;
}
place_sprite(1, p->on_screen, unit_sprites[p->type]);
if (p->dist_left == 0) {
units_still_moving--;
unit_has_arrived(p->on_screen, p->type, p->player);
hide_sprite(1);
}
}
}
return units_still_moving;
}
void move_unit(struct pos from, struct pos to, enum unit_types type, enum players player) {
if (grid[from.y][from.x] == unit_types[type].tiles[P1]) {
grid[from.y][from.x] = P1_PLAYFIELD;
} else if (grid[from.y][from.x] == unit_types[type].tiles[P2]) {
grid[from.y][from.x] = P2_PLAYFIELD;
} else {
grid[from.y][from.x] = PLAYFIELD;
}
for (int i = 0; i < moving_units_max; i++) {
auto p = &moving_units[i];
if (p->dist_left == 0) {
play_effect_oneshot(SFX_WALK, from);
p->on_screen = tile_on_screen(from);
p->direction = move_clamp_to_1(pos_difference(to, from));
p->dist_left = tile_side;
p->speed = 1;
p->type = type;
p->player = player;
return;
}
}
// No free spot on the list, just teleport it there
unit_has_arrived(tile_on_screen(to), type, player);
}
bool is_move_allowed(struct pos from_tile, struct pos towards_tile, enum players player) {
auto drag_direction = move_clamp_to_1(pos_difference(towards_tile, from_tile));
auto target_tile = offset_pos(from_tile, drag_direction);
enum bg_tiles *from = &grid[from_tile.y][from_tile.x];
enum bg_tiles *to = &grid[target_tile.y][target_tile.x];
if (
*from == unit_types[UNIT_WALKER].tiles[player] &&
(is_tile_unoccupied(*to) || *to == GRASS_ON_PLAYFIELD)
) {
return true;
}
return false;
}
bool do_player_move(struct pos drag_from_tile, struct pos drag_to_tile, enum players player) {
auto drag_direction = move_clamp_to_1(pos_difference(drag_to_tile, drag_from_tile));
auto target_tile = offset_pos(drag_from_tile, drag_direction);
if (is_move_allowed(drag_from_tile, drag_to_tile, player)) {
for (int i = 0; i < UNIT_TYPES_COUNT; i++) {
if (grid[drag_from_tile.y][drag_from_tile.x] == unit_types[i].tiles[player]) {
move_unit(drag_from_tile, target_tile, i, player);
}
}
return true;
} else {
play_effect_oneshot(SFX_KNOCK, target_tile);
return false;
}
}
const char *player_names[PLAYERS_COUNT] = {
[P1] = "Player",
[P2] = "CPU",
};
const char *unit_names[UNIT_TYPES_COUNT] = {
[UNIT_GRASS] = "Grass",
[UNIT_WALKER] = "Walker",
};
void deploy_unit(struct pos tile, enum players player, enum unit_types type) {
assert(is_tile_unoccupied(grid[tile.y][tile.x]));
assert(undeployed_units[player][type] > 0);
play_effect_oneshot(SFX_DRAG, tile);
place_unit(tile, player, type);
undeployed_units[player][type]--;
}
void undeploy_unit(struct pos tile, enum players player, enum unit_types type) {
assert(grid[tile.y][tile.x] == unit_types[type].tiles[player]);
undeployed_units[player][type]++;
grid[tile.y][tile.x] = PLAYFIELD;
}
constexpr struct move directions[8] = {
{-1, -1}, {0, -1}, {1, -1},
{-1, 0}, {1, 0},
{-1, 1}, {0, 1}, {1, 1},
};
struct maybe_pos find_tile(bool (*predicate)(enum bg_tiles), struct pos start) {
for (int dy = 0; dy < grid_rect.h; dy++) {
int y = (start.y + dy) % grid_rect.h;
for (int dx = 0; dx < grid_rect.w; dx++) {
int x = (start.x + dx) % grid_rect.w;
if (predicate(grid[y][x])) {
return (struct maybe_pos){IS, {x, y}};
}
}
}
return (struct maybe_pos){ISNAE};
}
void for_tile(bool (*func)(struct pos), struct pos start) {
for (int dy = 0; dy < grid_rect.h; dy++) {
int y = (start.y + dy) % grid_rect.h;
for (int dx = 0; dx < grid_rect.w; dx++) {
int x = (start.x + dx) % grid_rect.w;
if (func((struct pos){x, y})) {
return;
}
}
}
}
bool is_tile_player_walker(enum bg_tiles tile) {
return tile == unit_types[UNIT_WALKER].tiles[P1];
}
bool is_tile_player_playfield(enum bg_tiles tile) {
return tile == P1_PLAYFIELD;
}
struct pos random_grid_square(void) {
return (struct pos){rand() % grid_rect.w, rand() % grid_rect.h};
}
bool move_ai_unit(struct pos from) {
for (int type = 0; type < UNIT_TYPES_COUNT; type++) {
if (grid[from.y][from.x] == unit_types[type].tiles[P2]) {
struct maybe_pos target = {.tag = ISNAE};
if (target.tag == ISNAE) {
target = find_tile(is_tile_player_walker, random_grid_square());
}
if (target.tag == ISNAE) {
target = find_tile(is_tile_player_playfield, random_grid_square());
}
if (target.tag == IS) {
auto dir = move_clamp_to_1(pos_difference(target.payload, from));
auto to = offset_pos(from, dir);
if (!is_move_allowed(from, to, P2)) continue;
move_unit(from, to, type, P2);
return true;
}
int dir_start = rand() % 8;
for (int ddir = 0; ddir < 8; ddir++) {
int dir = (dir_start + ddir) % 8;
auto to = offset_pos(from, directions[dir]);
if (!is_within(grid_rect, to)) continue;
if (!is_move_allowed(from, to, P2)) continue;
move_unit(from, to, type, P2);
return true;
}
}
}
return false;
}
void ai_turn(void) {
if (left_to_deploy(P2) > 0) {
int type;
struct maybe_pos maybe_tile;
do {
type = rand() % UNIT_TYPES_COUNT;
maybe_tile = find_tile(is_tile_unoccupied, random_grid_square());
assert(maybe_tile.tag == IS);
} while(!can_player_deploy(maybe_tile.payload, P2, type));
deploy_unit(maybe_tile.payload, P2, type);
} else {
for_tile(move_ai_unit, random_grid_square());
}
}
#define ERRX(...) { printf(__VA_ARGS__); for (;;) { swiWaitForVBlank(); } }
#define LOAD_EFFECT(effect) { \
ret = mmLoadEffect(effect); \
if (ret != 0) { \
ERRX("mmLoadEffect(" #effect "): %d\n", ret); \
} \
}
void autoselect_unit_to_deploy(void) {
if (undeployed_units[P1][unit_to_place] > 0) {
return;
}
if (left_to_deploy(P1) > 0) {
for (int d = 0; d < UNIT_TYPES_COUNT; d++) {
int i = (unit_to_place + d) % UNIT_TYPES_COUNT;
if (undeployed_units[P1][i] > 0) {
unit_to_place = i;
return;
}
}
}
}
void for_neighbours(void* ctx, void (*each)(void*, struct pos), struct pos pos) {
for (int y = 0; y < grid_rect.h; y++) {
for (int x = 0; x < grid_rect.w; x++) {
for (int i = 0; i < 8; i++) {
auto dir = directions[i];
auto neighbour = offset_pos(pos, dir);
if (is_within(grid_rect, neighbour)) {
each(ctx, neighbour);
}
}
}
}
}
struct destroy_adjacent_walkers_ctx {
enum players player;
bool any_found;
};
void destroy_adjacent_walkers(void *ctx, struct pos pos) {
struct destroy_adjacent_walkers_ctx *p = ctx;
if (grid[pos.y][pos.x] == unit_types[UNIT_WALKER].tiles[p->player]) {
play_effect_oneshot(SFX_AA, pos);
undeploy_unit(pos, p->player, UNIT_WALKER);
p->any_found = true;
}
}
void cellular_automata_walker(struct pos pos) {
for (int player = 0; player < PLAYERS_COUNT; player++) {
if (grid[pos.y][pos.x] == unit_types[UNIT_WALKER].tiles[player]) {
struct destroy_adjacent_walkers_ctx destroy_adjacent_walkers_ctx = {
.player = P2 - player,
.any_found = false,
};
for_neighbours(&destroy_adjacent_walkers_ctx, destroy_adjacent_walkers, pos);
if (destroy_adjacent_walkers_ctx.any_found) {
undeploy_unit(pos, player, UNIT_WALKER);
}
}
}
}
void clear_colour_around(void *, struct pos pos) {
if (is_tile_unoccupied(grid[pos.y][pos.x])) {
grid[pos.y][pos.x] = PLAYFIELD;
}
}
void cellular_automata(void) {
for (int y = 0; y < grid_rect.h; y++) {
for (int x = 0; x < grid_rect.w; x++) {
struct pos pos = {x, y};
cellular_automata_walker(pos);
}
}
}
void initial_grid(void) {
for (int y = 0; y < grid_rect.h; y++) {
for (int x = 0; x < grid_rect.w; x++) {
if (x < grid_rect.w / 2) {
grid[y][x] = P1_PLAYFIELD;
} else {
grid[y][x] = P2_PLAYFIELD;
}
}
}
}
int main(void) {
init_graphics();
initial_grid();
update_bg();
bgShow(mainBg);
soundEnable();
int ret;
consoleDemoInit();
if (!mmInitDefaultMem((mm_addr)soundbank_bin)) {
ERRX("mmInitDefaultMem() failed\n");
}
LOAD_EFFECT(SFX_DRAG);
LOAD_EFFECT(SFX_DRAG_START);
LOAD_EFFECT(SFX_KNOCK);
LOAD_EFFECT(SFX_WALK);
LOAD_EFFECT(SFX_AA);
init_graphics_sub();
update_bg_sub();
bgShow(subBg);
constexpr int drag_direction_arrow_sprite = 0;
struct pos drag_from_tile = {}, drag_to_tile = {};
bool dragging = false;
bool ready_to_move = true, player_want_move = false, ai_want_move = false;
bool player_did_turn = false, must_run_cellular_automata = false;
autoselect_unit_to_deploy();
// Sorry, this is a giant hack
while (left_to_deploy(P2)) {
// Relies on the AI always placing first
ai_turn();
}
for (;;) {
if (must_run_cellular_automata && ready_to_move) {
must_run_cellular_automata = false;
cellular_automata();
}
swiWaitForVBlank();
update_bg();
update_bg_sub();
oamUpdate(&oamMain);
scanKeys();
unsigned keys_held = keysHeld();
unsigned keys_down = keysDown();
if (keys_held & KEY_TOUCH && !player_want_move) {
touchPosition touch_pos;
touchRead(&touch_pos);
auto touched = pos_from_touchPosition(touch_pos);
if (is_within(playfield, touched)) {
if (keys_down & KEY_TOUCH) {
drag_from_tile = hit_tile(touched);
dragging = true;
}
drag_to_tile = hit_tile(touched);
}
}
if (dragging && ready_to_move) {
if (can_player_deploy(drag_from_tile, P1, unit_to_place)) {
player_did_turn = true;
deploy_unit(drag_from_tile, P1, unit_to_place);
}
}
if (!(keys_held & KEY_TOUCH) && dragging) {
dragging = false;
player_want_move = true;
}
if (keys_down & KEY_LEFT) {
if (unit_to_place == 0) { unit_to_place = UNIT_TYPES_COUNT; }
unit_to_place--;
}
if (keys_down & KEY_RIGHT) {
unit_to_place++;
if (unit_to_place == UNIT_TYPES_COUNT) { unit_to_place = 0; }
}
if (keys_down & KEY_SELECT) {
credits();
init_graphics_sub();
update_bg_sub();
bgShow(subBg);
}
if (ai_want_move && ready_to_move) {
ai_want_move = false;
ai_turn();
must_run_cellular_automata = true;
}
if (player_want_move && ready_to_move) {
player_want_move = false;
player_did_turn |= do_player_move(drag_from_tile, drag_to_tile, P1);
if (player_did_turn) {
player_did_turn = false;
ai_want_move = true;
must_run_cellular_automata = true;
}
}
if (dragging && !is_same_pos(drag_from_tile, drag_to_tile) && is_move_allowed(drag_from_tile, drag_to_tile, P1)) {
auto drag_direction = move_clamp_to_1(pos_difference(drag_to_tile, drag_from_tile));
int arrow_index = arrow_index_from_direction(drag_direction);
auto half_tile = scale_move(drag_direction, tile_side, 2);
auto arrow_top_left = offset_pos(tile_on_screen(drag_from_tile), half_tile);
place_sprite(drag_direction_arrow_sprite, arrow_top_left, arrows[arrow_index]);
} else {
hide_sprite(drag_direction_arrow_sprite);
}
int units_still_moving = animate_moving_units();
ready_to_move = units_still_moving == 0;
}
}