From 6d6fa6376434d36812585ff1304e7f8c79f696db Mon Sep 17 00:00:00 2001 From: Dan Whitacre Date: Sat, 24 Feb 2024 20:48:36 -0500 Subject: [PATCH] initial snake game plugin --- .bin/build.sh | 97 +++++++++++++++++++++++++++++++++++++ .bin/log.sh | 68 ++++++++++++++++++++++++++ .bin/watch_fs.sh | 22 +++++++++ .gitignore | 3 ++ LICENSE | 21 ++++++++ info.toml | 6 +++ src/Apple.as | 11 +++++ src/GameController.as | 72 +++++++++++++++++++++++++++ src/Interface.as | 18 +++++++ src/Log.as | 17 +++++++ src/Main.as | 49 +++++++++++++++++++ src/Notify.as | 19 ++++++++ src/Plugin.as | 4 ++ src/Snake.as | 58 ++++++++++++++++++++++ src/settings/10_Snake.as | 23 +++++++++ src/settings/20_Display.as | 11 +++++ src/settings/27_Advanced.as | 2 + 17 files changed, 501 insertions(+) create mode 100644 .bin/build.sh create mode 100644 .bin/log.sh create mode 100644 .bin/watch_fs.sh create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 info.toml create mode 100644 src/Apple.as create mode 100644 src/GameController.as create mode 100644 src/Interface.as create mode 100644 src/Log.as create mode 100644 src/Main.as create mode 100644 src/Notify.as create mode 100644 src/Plugin.as create mode 100644 src/Snake.as create mode 100644 src/settings/10_Snake.as create mode 100644 src/settings/20_Display.as create mode 100644 src/settings/27_Advanced.as diff --git a/.bin/build.sh b/.bin/build.sh new file mode 100644 index 0000000..66f7e48 --- /dev/null +++ b/.bin/build.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source "$SCRIPT_DIR/log.sh" +source "$SCRIPT_DIR/watch_fs.sh" + +log "lgrey" "Loaded scripts from the directory $SCRIPT_DIR" + +GIT_DIR=$(git rev-parse --show-toplevel) +SRC_DIR="$GIT_DIR/src" +INFO_TOML="$GIT_DIR/info.toml" +PLUGINS_DIR=${PLUGINS_DIR:-$HOME/OpenplanetNext/Plugins} +ZIP=${ZIP:-"$PROGRAMFILES/7-Zip/7z.exe"} + +PLUGIN_PRETTY_NAME="$(cat $INFO_TOML | dos2unix | grep '^name' | cut -f 2 -d '=' | tr -d '\"\r' | sed 's/^[ ]*//')" +PLUGIN_VERSION="$(cat $INFO_TOML | dos2unix | grep '^version' | cut -f 2 -d '=' | tr -d '\"\r' | sed 's/^[ ]*//')" +PLUGIN_NAME=$(echo "$PLUGIN_PRETTY_NAME" | tr -d '(),:;'\''"' | tr 'A-Z ' 'a-z-') +PLUGIN_BUILD_DIR="$GIT_DIR/dist" +PLUGIN_BUILD_NAME="$PLUGIN_BUILD_DIR/$PLUGIN_NAME-$PLUGIN_VERSION.op" +PLUGIN_DIRTY_FLAG="$PLUGIN_BUILD_DIR/dirty" + +WATCH=false +CI=false +POSITIONAL_ARGS=() + +while [[ $# -gt 0 ]]; do + case $1 in + -w|--watch) + WATCH=true + shift + ;; + --ci) + CI=true + shift + ;; + -*|--*) + log "red" "Unknown option $1. Exiting." + exit 1 + ;; + *) + POSITIONAL_ARGS+=("$1") + shift + ;; + esac +done + +set -- "${POSITIONAL_ARGS[@]}" + +function publish() { + if ! $CI; then + local busy=true + log "lgrey" "Waiting to publish when not busy.. Stop/Unload the plugin." + while $busy; do + cp $PLUGIN_BUILD_NAME $PLUGINS_DIR 2> /dev/null && busy=false + sleep 1 + done + log "green" "Copied build to plugins directory: $PLUGINS_DIR" + fi +} + +function publish_repeat() { + while [[ true ]]; do + publish + watch_fs $PLUGIN_DIRTY_FLAG + done +} + +function build() { + log "white" "Running build script for plugin: $PLUGIN_PRETTY_NAME v$PLUGIN_VERSION" + + if ! test -d $PLUGIN_BUILD_DIR; then + mkdir $PLUGIN_BUILD_DIR + fi + + if test -f $PLUGIN_BUILD_NAME; then + rm -f $PLUGIN_BUILD_NAME + fi + + "$ZIP" a -mx1 -tzip $PLUGIN_BUILD_NAME $INFO_TOML $SRC_DIR + log "lgreen" "Build complete: $PLUGIN_BUILD_NAME" + echo "$(date)" > $PLUGIN_DIRTY_FLAG +} + +build +if $WATCH; then + publish_repeat & + while [[ true ]]; do + watch_fs $SRC_DIR + build + done +else + publish & +fi + +if ps -p $! > /dev/null; then + wait $! +fi \ No newline at end of file diff --git a/.bin/log.sh b/.bin/log.sh new file mode 100644 index 0000000..2723930 --- /dev/null +++ b/.bin/log.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +function log() { + if [[ "$#" -eq 0 || "$#" -gt 2 ]]; then return + fi + + local color=$1 + local text=$2 + + local esc="\e[" + local reset="${esc}39m" + + local black="${esc}30m" + local red="${esc}31m" + local green="${esc}32m" + local yellow="${esc}33m" + local blue="${esc}34m" + local magenta="${esc}35m" + local cyan="${esc}36m" + local lightgrey="${esc}37m" + local darkgrey="${esc}90m" + local lightred="${esc}91m" + local lightgreen="${esc}92m" + local lightyellow="${esc}93m" + local lightblue="${esc}94m" + local lightmagenta="${esc}95m" + local lightcyan="${esc}96m" + local white="${esc}97m" + + case ${color} in + "black") color=${black} + ;; + "red") color=${red} + ;; + "green") color=${green} + ;; + "yellow") color=${yellow} + ;; + "blue") color=${blue} + ;; + "magenta") color=${magenta} + ;; + "cyan") color=${cyan} + ;; + "lgrey") color=${lightgrey} + ;; + "dgrey") color=${darkgrey} + ;; + "lred") color=${lightred} + ;; + "lgreen") color=${lightgreen} + ;; + "lyellow") color=${lightyellow} + ;; + "lblue") color=${lightblue} + ;; + "lmagenta") color=${lightmagenta} + ;; + "lcyan") color=${lightcyan} + ;; + "white") color=${white} + ;; + *) echo -ne "${red}Unknown text color:${reset}" + ;; + esac + + echo -e "${color}${text}${reset}" +} \ No newline at end of file diff --git a/.bin/watch_fs.sh b/.bin/watch_fs.sh new file mode 100644 index 0000000..0f5e7ac --- /dev/null +++ b/.bin/watch_fs.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +function watch_fs() { + if [[ "$#" -eq 0 || "$#" -gt 1 ]]; then return + fi + + local dir=$1 + local chsum1="" + + log "white" "Watching $dir for changes..." + + while [[ true ]]; do + chsum2="$(find "$dir" -type f -exec md5sum {} \;)" + if [[ $chsum1 != $chsum2 ]] ; then + if [ -n "$chsum1" ]; then + return + fi + chsum1=$chsum2 + fi + sleep 1 + done +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca2b502 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.zip +*.op +dist/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e97dd38 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Dan Whitacre + +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. diff --git a/info.toml b/info.toml new file mode 100644 index 0000000..352cc4b --- /dev/null +++ b/info.toml @@ -0,0 +1,6 @@ +[meta] +name = "Snake" +author = "DanOnTheMoon" +category = "Game" +version = "1.0.0" +siteid = 510 \ No newline at end of file diff --git a/src/Apple.as b/src/Apple.as new file mode 100644 index 0000000..39dfb5d --- /dev/null +++ b/src/Apple.as @@ -0,0 +1,11 @@ +class Apple { + vec2 position; + + Apple() { + this.Respawn(); + } + + void Respawn() { + position = vec2(Math::Rand(0, (Draw::GetWidth() - 1) / g_gridSize), Math::Rand(0, (Draw::GetHeight() - 1)) / g_gridSize); + } +} \ No newline at end of file diff --git a/src/GameController.as b/src/GameController.as new file mode 100644 index 0000000..99a6c73 --- /dev/null +++ b/src/GameController.as @@ -0,0 +1,72 @@ +class GameController { + bool running; + Snake@ snake; + Apple@ apple; + + GameController() { + running = false; + @snake = @Snake(); + @apple = @Apple(); + } + + void StartGame() { + if (running) return; + LogTrace("Game started"); + running = true; + snake = Snake(); + apple.Respawn(); + } + + void StopGame() { + if (!running) return; + LogTrace("Game stopped"); + running = false; + if (S_Snake_LastScore > S_Snake_HighScore) { + S_Snake_HighScore = S_Snake_LastScore; + NotifySuccess("New high score! " + Text::Format("%d", S_Snake_HighScore)); + } + } + + void Update() { + if (!running) return; + + LogTrace("Snake position: " + Text::Format("%f", snake.segments[0].position.x) + "," + Text::Format("%f", snake.segments[0].position.y)); + LogTrace("Apple position: " + Text::Format("%f", apple.position.x) + "," + Text::Format("%f", apple.position.y)); + + snake.Move(); + + if (snake.segments[0].position == apple.position) { + snake.Grow(); + apple.Respawn(); + LogTrace("Got apple! Score: " + Text::Format("%d", S_Snake_LastScore)); + } + + if (!snake.alive) { + NotifyInfo("You died! Score: " + Text::Format("%d", S_Snake_LastScore)); + StopGame(); + } + } + + void Draw() { + if (S_Display_HideWithIFace && !UI::IsGameUIVisible()) return; + if (S_Display_HideWithOverlay && !UI::IsOverlayShown()) return; + if (!S_Display_Visible) return; + + if (!running) return; + + DrawApple(apple); + DrawSnake(snake); + } + + void HandleInput(VirtualKey key) { + if (!running) { + if (key == S_Snake_StartGameKey) StartGame(); + return; + } + + if (key == S_Snake_LeftKey) snake.ChangeDirection(vec2(-1, 0)); + else if (key == S_Snake_RightKey) snake.ChangeDirection(vec2(1, 0)); + else if (key == S_Snake_UpKey) snake.ChangeDirection(vec2(0, -1)); + else if (key == S_Snake_DownKey) snake.ChangeDirection(vec2(0, 1)); + } +} \ No newline at end of file diff --git a/src/Interface.as b/src/Interface.as new file mode 100644 index 0000000..7b5986b --- /dev/null +++ b/src/Interface.as @@ -0,0 +1,18 @@ +void DrawSnake(Snake@ snake) { + nvg::FillColor(vec4(0, 255, 0, 255)); + + for (uint i = 0; i < snake.segments.Length; i++) { + auto segment = snake.segments[i]; + nvg::BeginPath(); + nvg::Rect(segment.position.x * g_gridSize, segment.position.y * g_gridSize, g_gridSize, g_gridSize); + nvg::Fill(); + } +} + +void DrawApple(Apple@ apple) { + nvg::FillColor(vec4(255, 0, 0, 255)); + + nvg::BeginPath(); + nvg::Rect(apple.position.x * g_gridSize, apple.position.y * g_gridSize, g_gridSize, g_gridSize); + nvg::Fill(); +} \ No newline at end of file diff --git a/src/Log.as b/src/Log.as new file mode 100644 index 0000000..fc5a3cb --- /dev/null +++ b/src/Log.as @@ -0,0 +1,17 @@ +void LogTrace(const string &in msg) { + if (S_Advanced_DevLog) { + trace(msg); + } +} + +void LogInfo(const string &in msg) { + trace(msg); +} + +void LogWarning(const string &in msg) { + warn(msg); +} + +void LogError(const string &in msg) { + error(msg); +} diff --git a/src/Main.as b/src/Main.as new file mode 100644 index 0000000..277c570 --- /dev/null +++ b/src/Main.as @@ -0,0 +1,49 @@ +GameController gc; +float g_delta_step = 50.f; +float g_delta_current = 0.f; +int g_gridSize = 20; + +void Main() { + while (true) { + try { + if (gc is null) { + gc = GameController(); + } + } catch { + LogError(getExceptionInfo()); + } + sleep(60000); + } +} + +void OnKeyPress(bool down, VirtualKey key) { + if (down && gc !is null) gc.HandleInput(key); + if (down && key == S_Display_VisibleKey) S_Display_Visible = !S_Display_Visible; +} + +void RenderMenu() { + if (UI::BeginMenu(PluginDisplayName)) { + if (UI::MenuItem("Play Game") && gc !is null) gc.StartGame(); + if (UI::MenuItem("Stop Game") && gc !is null) gc.StopGame(); + UI::MenuItem("High Score: " + Text::Format("%d", S_Snake_HighScore)); + UI::MenuItem("Last Score: " + Text::Format("%d", S_Snake_LastScore)); + if (UI::MenuItem("Toggle Display", "", S_Display_Visible)) S_Display_Visible = !S_Display_Visible; + + UI::EndMenu(); + } +} + +void Render() { + if (gc is null) return; + gc.Draw(); +} + +void Update(float dt) { + if (gc is null) return; + g_delta_current += dt; + // LogTrace("DT: " + Text::Format("%f", dt) + " Cur: " + Text::Format("%f", g_delta_current)); + if (g_delta_current >= g_delta_step) { + gc.Update(); + g_delta_current = 0.f; + } +} diff --git a/src/Notify.as b/src/Notify.as new file mode 100644 index 0000000..31cd8f8 --- /dev/null +++ b/src/Notify.as @@ -0,0 +1,19 @@ +void NotifyInfo(const string &in msg) { + UI::ShowNotification(Meta::ExecutingPlugin().Name, msg); + LogInfo("Notified: " + msg); +} + +void NotifySuccess(const string &in msg) { + UI::ShowNotification(Meta::ExecutingPlugin().Name, msg, vec4(.4, .7, .1, .3), 10000); + LogInfo("Notified: " + msg); +} + +void NotifyWarning(const string &in msg) { + LogWarning(msg); + UI::ShowNotification(Meta::ExecutingPlugin().Name + ": Warning", msg, vec4(.9, .6, .2, .3), 15000); +} + +void NotifyError(const string &in msg) { + LogError(msg); + UI::ShowNotification(Meta::ExecutingPlugin().Name + ": Error", msg, vec4(.9, .3, .1, .3), 15000); +} diff --git a/src/Plugin.as b/src/Plugin.as new file mode 100644 index 0000000..eed985a --- /dev/null +++ b/src/Plugin.as @@ -0,0 +1,4 @@ +string PluginDisplayIcon = "\\$0f0" + Icons::Apple + "\\$z"; +string PluginDisplayName = PluginDisplayIcon + " " + Meta::ExecutingPlugin().Name; +string PluginDisplayVersion = "\\$0f0 v" + Meta::ExecutingPlugin().Version + "\\$z"; +string PluginDisplayNameAndVersion = PluginDisplayName + " " + PluginDisplayVersion; diff --git a/src/Snake.as b/src/Snake.as new file mode 100644 index 0000000..2b0f1c6 --- /dev/null +++ b/src/Snake.as @@ -0,0 +1,58 @@ +class SnakeSegment { + vec2 position; + SnakeSegment(vec2 pos) { + position = pos; + } +} + +class Snake { + array segments = {}; + vec2 direction; + bool alive; + + Snake() { + direction = vec2(1, 0); + alive = true; + S_Snake_LastScore = 0; + + for (uint i = 0; i < 4; i++) { + segments.InsertLast(SnakeSegment(vec2(10 - i, 5))); + } + } + + void Move() { + if (!alive) return; + + vec2 nextPos = segments[0].position + direction; + + // check for collision + if (nextPos.x < 0 || nextPos.x * g_gridSize >= Draw::GetWidth() || nextPos.y < 0 || nextPos.y * g_gridSize >= Draw::GetHeight() || CheckSelfCollision(nextPos)) { + alive = false; + return; + } + + // move the snake by adding a new segment at the front and removing the last segment + segments.InsertAt(0, SnakeSegment(nextPos)); + segments.RemoveLast(); + } + + bool CheckSelfCollision(vec2 pos) { + for (uint i = 1; i < segments.Length; i++) { + if (segments[i].position == pos) { + return true; + } + } + return false; + } + + void ChangeDirection(vec2 newDir) { + LogTrace("Change Direction: " + Text::Format("%f", newDir.x) + "," + Text::Format("%f", newDir.y)); + if (newDir * -1 != direction) // Prevent the snake from reversing + direction = newDir; + } + + void Grow() { + segments.InsertLast(SnakeSegment(segments[segments.Length - 1].position)); + S_Snake_LastScore += S_Snake_ScorePerApple; + } +} diff --git a/src/settings/10_Snake.as b/src/settings/10_Snake.as new file mode 100644 index 0000000..0e60041 --- /dev/null +++ b/src/settings/10_Snake.as @@ -0,0 +1,23 @@ +[Setting category="Snake" hidden] +int S_Snake_HighScore = 0; + +[Setting category="Snake" hidden] +int S_Snake_LastScore = 0; + +[Setting category="Snake" hidden] +int S_Snake_ScorePerApple = 10; + +[Setting category="Snake" name="Start Game"] +VirtualKey S_Snake_StartGameKey = VirtualKey::F7; + +[Setting category="Snake" name="Left Key"] +VirtualKey S_Snake_LeftKey = VirtualKey::Left; + +[Setting category="Snake" name="Right Key"] +VirtualKey S_Snake_RightKey = VirtualKey::Right; + +[Setting category="Snake" name="Up Key"] +VirtualKey S_Snake_UpKey = VirtualKey::Up; + +[Setting category="Snake" name="Down Key"] +VirtualKey S_Snake_DownKey = VirtualKey::Down; \ No newline at end of file diff --git a/src/settings/20_Display.as b/src/settings/20_Display.as new file mode 100644 index 0000000..917f85a --- /dev/null +++ b/src/settings/20_Display.as @@ -0,0 +1,11 @@ +[Setting category="Display" name="Hide when the game interface is hidden"] +bool S_Display_HideWithIFace = false; + +[Setting category="Display" name="Hide when the Openplanet overlay is hidden"] +bool S_Display_HideWithOverlay = false; + +[Setting category="Display" name="Display visibility hotkey"] +VirtualKey S_Display_VisibleKey = VirtualKey(0); + +[Setting category="Display" name="Display visible"] +bool S_Display_Visible = true; diff --git a/src/settings/27_Advanced.as b/src/settings/27_Advanced.as new file mode 100644 index 0000000..affa48e --- /dev/null +++ b/src/settings/27_Advanced.as @@ -0,0 +1,2 @@ +[Setting category="Advanced" name="Developer log trace" description="Log way too much info. Only need to enable this when trying to capture logs for reporting issues."] +bool S_Advanced_DevLog = false; \ No newline at end of file