SVI преди 3 месеца
родител
ревизия
4d7a6a8534
променени са 16 файла, в които са добавени 326 реда и са изтрити 10696 реда
  1. 4 0
      .gitignore
  2. 8 0
      README.md
  3. BIN
      assets/UbuntuMono-R.ttf
  4. BIN
      assets/font.png
  5. 0 1
      assets/font.png.dat
  6. BIN
      assets/icon.png
  7. 1 41
      config.nims
  8. 1 1
      doc/part01/main.md
  9. 0 28
      index.html
  10. 0 126
      nico_minimal.html
  11. 16 0
      nimble.lock
  12. 3 8
      nimble.paths
  13. 0 126
      nimcity.html
  14. 0 10305
      nimcity.js
  15. 16 60
      nimcity.nimble
  16. 277 0
      src/nimcity.nim

+ 4 - 0
.gitignore

@@ -2,3 +2,7 @@
 /nimcity.wasm
 /nimсity
 /tmp
+nimble.develop
+nimble.paths
+nimbledeps
+/nimcity

+ 8 - 0
README.md

@@ -4,6 +4,14 @@
 
 [Документация](./doc/index.md)
 
+## Зависимости
+
+```bash
+sudo apt update && sudo apt install libsdl2-dev -y
+sudo apt update && sudo apt install libsdl2-ttf-dev libsdl2-image-dev -y
+sudo apt update && sudo apt install fonts-liberation -y
+```
+
 ## Сборка
 
 ```bash

BIN
assets/UbuntuMono-R.ttf


BIN
assets/font.png


+ 0 - 1
assets/font.png.dat

@@ -1 +0,0 @@
- !"#$%&'()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ{}~

BIN
assets/icon.png


+ 1 - 41
config.nims

@@ -1,45 +1,5 @@
-when defined(emscripten):
-  import std/compilesettings
-  import strformat
-
-  # This path will only run if -d:emscripten is passed to nim.
-  --nimcache:tmp # Store intermediate files close by in the ./tmp dir.
-
-  --os:linux # Emscripten pretends to be linux.
-  --cpu:wasm32 # Emscripten is 32bits.
-  --cc:clang # Emscripten is very close to clang, so we ill replace it.
-  when defined(windows):
-    --clang.exe:emcc.bat  # Replace C
-    --clang.linkerexe:emcc.bat # Replace C linker
-    --clang.cpp.exe:emcc.bat # Replace C++
-    --clang.cpp.linkerexe:emcc.bat # Replace C++ linker.
-  else:
-    --clang.exe:emcc  # Replace C
-    --clang.linkerexe:emcc # Replace C linker
-    --clang.cpp.exe:emcc # Replace C++
-    --clang.cpp.linkerexe:emcc # Replace C++ linker.
-  --listCmd # List what commands we are running so that we can debug them.
-
-  --gc:orc # GC:arc is friendlier with crazy platforms.
-  --exceptions:goto # Goto exceptions are friendlier with crazy platforms.
-  --define:noSignalHandler # Emscripten doesn't support signal handlers.
-
-  --dynlibOverride:SDL2
-
-  when defined(opengl):
-    --dynlibOverride:opengl
-
-  --define:emscripten
-
-  # Pass this to Emscripten linker to generate html file scaffold for us.
-  const projName = projectName()
-  const userSetOutFile = querySetting(SingleValueSetting.outFile)
-  const outFile = if userSetOutFile == "": &"{projName}.html" else: userSetOutFile
-  when defined(debug):
-    switch("passL", &"-o {outFile} --shell-file nico_minimal.html -lidbfs.js -s ASSERTIONS=1 -s USE_SDL=2 -s FORCE_FILESYSTEM=1 -s ALLOW_MEMORY_GROWTH -s EXPORTED_FUNCTIONS=[\"_main\",\"_initConfigDone\"] -s EXPORTED_RUNTIME_METHODS=[\"ccall\",\"cwrap\",\"print\"] -O0 --preload-file assets")
-  else:
-    switch("passL", &"-o {outFile} --shell-file nico_minimal.html -lidbfs.js -s ASSERTIONS=1 -s USE_SDL=2 -s FORCE_FILESYSTEM=1 -s ALLOW_MEMORY_GROWTH -s EXPORTED_FUNCTIONS=[\"_main\",\"_initConfigDone\"] -s EXPORTED_RUNTIME_METHODS=[\"ccall\",\"cwrap\",\"print\"] -O3 --preload-file assets")
 # begin Nimble config (version 2)
+--noNimblePath
 when withDir(thisDir(), system.fileExists("nimble.paths")):
   include "nimble.paths"
 # end Nimble config

+ 1 - 1
doc/part01/main.md

@@ -1,3 +1,3 @@
 # Часть 1. Введение в Nim с помощью графики
 
-- [Главай 1](./ch01.md)
+- [Глава 1](./ch01.md)

+ 0 - 28
index.html

@@ -1,28 +0,0 @@
-<html>
-	<head>
-		<style>
-			body {
-				background: #111111;
-				width: 100%;
-				margin: auto 0;
-				overflow: hidden;
-			}
-			canvas {
-				display: block;
-				margin: 2em auto;
-				image-rendering: -moz-crisp-edges;
-				image-rendering: -webkit-crisp-edges;
-				image-rendering: pixelated;
-			}
-		</style>
-		<title>nimсity</title>
-	</head>
-	<body>
-	<div id="nicogame"></div>
-		<script type="text/javascript">
-		function handleMouseDown(evt) { window.focus(); evt.preventDefault(); evt.stopPropagation(); }
-		document.getElementById('nicogame').addEventListener('mousedown', handleMouseDown, false);
-		</script>
-		<script type="text/javascript" src="nimсity.js"></script>
-	</body>
-</html>

+ 0 - 126
nico_minimal.html

@@ -1,126 +0,0 @@
-<!doctype html>
-<html lang="en-us">
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-    <title>nimсity</title>
-    <style>
-        body {
-            background: #000;
-            margin: 0;
-            padding: 0;
-        }
-      textarea.emscripten { font-family: monospace; width: 80%; }
-      div.emscripten { text-align: center; }
-      div.emscripten_border { border: 1px solid black; }
-      /* the canvas *must not* have any border or padding, or mouse coords will be wrong */
-      canvas.emscripten {
-          border: 0px none; background-color: black; margin: 0;
-            position: absolute;
-                left: 0;
-                top: 0;
-                width: 100%;
-                height: 100%;
-      }
-
-      .spinner {
-        height: 50px;
-        width: 50px;
-        margin: 0px auto;
-        -webkit-animation: rotation .8s linear infinite;
-        -moz-animation: rotation .8s linear infinite;
-        -o-animation: rotation .8s linear infinite;
-        animation: rotation 0.8s linear infinite;
-        border-left: 10px solid rgb(0,150,240);
-        border-right: 10px solid rgb(0,150,240);
-        border-bottom: 10px solid rgb(0,150,240);
-        border-top: 10px solid rgb(100,0,200);
-        border-radius: 100%;
-        background-color: rgb(200,100,250);
-      }
-      @-webkit-keyframes rotation {
-        from {-webkit-transform: rotate(0deg);}
-        to {-webkit-transform: rotate(360deg);}
-      }
-      @-moz-keyframes rotation {
-        from {-moz-transform: rotate(0deg);}
-        to {-moz-transform: rotate(360deg);}
-      }
-      @-o-keyframes rotation {
-        from {-o-transform: rotate(0deg);}
-        to {-o-transform: rotate(360deg);}
-      }
-      @keyframes rotation {
-        from {transform: rotate(0deg);}
-        to {transform: rotate(360deg);}
-      }
-
-    </style>
-  </head>
-  <body>
-    <figure style="overflow:visible;" id="spinner"><div class="spinner"></div><center style="margin-top:0.5em"><strong>emscripten</strong></center></figure>
-    <div class="emscripten" id="status">Downloading...</div>
-    <div class="emscripten">
-      <progress value="0" max="100" id="progress" hidden=1></progress>
-    </div>
-      <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" onmouseenter="window.focus()" onclick="window.focus()" tabindex=-1></canvas>
-    <script type='text/javascript'>
-      var statusElement = document.getElementById('status');
-      var progressElement = document.getElementById('progress');
-      var spinnerElement = document.getElementById('spinner');
-
-      var Module = {
-        preRun: [],
-        postRun: [],
-        canvas: (function() {
-          var canvas = document.getElementById('canvas');
-
-          // As a default initial behavior, pop up an alert when webgl context is lost. To make your
-          // application robust, you may want to override this behavior before shipping!
-          // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
-          canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
-
-          return canvas;
-        })(),
-        setStatus: function(text) {
-          if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
-          if (text === Module.setStatus.last.text) return;
-          var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
-          var now = Date.now();
-          if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
-          Module.setStatus.last.time = now;
-          Module.setStatus.last.text = text;
-          if (m) {
-            text = m[1];
-            progressElement.value = parseInt(m[2])*100;
-            progressElement.max = parseInt(m[4])*100;
-            progressElement.hidden = false;
-            spinnerElement.hidden = false;
-          } else {
-            progressElement.value = null;
-            progressElement.max = null;
-            progressElement.hidden = true;
-            if (!text) spinnerElement.hidden = true;
-          }
-          statusElement.innerHTML = text;
-        },
-        totalDependencies: 0,
-        monitorRunDependencies: function(left) {
-          this.totalDependencies = Math.max(this.totalDependencies, left);
-          Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
-        },
-        'print': function(text) { console.log('stdout: ' + text) },
-        'printErr': function(text) { console.log('stderr: ' + text) }
-      };
-      Module.setStatus('Downloading...');
-      window.onerror = function() {
-        Module.setStatus('Exception thrown, see JavaScript console');
-        spinnerElement.style.display = 'none';
-        Module.setStatus = function(text) {
-          if (text) Module.printErr('[post-exception status] ' + text);
-        };
-      };
-    </script>
-    {{{ SCRIPT }}}
-  </body>
-</html>

+ 16 - 0
nimble.lock

@@ -0,0 +1,16 @@
+{
+  "version": 2,
+  "packages": {
+    "nim": {
+      "version": "2.2.10",
+      "vcsRevision": "9fe2137fa2f3f66cf5a44f357d461829ac9e20c4",
+      "url": "https://github.com/nim-lang/Nim.git",
+      "downloadMethod": "git",
+      "dependencies": [],
+      "checksums": {
+        "sha1": "17ec440fdb89f8903db29a17898af590087d2b64"
+      }
+    }
+  },
+  "tasks": {}
+}

+ 3 - 8
nimble.paths

@@ -1,9 +1,4 @@
 --noNimblePath
---path:"/home/user/.nimble/pkgs2/gifenc-0.1.1-5894fb9561f4ed59c1f7a2e9b57af9a0348fe390"
---path:"/home/user/coding/nim/nimсity/src"
---path:"/home/user/.nimble/pkgs2/x11-1.2-6db9aa495a228e9e68faa80acf301f60accd8e5a"
---path:"/home/user/.nimble/pkgs2/zippy-0.10.19-706d8c10778f32d28b060dbcc84143ca4bfb9bcd"
---path:"/home/user/.nimble/pkgs2/nimPNG-0.3.6-35de54ccffac88f3bfae87faddce507bdebeaae1"
---path:"/home/user/.nimble/pkgs2/sdl2_nim-2.0.14.3-dd020f93fcf762e28d3d1462a7bba0c91a5a36dd"
---path:"/home/user/.nimble/pkgs2/winim-3.9.4-4b7c430159be2e8b2dc4b663a9b19f7678b58f87"
---path:"/home/user/.nimble/pkgs2/nico-0.4.10-f4d0514d40cc9aeaa3f02e3654986ff28d1038ec"
+--path:"/home/user/.nimble/pkgs2/sdl2-2.0.6-360d842a8065b55cea326727422ca0f5b052ffea"
+--path:"/home/user/.nimble/pkgs2/pixels-0.1.0-d6b41f4a271e9eedfe1e6e8c13d7fb6f010b6c23"
+--path:"/home/user/coding/nim/nimcity/src"

+ 0 - 126
nimcity.html

@@ -1,126 +0,0 @@
-<!doctype html>
-<html lang="en-us">
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-    <title>nimсity</title>
-    <style>
-        body {
-            background: #000;
-            margin: 0;
-            padding: 0;
-        }
-      textarea.emscripten { font-family: monospace; width: 80%; }
-      div.emscripten { text-align: center; }
-      div.emscripten_border { border: 1px solid black; }
-      /* the canvas *must not* have any border or padding, or mouse coords will be wrong */
-      canvas.emscripten {
-          border: 0px none; background-color: black; margin: 0;
-            position: absolute;
-                left: 0;
-                top: 0;
-                width: 100%;
-                height: 100%;
-      }
-
-      .spinner {
-        height: 50px;
-        width: 50px;
-        margin: 0px auto;
-        -webkit-animation: rotation .8s linear infinite;
-        -moz-animation: rotation .8s linear infinite;
-        -o-animation: rotation .8s linear infinite;
-        animation: rotation 0.8s linear infinite;
-        border-left: 10px solid rgb(0,150,240);
-        border-right: 10px solid rgb(0,150,240);
-        border-bottom: 10px solid rgb(0,150,240);
-        border-top: 10px solid rgb(100,0,200);
-        border-radius: 100%;
-        background-color: rgb(200,100,250);
-      }
-      @-webkit-keyframes rotation {
-        from {-webkit-transform: rotate(0deg);}
-        to {-webkit-transform: rotate(360deg);}
-      }
-      @-moz-keyframes rotation {
-        from {-moz-transform: rotate(0deg);}
-        to {-moz-transform: rotate(360deg);}
-      }
-      @-o-keyframes rotation {
-        from {-o-transform: rotate(0deg);}
-        to {-o-transform: rotate(360deg);}
-      }
-      @keyframes rotation {
-        from {transform: rotate(0deg);}
-        to {transform: rotate(360deg);}
-      }
-
-    </style>
-  </head>
-  <body>
-    <figure style="overflow:visible;" id="spinner"><div class="spinner"></div><center style="margin-top:0.5em"><strong>emscripten</strong></center></figure>
-    <div class="emscripten" id="status">Downloading...</div>
-    <div class="emscripten">
-      <progress value="0" max="100" id="progress" hidden=1></progress>
-    </div>
-      <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" onmouseenter="window.focus()" onclick="window.focus()" tabindex=-1></canvas>
-    <script type='text/javascript'>
-      var statusElement = document.getElementById('status');
-      var progressElement = document.getElementById('progress');
-      var spinnerElement = document.getElementById('spinner');
-
-      var Module = {
-        preRun: [],
-        postRun: [],
-        canvas: (function() {
-          var canvas = document.getElementById('canvas');
-
-          // As a default initial behavior, pop up an alert when webgl context is lost. To make your
-          // application robust, you may want to override this behavior before shipping!
-          // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
-          canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
-
-          return canvas;
-        })(),
-        setStatus: function(text) {
-          if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
-          if (text === Module.setStatus.last.text) return;
-          var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
-          var now = Date.now();
-          if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
-          Module.setStatus.last.time = now;
-          Module.setStatus.last.text = text;
-          if (m) {
-            text = m[1];
-            progressElement.value = parseInt(m[2])*100;
-            progressElement.max = parseInt(m[4])*100;
-            progressElement.hidden = false;
-            spinnerElement.hidden = false;
-          } else {
-            progressElement.value = null;
-            progressElement.max = null;
-            progressElement.hidden = true;
-            if (!text) spinnerElement.hidden = true;
-          }
-          statusElement.innerHTML = text;
-        },
-        totalDependencies: 0,
-        monitorRunDependencies: function(left) {
-          this.totalDependencies = Math.max(this.totalDependencies, left);
-          Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
-        },
-        'print': function(text) { console.log('stdout: ' + text) },
-        'printErr': function(text) { console.log('stderr: ' + text) }
-      };
-      Module.setStatus('Downloading...');
-      window.onerror = function() {
-        Module.setStatus('Exception thrown, see JavaScript console');
-        spinnerElement.style.display = 'none';
-        Module.setStatus = function(text) {
-          if (text) Module.printErr('[post-exception status] ' + text);
-        };
-      };
-    </script>
-    <script async type="text/javascript" src="nimсity.js"></script>
-  </body>
-</html>

Файловите разлики са ограничени, защото са твърде много
+ 0 - 10305
nimcity.js


+ 16 - 60
nimcity.nimble

@@ -1,66 +1,22 @@
 # Package
 
-version = "0.1.0"
-author = "SVI"
-description = "nimсity"
-license = "MIT"
+version       = "0.1.0"
+author        = "SVI"
+description   = "NimCity -- like SimCity, but with Nim)"
+license       = "MIT"
+srcDir        = "src"
+bin           = @["nimcity"]
 
-# Deps
-requires "nim >= 2.2.10"
-requires "nico >= 0.2.5"
-
-srcDir = "src"
-
-import strformat
-
-const releaseOpts = "-d:danger"
-const debugOpts = "-d:debug"
-
-task runr, "Runs nimсity for current platform":
- exec &"nim c -r {releaseOpts} -o:nimсity src/main.nim"
+# Dependencies
 
-task rund, "Runs debug nimсity for current platform":
- exec &"nim c -r {debugOpts} -o:nimсity src/main.nim"
-
-task release, "Builds nimсity for current platform":
- exec &"nim c {releaseOpts} -o:nimсity src/main.nim"
-
-task webd, "Builds debug nimсity for web":
- exec &"nim c {debugOpts} -d:emscripten -o:nimсity.html src/main.nim"
-
-task webr, "Builds release nimсity for web":
- exec &"nim c {releaseOpts} -d:emscripten -o:nimсity.html src/main.nim"
-
-task debug, "Builds debug nimсity for current platform":
- exec &"nim c {debugOpts} -o:nimсity_debug src/main.nim"
+requires "nim >= 2.2.10"
+requires "pixels"
+requires "sdl2"
 
-task deps, "Downloads dependencies":
- if defined(windows):
-  if not fileExists("SDL2.dll"):
-   if not fileExists("SDL2_x64.zip"):
-    exec "curl https://www.libsdl.org/release/SDL2-2.0.20-win32-x64.zip -o SDL2_x64.zip"
-   if findExe("tar") != "":
-    exec "tar -xf SDL2_x64.zip SDL2.dll"
-   else:
-    exec "unzip SDL2_x64.zip SDL2.dll"
-  if fileExists("SDL2_x64.zip"):
-   rmFile("SDL2_x64.zip")
- elif defined(macosx) and findExe("brew") != "":
-  exec "brew list sdl2 || brew install sdl2"
- else:
-  echo "I don't know how to install SDL on your OS! 😿"
+task prod, "Сборка для прода":
+  # Exec запускает внешнюю команду (в данном случае компилятор Nim)
+  exec "nim c -d:release --opt:speed ./src/nimcity.nim"
 
-task androidr, "Release build for android":
-  if defined(windows):
-    exec &"nicoandroid.cmd"
-  else:
-    exec &"nicoandroid"
-  exec &"nim c -c --nimcache:android/app/jni/src/armeabi {releaseOpts}  --cpu:arm   --os:android -d:androidNDK --noMain --genScript src/main.nim"
-  exec &"nim c -c --nimcache:android/app/jni/src/arm64   {releaseOpts}  --cpu:arm64 --os:android -d:androidNDK --noMain --genScript src/main.nim"
-  exec &"nim c -c --nimcache:android/app/jni/src/x86     {releaseOpts}  --cpu:i386  --os:android -d:androidNDK --noMain --genScript src/main.nim"
-  exec &"nim c -c --nimcache:android/app/jni/src/x86_64  {releaseOpts}  --cpu:amd64 --os:android -d:androidNDK --noMain --genScript src/main.nim"
-  withDir "android":
-    if defined(windows):
-      exec &"gradlew.bat assembleDebug"
-    else:
-      exec "./gradlew assembleDebug"
+task dev, "Сборка для отладки":
+  # Exec запускает внешнюю команду (в данном случае компилятор Nim)
+  exec "nim c -d:debug ./src/nimcity.nim"

+ 277 - 0
src/nimcity.nim

@@ -0,0 +1,277 @@
+import random
+
+import sdl2
+import sdl2/ttf
+
+const
+  WindowWidth = 640
+  WindowHeight = 480
+
+  PaddleWidth = 16
+  PaddleHeight = 64
+  PaddleSpeed = 400.0
+
+  BallRadius = 8
+
+  MaxBallComponentSpeed = 1000'f32
+
+  TextWidth = 128
+  TextHeight = 64
+
+type
+  Paddle = ref object
+    x, y: float32
+
+  Ball = ref object
+    x, y: float32
+    vx, vy: float32
+
+  Input {.pure.} = enum
+    Up,
+    Down,
+    None
+
+  Game = ref object
+    running: bool
+
+    inputs: array[Input, bool]
+    player, opponent: Paddle
+    ball: Ball
+
+    scores: tuple[player: uint, opponent: uint]
+
+proc newPaddle(x: float32): Paddle =
+  Paddle(
+    x: x,
+    y: (WindowHeight + PaddleHeight) / 2,
+  )
+
+proc newBall(): Ball =
+  Ball(
+    x: WindowWidth / 2 - BallRadius,
+    y: WindowHeight / 2 - BallRadius,
+    vx: (rand(2.0) - 1) * 100,
+    vy: (rand(2.0) - 1) * 300
+  )
+
+func collision(ball: Ball, paddle: Paddle): bool =
+  return not (
+    ball.x > paddle.x + PaddleWidth or
+    ball.x + 2 * BallRadius < paddle.x or
+    ball.y > paddle.y + PaddleHeight or
+    ball.y + 2 * BallRadius < paddle.y
+  )
+
+proc updatePlayer(g: Game, dt: float32) =
+  if g.inputs[Input.Up]:
+    g.player.y -= PaddleSpeed * dt
+  if g.inputs[Input.Down]:
+    g.player.y += PaddleSpeed * dt
+
+  if g.player.y < 0:
+    g.player.y = 0
+  elif g.player.y + PaddleHeight > WindowHeight:
+    g.player.y = WindowHeight - PaddleHeight
+
+proc updateOpponent(g: Game, dt: float32) =
+  let dist = (g.ball.y + BallRadius) - (g.opponent.y + PaddleHeight / 2)
+  var dy: float32
+
+  if dist > 0:
+    dy = min(dist, PaddleSpeed * dt)
+  elif dist < 0:
+    dy = max(dist, -PaddleSpeed * dt)
+
+  g.opponent.y += dy
+  g.opponent.y = max(g.opponent.y, 0)
+  g.opponent.y = min(g.opponent.y, WindowHeight - PaddleWidth)
+
+proc bounce(v: var float32) =
+  v *= -1
+
+  v = max(v, -MaxBallComponentSpeed)
+  v = min(v,  MaxBallComponentSpeed)
+
+proc speedup(v: var float32) =
+  v *= (1 + rand(0.5))
+
+  v = max(v, -MaxBallComponentSpeed)
+  v = min(v,  MaxBallComponentSpeed)
+
+proc updateBall(g: Game, dt: float32) =
+  g.ball.x += g.ball.vx * dt
+  g.ball.y += g.ball.vy * dt
+
+  # bounce on upper and lower borders, add speedup on bounce
+  if g.ball.y < 0:
+    g.ball.y = 0
+    bounce(g.ball.vy)
+    speedup(g.ball.vx)
+  elif g.ball.y + 2 * BallRadius > WindowHeight:
+    g.ball.y = WindowHeight - 2 * BallRadius
+    bounce(g.ball.vy)
+    speedup(g.ball.vx)
+
+  # bounce on paddles
+  if g.ball.collision g.player:
+    g.ball.x = g.player.x + PaddleWidth
+    bounce(g.ball.vx)
+    speedup(g.ball.vy)
+  elif g.ball.collision g.opponent:
+    g.ball.x = g.opponent.x - 2 * BallRadius
+    bounce(g.ball.vx)
+    speedup(g.ball.vy)
+
+  # opponent scored
+  if g.ball.x + 2 * BallRadius < 0:
+    inc g.scores.opponent
+    g.ball = newBall()
+
+  # player scored
+  elif g.ball.x > WindowWidth:
+    inc g.scores.player
+    g.ball = newBall()
+
+
+proc draw(renderer: RendererPtr, paddle: Paddle) =
+  renderer.setDrawColor 255, 255, 255, 255 # white
+  var r = rect(
+    cint(paddle.x), cint(paddle.y),
+    cint(PaddleWidth), cint(PaddleHeight)
+  )
+  renderer.fillRect(r)
+
+proc draw(renderer: RendererPtr, ball: Ball) =
+  renderer.setDrawColor 255, 255, 255, 255 # white
+  var r = rect(
+    cint(ball.x), cint(ball.y),
+    cint(2 * BallRadius), cint(2 * BallRadius)
+  )
+  renderer.fillRect(r)
+
+proc drawScores(
+  renderer: RendererPtr, font: FontPtr, scores: tuple[player: uint, opponent: uint]
+) =
+  let
+    color = color(255, 255, 255, 0)
+    text = $scores.player & " : " & $scores.opponent
+    surface = ttf.renderTextSolid(font, cstring text, color)
+    texture = renderer.createTextureFromSurface(surface)
+
+  surface.freeSurface
+  defer: texture.destroy
+
+  var r = rect(
+    (WindowWidth - TextWidth) div 2,
+    0,
+    TextWidth,
+    TextHeight
+  )
+  renderer.copy texture, nil, addr r
+
+proc newGame(): Game =
+  Game(
+    running: true,
+    player: newPaddle(PaddleWidth),
+    opponent: newPaddle(WindowWidth - 2 * PaddleWidth),
+    ball: newBall(),
+    scores: (0'u, 0'u)
+  )
+
+proc update(g: Game, dt: float32) =
+  g.updatePlayer dt
+  g.updateOpponent dt
+  g.updateBall dt
+
+proc draw(g: Game, renderer: RendererPtr, font: FontPtr) =
+  renderer.setDrawColor 0, 0, 0, 255 # black
+  renderer.clear()
+
+  renderer.draw(g.player)
+  renderer.draw(g.opponent)
+  renderer.draw(g.ball)
+
+  renderer.drawScores(font, g.scores)
+
+  renderer.present()
+
+func toInput(key: Scancode): Input =
+  case key
+  of SDL_SCANCODE_UP: Input.Up
+  of SDL_SCANCODE_DOWN: Input.Down
+  else: Input.None
+
+type SDLException = object of Defect
+
+template sdlFailIf(condition: typed, reason: string) =
+  if condition: raise SDLException.newException(
+    reason & ", SDL error " & $getError()
+  )
+
+proc main =
+  sdlFailIf(not sdl2.init(INIT_VIDEO or INIT_TIMER or INIT_EVENTS)):
+    "SDL2 initialization failed"
+  defer: sdl2.quit()
+
+  let window = createWindow(
+    title = "Pong",
+    x = SDL_WINDOWPOS_CENTERED,
+    y = SDL_WINDOWPOS_CENTERED,
+    w = WindowWidth,
+    h = WindowHeight,
+    flags = SDL_WINDOW_SHOWN
+  )
+
+  sdlFailIf window.isNil: "window could not be created"
+  defer: window.destroy()
+
+  let renderer = createRenderer(
+    window = window,
+    index = -1,
+    flags = Renderer_Accelerated or Renderer_PresentVsync or Renderer_TargetTexture
+  )
+  sdlFailIf renderer.isNil: "renderer could not be created"
+  defer: renderer.destroy()
+
+  sdlFailIf(not ttfInit()): "SDL_TTF initialization failed"
+  defer: ttfQuit()
+
+  let font = ttf.openFont("./assets/UbuntuMono-R.ttf", TextHeight)
+  sdlFailIf font.isNil: "font could not be created"
+
+  var
+    running = true
+    game = newGame()
+
+    dt: float32
+
+    counter: uint64
+    previousCounter: uint64
+
+  counter = getPerformanceCounter()
+
+  while running:
+    previousCounter = counter
+    counter = getPerformanceCounter()
+
+    dt = (counter - previousCounter).float / getPerformanceFrequency().float
+
+    var event = defaultEvent
+
+    while pollEvent(event):
+      case event.kind
+      of QuitEvent:
+        running = false
+        break
+
+      of KeyDown:
+        game.inputs[event.key.keysym.scancode.toInput] = true
+      of KeyUp:
+        game.inputs[event.key.keysym.scancode.toInput] = false
+      else:
+        discard
+
+    game.update(dt)
+    game.draw(renderer, font)
+
+main()

Някои файлове не бяха показани, защото твърде много файлове са промени