summaryrefslogtreecommitdiffstats
path: root/src/game.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/game.cpp')
-rw-r--r--src/game.cpp528
1 files changed, 528 insertions, 0 deletions
diff --git a/src/game.cpp b/src/game.cpp
new file mode 100644
index 0000000..c7d86e0
--- /dev/null
+++ b/src/game.cpp
@@ -0,0 +1,528 @@
+/*
+ * Author: Sven Gothel <[email protected]> and Svenson Han Gothel
+ * Copyright (c) 2022 Gothel Software e.K.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+#include <pacman/game.hpp>
+#include <pacman/globals.hpp>
+
+#include <limits>
+
+#include <cstdio>
+#include <time.h>
+
+static constexpr const bool DEBUG_GFX_BOUNDS = false;
+
+//
+// globals across modules 'globals.hpp'
+//
+
+int win_pixel_width = 0;
+int win_pixel_scale = 1;
+
+static int frames_per_sec=0;
+int get_frames_per_sec() { return frames_per_sec; }
+
+std::unique_ptr<maze_t> pacman_maze;
+std::shared_ptr<global_tex_t> global_tex;
+std::vector<ghost_ref> ghosts;
+pacman_ref pacman;
+
+static bool original_pacman_behavior = true;
+bool use_original_pacman_behavior() { return original_pacman_behavior; }
+
+//
+// score_t
+//
+
+score_t tile_to_score(const tile_t tile) {
+ switch(tile) {
+ case tile_t::PELLET: return score_t::PELLET;
+ case tile_t::PELLET_POWER: return score_t::PELLET_POWER;
+ case tile_t::CHERRY: return score_t::CHERRY;
+ case tile_t::STRAWBERRY: return score_t::STRAWBERRY;
+ case tile_t::ORANGE: return score_t::ORANGE;
+ case tile_t::APPLE: return score_t::APPLE;
+ case tile_t::MELON: return score_t::MELON;
+ case tile_t::GALAXIAN: return score_t::GALAXIAN;
+ case tile_t::BELL: return score_t::BELL;
+ case tile_t::KEY: return score_t::KEY;
+ default: return score_t::NONE;
+ }
+}
+
+//
+// global_tex_t
+//
+
+int global_tex_t::tile_to_texidx(const tile_t tile) const {
+ if( tile_t::PELLET <= tile && tile <= tile_t::KEY ) {
+ const int tile_i = static_cast<int>(tile);
+ const int pellet_i = static_cast<int>(tile_t::PELLET);
+ const int idx = tile_i - pellet_i;
+ if( 0 <= idx && (size_t)idx < textures.size() ) {
+ return idx;
+ }
+ }
+ return -1;
+}
+
+int global_tex_t::validate_texidx(const int idx) const {
+ if( 0 <= idx && (size_t)idx < textures.size() ) {
+ return idx;
+ }
+ return -1;
+}
+
+global_tex_t::global_tex_t(SDL_Renderer* rend)
+: all_images( std::make_shared<texture_t>(rend, "media/tiles_all.png") ),
+ atex_pellet_power( "PP", rend, 250, all_images, 0, 0, 14, 14, { { 1*14, 0 }, { -1, -1} })
+{
+ add_sub_textures(textures, rend, all_images, 0, 0, 14, 14, {
+ { 0*14, 0 }, { 1*14, 0 }, { 2*14, 0 }, { 3*14, 0 }, { 4*14, 0 }, { 5*14, 0 }, { 6*14, 0 },
+ { 7*14, 0 }, { 8*14, 0 }, { 9*14, 0 }, { 10*14, 0 }, { 11*14, 0 }, { 12*14, 0 }, { 13*14, 0 }, } );
+}
+
+void global_tex_t::destroy() {
+ for(size_t i=0; i<textures.size(); ++i) {
+ textures[i]->destroy();
+ }
+ textures.clear();
+ all_images->destroy();
+}
+
+std::shared_ptr<texture_t> global_tex_t::get_tex(const int idx) {
+ const int idx2 = validate_texidx(idx);
+ return 0 <= idx2 ? textures[idx2] : nullptr;
+}
+std::shared_ptr<const texture_t> global_tex_t::get_tex(const int idx) const {
+ const int idx2 = validate_texidx(idx);
+ return 0 <= idx2 ? textures[idx2] : nullptr;
+}
+
+std::shared_ptr<texture_t> global_tex_t::get_tex(const tile_t tile) {
+ const int idx = tile_to_texidx(tile);
+ return 0 <= idx ? textures[idx] : nullptr;
+}
+std::shared_ptr<const texture_t> global_tex_t::get_tex(const tile_t tile) const {
+ const int idx = tile_to_texidx(tile);
+ return 0 <= idx ? textures[idx] : nullptr;
+}
+
+void global_tex_t::draw_tile(const tile_t tile, SDL_Renderer* rend, const int x, const int y) {
+ if( tile_t::PELLET_POWER == tile ) {
+ atex_pellet_power.draw(rend, x, y, true /* maze_offset */);
+ } else {
+ std::shared_ptr<texture_t> tex = get_tex(tile);
+ if( nullptr != tex ) {
+ tex->draw(rend, x, y, true /* maze_offset */);
+ }
+ }
+}
+
+std::string global_tex_t::toString() const {
+ return "tiletex[count "+std::to_string(textures.size())+"]";
+}
+
+//
+// main
+//
+
+TTF_Font* font_ttf = nullptr;
+
+static void on_window_resized(SDL_Renderer* rend, const int win_width_l, const int win_height_l) {
+ int win_pixel_height=0;
+ SDL_GetRendererOutputSize(rend, &win_pixel_width, &win_pixel_height);
+
+ float sx = win_pixel_width / pacman_maze->get_pixel_width();
+ float sy = win_pixel_height / pacman_maze->get_pixel_height();
+ win_pixel_scale = static_cast<int>( std::round( std::fmin<float>(sx, sy) ) );
+
+ if( nullptr != font_ttf ) {
+ TTF_CloseFont(font_ttf);
+ font_ttf = nullptr;
+ }
+ int font_height;
+ {
+ const std::string fontfilename = "fonts/freefont/FreeSansBold.ttf";
+ font_height = pacman_maze->get_ppt_y() * win_pixel_scale;
+ font_ttf = TTF_OpenFont(fontfilename.c_str(), font_height);
+ }
+ log_print("Window Resized: %d x %d pixel ( %d x %d logical ) @ %d hz\n",
+ win_pixel_width, win_pixel_height, win_width_l, win_height_l, get_frames_per_sec());
+ log_print("Pixel scale: %f x %f -> %d, font[ok %d, height %d]\n", sx, sy, win_pixel_scale, nullptr!=font_ttf, font_height);
+}
+
+static std::string get_usage(const std::string& exename) {
+ return "Usage: "+exename+" [-step] [-show_fps] [-no_vsync] [-fps <int>] [-speed <int>] [-wwidth <int>] [-wheight <int>] [-show_ghost_moves] [-show_targets] [-bugfix]";
+}
+
+int main(int argc, char *argv[])
+{
+ bool auto_move = true;
+ bool show_fps = false;
+ bool enable_vsync = true;
+ int forced_fps = -1;
+ float fields_per_sec=8;
+ int win_width = 640, win_height = 720;
+ bool show_ghost_moves = false;
+ bool show_targets = false;
+ {
+ for(int i=1; i<argc; ++i) {
+ if( 0 == strcmp("-step", argv[i]) ) {
+ auto_move = false;
+ } else if( 0 == strcmp("-show_fps", argv[i]) ) {
+ show_fps = true;
+ } else if( 0 == strcmp("-no_vsync", argv[i]) ) {
+ enable_vsync = false;
+ } else if( 0 == strcmp("-fps", argv[i]) && i+1<argc) {
+ forced_fps = atoi(argv[i+1]);
+ enable_vsync = false;
+ ++i;
+ } else if( 0 == strcmp("-speed", argv[i]) && i+1<argc) {
+ fields_per_sec = atof(argv[i+1]);
+ ++i;
+ } else if( 0 == strcmp("-wwidth", argv[i]) && i+1<argc) {
+ win_width = atoi(argv[i+1]);
+ ++i;
+ } else if( 0 == strcmp("-wheight", argv[i]) && i+1<argc) {
+ win_height = atoi(argv[i+1]);
+ ++i;
+ } else if( 0 == strcmp("-show_ghost_moves", argv[i]) ) {
+ show_ghost_moves = true;
+ } else if( 0 == strcmp("-show_targets", argv[i]) ) {
+ show_targets = true;
+ } else if( 0 == strcmp("-bugfix", argv[i]) ) {
+ original_pacman_behavior = false;
+ }
+ }
+ log_print("%s\n", get_usage(argv[0]).c_str());
+ log_print("- auto_move %d\n", auto_move);
+ log_print("- show_fps %d\n", show_fps);
+ log_print("- enable_vsync %d\n", enable_vsync);
+ log_print("- forced_fps %d\n", forced_fps);
+ log_print("- fields_per_sec %5.2f\n", fields_per_sec);
+ log_print("- win size %d x %d\n", win_width, win_height);
+ log_print("- show_ghost_moves %d\n", show_ghost_moves);
+ log_print("- show_targets %d\n", show_targets);
+ log_print("- use_bugfix_pacman %d\n", !use_original_pacman_behavior());
+ }
+
+ pacman_maze = std::make_unique<maze_t>("media/playfield_pacman.txt");
+
+ if( !pacman_maze->is_ok() ) {
+ log_print("Maze: Error: %s\n", pacman_maze->toString().c_str());
+ return -1;
+ }
+ {
+ log_print("--- 8< ---\n");
+ const int maze_width = pacman_maze->get_width();
+ pacman_maze->draw( [&maze_width](int x, int y, tile_t tile) {
+ fprintf(stderr, "%s", to_string(tile).c_str());
+ if( x == maze_width-1 ) {
+ fprintf(stderr, "\n");
+ }
+ (void)y;
+ });
+ log_print("--- >8 ---\n");
+ log_print("Maze: %s\n", pacman_maze->toString().c_str());
+ }
+
+ if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
+ log_print("error initializing SDL: %s\n", SDL_GetError());
+ }
+
+ if ( ( IMG_Init(IMG_INIT_PNG) & IMG_INIT_PNG ) != IMG_INIT_PNG ) {
+ log_print("error initializing SDL_image: %s\n", SDL_GetError());
+ }
+
+ if( 0 != TTF_Init() ) {
+ log_print("Font: Error initializing.\n");
+ }
+
+ if( enable_vsync ) {
+ SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1");
+ }
+
+ SDL_Window* win = SDL_CreateWindow("Pacman",
+ SDL_WINDOWPOS_UNDEFINED,
+ SDL_WINDOWPOS_UNDEFINED,
+ win_width,
+ win_height,
+ SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE);
+
+ const Uint32 render_flags = enable_vsync ? SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC :
+ SDL_RENDERER_ACCELERATED;
+ Uint32 fullscreen_flags = 0;
+ bool uses_vsync = false;
+
+ SDL_Renderer* rend = SDL_CreateRenderer(win, -1, render_flags);
+ {
+ SDL_RendererInfo info;
+ SDL_GetRendererInfo(rend, &info);
+ bool _uses_vsync = 0 != ( info.flags & SDL_RENDERER_PRESENTVSYNC );
+ uses_vsync = _uses_vsync | enable_vsync; // FIXME: Assume yes if enforced with enable_vsync, since info.flags is not reliable
+ log_print("renderer: name: %s\n", info.name);
+ log_print("renderer: accel %d\n", 0 != ( info.flags & SDL_RENDERER_ACCELERATED ));
+ log_print("renderer: soft %d\n", 0 != ( info.flags & SDL_RENDERER_SOFTWARE ));
+ log_print("renderer: vsync %d -> %d\n", _uses_vsync, uses_vsync);
+ }
+
+ std::unique_ptr<texture_t> pacman_maze_tex = std::make_unique<texture_t>(rend, "media/"+pacman_maze->get_texture_file());
+ {
+ int win_pixel_width_=0;
+ int win_pixel_height_=0;
+ SDL_GetRendererOutputSize(rend, &win_pixel_width_, &win_pixel_height_);
+ {
+ SDL_DisplayMode mode;
+ {
+ const int num_displays = SDL_GetNumVideoDisplays();
+ for(int i=0; i<num_displays; ++i) {
+ bzero(&mode, sizeof(mode));
+ SDL_GetCurrentDisplayMode(i, &mode);
+ log_print("Display %d: %d x %d @ %d Hz\n", i, mode.w, mode.h, mode.refresh_rate);
+ }
+ }
+ const int win_display_idx = SDL_GetWindowDisplayIndex(win);
+ bzero(&mode, sizeof(mode));
+ SDL_GetCurrentDisplayMode(win_display_idx, &mode); // SDL_GetWindowDisplayMode(..) fails on some systems (wrong refresh_rate and logical size
+ log_print("WindowDisplayMode: %d x %d @ %d Hz @ display %d\n", mode.w, mode.h, mode.refresh_rate, win_display_idx);
+ if( 0 < forced_fps ) {
+ frames_per_sec = forced_fps;
+ } else {
+ frames_per_sec = mode.refresh_rate;
+ }
+ }
+ on_window_resized(rend, win_pixel_width_, win_pixel_height_);
+ }
+ SDL_SetWindowSize(win, pacman_maze->get_pixel_width()*win_pixel_scale,
+ pacman_maze->get_pixel_height()*win_pixel_scale);
+
+ global_tex = std::make_shared<global_tex_t>(rend);
+ pacman = std::make_shared<pacman_t>(rend, fields_per_sec, auto_move);
+ ghosts.push_back( std::make_shared<ghost_t>(ghost_t::personality_t::BLINKY, rend, fields_per_sec) );
+ ghosts.push_back( std::make_shared<ghost_t>(ghost_t::personality_t::PINKY, rend, fields_per_sec) );
+ // ghosts.push_back( std::make_shared<ghost_t>(ghost_t::personality_t::CLYDE, rend, fields_per_sec) );
+ // ghosts.push_back( std::make_shared<ghost_t>(ghost_t::personality_t::INKY, rend, fields_per_sec) );
+ for(ghost_ref g : ghosts) {
+ g->set_log_moves(show_ghost_moves);
+ }
+
+ bool close = false;
+ bool set_dir = false;
+ bool pause = true;
+ direction_t dir = pacman->get_dir();
+
+ const uint64_t fps_range_ms = 3000;
+ uint64_t t0 = getCurrentMilliseconds();
+ uint64_t frame_count = 0;
+ uint64_t t1 = t0;
+ uint64_t td_print_fps = t0;
+ uint64_t reset_fps_frame = fps_range_ms;
+
+ uint64_t last_score = pacman->get_score();
+ std::shared_ptr<text_texture_t> ttex_score = nullptr;
+ std::shared_ptr<text_texture_t> ttex_score_title = nullptr;
+
+ while (!close) {
+ SDL_Event event;
+ while (SDL_PollEvent(&event)) {
+ switch (event.type) {
+ case SDL_QUIT:
+ close = true;
+ break;
+
+ case SDL_KEYUP:
+ set_dir = false;
+ break;
+
+ case SDL_WINDOWEVENT:
+ switch (event.window.event) {
+ case SDL_WINDOWEVENT_SHOWN:
+ pause = false;
+ break;
+ case SDL_WINDOWEVENT_HIDDEN:
+ pause = true;
+ break;
+ case SDL_WINDOWEVENT_RESIZED:
+ on_window_resized(rend, event.window.data1, event.window.data2);
+ ttex_score_title = nullptr;
+ ttex_score = nullptr;
+ break;
+ case SDL_WINDOWEVENT_SIZE_CHANGED:
+ // INFO_PRINT("Window SizeChanged: %d x %d\n", event.window.data1, event.window.data2);
+ break;
+ }
+ break;
+
+ case SDL_KEYDOWN:
+ // keyboard API for key pressed
+ switch (event.key.keysym.scancode) {
+ case SDL_SCANCODE_Q:
+ [[fallthrough]];
+ case SDL_SCANCODE_ESCAPE:
+ close = true;
+ break;
+ case SDL_SCANCODE_P:
+ pause = pause ? false : true;
+ break;
+ case SDL_SCANCODE_R:
+ pacman_maze->reset();
+ break;
+ case SDL_SCANCODE_W:
+ case SDL_SCANCODE_UP:
+ dir = direction_t::UP;
+ set_dir = true;
+ break;
+ case SDL_SCANCODE_A:
+ case SDL_SCANCODE_LEFT:
+ dir = direction_t::LEFT;
+ set_dir = true;
+ break;
+ case SDL_SCANCODE_S:
+ case SDL_SCANCODE_DOWN:
+ dir = direction_t::DOWN;
+ set_dir = true;
+ break;
+ case SDL_SCANCODE_D:
+ case SDL_SCANCODE_RIGHT:
+ dir = direction_t::RIGHT;
+ set_dir = true;
+ break;
+ case SDL_SCANCODE_F:
+ fullscreen_flags = 0 == fullscreen_flags ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0;
+ SDL_SetWindowFullscreen(win, fullscreen_flags);
+ break;
+ default:
+ // nop
+ break;
+ }
+ }
+ }
+ if( pause ) {
+ SDL_Delay( 1000 / 60 );
+ continue;
+ }
+ if( set_dir ) {
+ pacman->set_dir(dir);
+ }
+ global_tex->tick();
+ for(ghost_ref g : ghosts) {
+ g->tick();
+ }
+ pacman->tick();
+
+ SDL_RenderClear(rend);
+
+ pacman_maze_tex->draw(rend, 0, 0, false /* maze_offset */);
+ pacman_maze->draw( [&rend](int x, int y, tile_t tile) {
+ global_tex->draw_tile(tile, rend, x, y);
+ });
+ pacman->draw(rend);
+ for(ghost_ref ghost : ghosts) {
+ ghost->draw(rend);
+
+ if( show_targets ) {
+ uint8_t r, g, b, a;
+ SDL_GetRenderDrawColor(rend, &r, &g, &b, &a);
+ SDL_SetRenderDrawColor(rend, 150, 150, 150, 255);
+ const acoord_t& p1 = ghost->get_pos();
+ const acoord_t& p2 = ghost->get_target();
+ SDL_RenderDrawLine(rend,
+ pacman_maze->x_to_pixel( p1.get_x_i(), win_pixel_scale, false ),
+ pacman_maze->y_to_pixel( p1.get_y_i(), win_pixel_scale, false ),
+ pacman_maze->x_to_pixel( p2.get_x_i(), win_pixel_scale, false ),
+ pacman_maze->y_to_pixel( p2.get_y_i(), win_pixel_scale, false ) );
+ SDL_SetRenderDrawColor(rend, r, g, b, a);
+ }
+ }
+
+ if( nullptr != font_ttf ) {
+ if( nullptr == ttex_score_title ) {
+ const std::string highscore_s("HIGH SCORE");
+ ttex_score_title = draw_text_scaled(rend, font_ttf, highscore_s, 255, 255, 255, [&](const texture_t& tex, int &x, int&y) {
+ x = ( pacman_maze->get_pixel_width()*win_pixel_scale - tex.get_width() ) / 2;
+ y = pacman_maze->x_to_pixel(0, win_pixel_scale, false);
+ });
+ } else {
+ ttex_score_title->redraw(rend);
+ }
+ if( nullptr != ttex_score && last_score == pacman->get_score() ) {
+ ttex_score->redraw(rend);
+ } else {
+ std::string score_s = std::to_string( pacman->get_score() );
+ ttex_score = draw_text_scaled(rend, font_ttf, score_s, 255, 255, 255, [&](const texture_t& tex, int &x, int&y) {
+ x = ( pacman_maze->get_pixel_width()*win_pixel_scale - tex.get_width() ) / 2;
+ y = pacman_maze->x_to_pixel(1, win_pixel_scale, false);
+ });
+ last_score = pacman->get_score();
+ }
+ }
+
+ // swap double buffer incl. v-sync
+ SDL_RenderPresent(rend);
+ ++frame_count;
+ if( !uses_vsync ) {
+ const uint64_t ms_per_frame = (uint64_t)std::round(1000.0 / (float)get_frames_per_sec());
+ const uint64_t ms_last_frame = getCurrentMilliseconds() - t1;
+ if( ms_per_frame > ms_last_frame + 1 )
+ {
+ const uint64_t td = ms_per_frame - ms_last_frame;
+ #if 1
+ struct timespec ts { (long)(td/1000UL), (long)((td%1000UL)*1000000UL) };
+ nanosleep( &ts, NULL );
+ #else
+ SDL_Delay( td );
+ #endif
+ // INFO_PRINT("soft-sync exp %zd > has %zd, delay %zd (%lds, %ldns)\n", ms_per_frame, ms_last_frame, td, ts.tv_sec, ts.tv_nsec);
+ }
+ }
+ t1 = getCurrentMilliseconds();
+ if( show_fps && fps_range_ms <= t1 - td_print_fps ) {
+ const float fps = get_fps(t0, t1, frame_count);
+ std::string fps_str(64, '\0');
+ const int written = std::snprintf(&fps_str[0], fps_str.size(), "fps %6.2f", fps);
+ fps_str.resize(written);
+ // log_print("%s, td %" PRIu64 "ms, frames %" PRIu64 "\n", fps_str.c_str(), t1-t0, frame_count);
+ log_print("%s\n", fps_str.c_str());
+ td_print_fps = t1;
+ }
+ if( 0 == --reset_fps_frame ) {
+ t0 = t1;
+ frame_count = 0;
+ reset_fps_frame = fps_range_ms;
+ }
+ } // loop
+
+ pacman->destroy();
+ ghosts.clear();
+ pacman_maze_tex->destroy();
+
+ SDL_DestroyRenderer(rend);
+
+ SDL_DestroyWindow(win);
+
+ TTF_CloseFont(font_ttf);
+
+ SDL_Quit();
+
+ return 0;
+}