#include "PlayingState.hxx" #include "GameStateManager.hxx" #include #include #include #include #include namespace { SDL_Point head_position(SDL_FPoint const& position) { #pragma STDC FENV_ACCESS ON std::fesetround(FE_TONEAREST); return { .x = static_cast(std::nearbyint(position.x)), .y = static_cast(std::nearbyint(position.y)), }; } } static bool operator==(SDL_Point const& lhs, SDL_Point const& rhs) { return lhs.x==rhs.x && lhs.y==rhs.y; } static bool operator!=(SDL_Point const& lhs, SDL_Point const& rhs) { return lhs.x!=rhs.x || lhs.y!=rhs.y; } unsigned PlayingState::last_high_score_{0}; PlayingState::PlayingState() :generator_{std::random_device{}()}, font_{"kenney_pixel.ttf"} { } void PlayingState::on_enter(GameStateManager& gsm) { (void) gsm; length_ = 10u; std::uniform_int_distribution distribution_direction{0, 3}; direction_ = static_cast(distribution_direction(generator_)); place_head(); place_target(); tail_.clear(); speed_ = START_SPEED; } void PlayingState::on_event(GameStateManager& gsm, SDL_Event const& evt) { if (evt.type==SDL_KEYUP && evt.key.keysym.scancode==SDL_SCANCODE_ESCAPE) { gsm.push_state(GameStates::MainMenu); } else if (evt.type==SDL_KEYDOWN) { switch (evt.key.keysym.scancode) { default: break; case SDL_SCANCODE_UP: if (direction_==Direction::Left || direction_==Direction::Right) { direction_ = Direction::Up; } break; case SDL_SCANCODE_DOWN: if (direction_==Direction::Left || direction_==Direction::Right) { direction_ = Direction::Down; } break; case SDL_SCANCODE_LEFT: if (direction_==Direction::Up || direction_==Direction::Down) { direction_ = Direction::Left; } break; case SDL_SCANCODE_RIGHT: if (direction_==Direction::Up || direction_==Direction::Down) { direction_ = Direction::Right; } break; } } } void PlayingState::update(GameStateManager& gsm, std::chrono::milliseconds delta_time) { auto const distance = speed_*static_cast(delta_time.count()); if (distance>MAX_DISTANCE) { SDL_Log("Snake would move a distance of %f. Game might have been stuck. Skipping cycle.", distance); return; } SDL_FPoint new_head = head_; switch (direction_) { case Direction::Up: new_head.y -= distance; break; case Direction::Down: new_head.y += distance; break; case Direction::Left: new_head.x -= distance; break; case Direction::Right: new_head.x += distance; break; } auto const old_pos = ::head_position(head_); auto const new_pos = ::head_position(new_head); if (old_pos!=new_pos) { if (new_pos==target_) { ++length_; speed_ *= ACCELERATION; place_target(); } if (detect_death(new_pos)) { last_high_score_ = length_; gsm.replace_state(GameStates::GameOver); } tail_.push_front(old_pos); if (tail_.size()+1>length_) tail_.pop_back(); } head_ = new_head; } void PlayingState::render(SDLRenderer& renderer) { int width, height; SDL_GetRendererOutputSize(renderer, &width, &height); SDL_Rect playing_field; double const ratio = static_cast(CELLS_X)/CELLS_Y; if (width(playing_field.w/ratio); } else { playing_field.h = height-70; playing_field.w = static_cast(playing_field.h*ratio); } playing_field.x = (width-playing_field.w)/2; playing_field.y = 50; SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); SDL_RenderClear(renderer); render_ui(renderer, playing_field); render_target(renderer, playing_field); render_snake(renderer, playing_field); SDL_RenderPresent(renderer); } void PlayingState::render_ui(SDLRenderer& renderer, SDL_Rect const& playing_field) { TTF_Font* const font = font_; auto const score_text = std::format("Score: {}", length_); SDL_Surface* text_surface = TTF_RenderText_Solid(font, score_text.c_str(), {255, 255, 255, SDL_ALPHA_OPAQUE}); SDL_Texture* text = SDL_CreateTextureFromSurface(renderer, text_surface); SDL_FreeSurface(text_surface); int text_width, text_height; SDL_QueryTexture(text, nullptr, nullptr, &text_width, &text_height); SDL_Rect render_quad = {playing_field.x, 10, text_width, text_height}; SDL_RenderCopy(renderer, text, nullptr, &render_quad); SDL_DestroyTexture(text); SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE); SDL_RenderDrawRect(renderer, &playing_field); } void PlayingState::place_target() { target_.x = distribution_position_x_(generator_); target_.y = distribution_position_y_(generator_); } void PlayingState::place_head() { do { head_.x = static_cast(distribution_position_x_(generator_)); } while (head_.x<10.0f || head_.x>static_cast(CELLS_X-10)); do { head_.y = static_cast(distribution_position_y_(generator_)); } while (head_.y<10.0f || head_.y>static_cast(CELLS_Y-10)); } void PlayingState::render_target(SDLRenderer& renderer, SDL_Rect const& playing_field) { auto const ratio = playing_field.w/static_cast(CELLS_X); SDL_Rect const target_rect{ .x = static_cast(playing_field.x+ratio*target_.x), .y = static_cast(playing_field.y+ratio*target_.y), .w = static_cast(ratio), .h = static_cast(ratio), }; SDL_SetRenderDrawColor(renderer, 0, 255, 0, SDL_ALPHA_OPAQUE); SDL_RenderFillRect(renderer, &target_rect); } void PlayingState::render_snake(SDLRenderer& renderer, SDL_Rect const& playing_field) { auto const ratio = playing_field.w/static_cast(CELLS_X); auto const render_dot = [ratio, playing_field, &renderer](SDL_Point const& position, double const size_factor) { int const base_x = static_cast(playing_field.x+ratio*position.x); int const base_y = static_cast(playing_field.y+ratio*position.y); int const size = std::max(1, static_cast(ratio*size_factor)); int const padding = (static_cast(ratio)-size) >> 1; SDL_Rect const target_rect{ .x = base_x+padding, .y = base_y+padding, .w = size, .h = size, }; SDL_RenderFillRect(renderer, &target_rect); }; SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE); render_dot(::head_position(head_), 1.0); double size = 1.0; double const decay = 1.0/static_cast(tail_.size()+1); for (auto const& particle: tail_) { size = std::max(0.0, size-decay); render_dot(particle, size); } } bool PlayingState::detect_death(SDL_Point const& position) { // collision with wall if (position.x<0 || position.x>=CELLS_X || position.y<0 || position.y>=CELLS_Y) return true; // collision with self return (std::ranges::any_of(tail_, [&position](SDL_Point const& particle) { return position==particle; })); } unsigned PlayingState::last_high_score() { return last_high_score_; }