From 7eb6481cf23c8b2afafa5f00f31d888dad2ea20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Fri, 3 Jan 2020 10:24:50 +0100 Subject: [PATCH] First! --- Game/FrameBuffer.cs | 49 +++++++++ Game/Game.cs | 116 +++++++++++++++++++++ Game/Random.cs | 11 ++ Game/Snake.cs | 137 +++++++++++++++++++++++++ MiniBCL.cs | 106 ++++++++++++++++++++ MiniRuntime.cs | 89 +++++++++++++++++ Pal/Console.Windows.cs | 200 +++++++++++++++++++++++++++++++++++++ Pal/Environment.Windows.cs | 12 +++ Pal/Thread.Windows.cs | 10 ++ README.md | 79 +++++++++++++++ SeeSharpSnake.csproj | 49 +++++++++ SeeSharpSnake.gif | Bin 0 -> 14329 bytes nuget.config | 9 ++ 13 files changed, 867 insertions(+) create mode 100644 Game/FrameBuffer.cs create mode 100644 Game/Game.cs create mode 100644 Game/Random.cs create mode 100644 Game/Snake.cs create mode 100644 MiniBCL.cs create mode 100644 MiniRuntime.cs create mode 100644 Pal/Console.Windows.cs create mode 100644 Pal/Environment.Windows.cs create mode 100644 Pal/Thread.Windows.cs create mode 100644 README.md create mode 100644 SeeSharpSnake.csproj create mode 100644 SeeSharpSnake.gif create mode 100644 nuget.config diff --git a/Game/FrameBuffer.cs b/Game/FrameBuffer.cs new file mode 100644 index 0000000..5a34a01 --- /dev/null +++ b/Game/FrameBuffer.cs @@ -0,0 +1,49 @@ +using System; + +unsafe struct FrameBuffer +{ + public const int Width = 40; + public const int Height = 20; + public const int Area = Width * Height; + + fixed char _chars[Area]; + + public void SetPixel(int x, int y, char character) + { + _chars[y * Width + x] = character; + } + + public void Clear() + { + for (int i = 0; i < Area; i++) + _chars[i] = ' '; + } + + public readonly void Render() + { + Console.SetCursorPosition(0, 0); + + const ConsoleColor snakeColor = ConsoleColor.Green; + + Console.ForegroundColor = snakeColor; + + for (int i = 1; i <= Area; i++) + { + char c = _chars[i - 1]; + + if (c == '*' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) + { + Console.ForegroundColor = c == '*' ? ConsoleColor.Red : ConsoleColor.White; + Console.Write(c); + Console.ForegroundColor = snakeColor; + } + else + Console.Write(c); + + if (i % Width == 0) + { + Console.SetCursorPosition(0, i / Width - 1); + } + } + } +} diff --git a/Game/Game.cs b/Game/Game.cs new file mode 100644 index 0000000..a7e00f4 --- /dev/null +++ b/Game/Game.cs @@ -0,0 +1,116 @@ +using System; +using Thread = System.Threading.Thread; + +struct Game +{ + enum Result + { + Win, Loss + } + + private Random _random; + + private Game(uint randomSeed) + { + _random = new Random(randomSeed); + } + + private Result Run(ref FrameBuffer fb) + { + Snake s = new Snake( + (byte)(_random.Next() % FrameBuffer.Width), + (byte)(_random.Next() % FrameBuffer.Height), + (Snake.Direction)(_random.Next() % 4)); + + MakeFood(s, out byte foodX, out byte foodY); + + long gameTime = Environment.TickCount64; + + while (true) + { + fb.Clear(); + + if (!s.Update()) + { + s.Draw(ref fb); + return Result.Loss; + } + + s.Draw(ref fb); + + if (Console.KeyAvailable) + { + ConsoleKeyInfo ki = Console.ReadKey(intercept: true); + switch (ki.Key) + { + case ConsoleKey.UpArrow: + s.Course = Snake.Direction.Up; break; + case ConsoleKey.DownArrow: + s.Course = Snake.Direction.Down; break; + case ConsoleKey.LeftArrow: + s.Course = Snake.Direction.Left; break; + case ConsoleKey.RightArrow: + s.Course = Snake.Direction.Right; break; + } + } + + if (s.HitTest(foodX, foodY)) + { + if (s.Extend()) + MakeFood(s, out foodX, out foodY); + else + return Result.Win; + } + + fb.SetPixel(foodX, foodY, '*'); + + fb.Render(); + + gameTime += 100; + + long delay = gameTime - Environment.TickCount64; + if (delay >= 0) + Thread.Sleep((int)delay); + else + gameTime = Environment.TickCount64; + } + } + + void MakeFood(in Snake snake, out byte foodX, out byte foodY) + { + do + { + foodX = (byte)(_random.Next() % FrameBuffer.Width); + foodY = (byte)(_random.Next() % FrameBuffer.Height); + } + while (snake.HitTest(foodX, foodY)); + } + + public static void Main() + { + Console.SetWindowSize(FrameBuffer.Width, FrameBuffer.Height); + Console.SetBufferSize(FrameBuffer.Width, FrameBuffer.Height); + Console.Title = "See Sharp Snake"; + Console.CursorVisible = false; + + FrameBuffer fb = new FrameBuffer(); + + while (true) + { + Game g = new Game((uint)Environment.TickCount64); + Result result = g.Run(ref fb); + + string message = result == Result.Win ? "You win" : "You lose"; + + int position = (FrameBuffer.Width - message.Length) / 2; + for (int i = 0; i < message.Length; i++) + { + fb.SetPixel(position + i, FrameBuffer.Height / 2, message[i]); + } + + fb.Render(); + + Console.ReadKey(intercept: true); + } + } +} diff --git a/Game/Random.cs b/Game/Random.cs new file mode 100644 index 0000000..e8a6d2a --- /dev/null +++ b/Game/Random.cs @@ -0,0 +1,11 @@ +struct Random +{ + private uint _val; + + public Random(uint seed) + { + _val = seed; + } + + public uint Next() => _val = (1103515245 * _val + 12345) % 2147483648; +} \ No newline at end of file diff --git a/Game/Snake.cs b/Game/Snake.cs new file mode 100644 index 0000000..1fb5c90 --- /dev/null +++ b/Game/Snake.cs @@ -0,0 +1,137 @@ +struct Snake +{ + public const int MaxLength = 30; + + private int _length; + + // Body is a packed integer that packs the X coordinate, Y coordinate, and the character + // for the snake's body. + // Only primitive types can be used with C# `fixed`, hence this is an `int`. + private unsafe fixed int _body[MaxLength]; + + private Direction _direction; + private Direction _oldDirection; + + public Direction Course + { + set + { + if (_oldDirection != _direction) + _oldDirection = _direction; + + if (_direction - value != 2 && value - _direction != 2) + _direction = value; + } + } + + public unsafe Snake(byte x, byte y, Direction direction) + { + _body[0] = new Part(x, y, DirectionToChar(direction, direction)).Pack(); + _direction = direction; + _oldDirection = direction; + _length = 1; + } + + public unsafe bool Update() + { + Part oldHead = Part.Unpack(_body[0]); + Part newHead = new Part( + (byte)(_direction switch + { + Direction.Left => oldHead.X == 0 ? FrameBuffer.Width - 1 : oldHead.X - 1, + Direction.Right => (oldHead.X + 1) % FrameBuffer.Width, + _ => oldHead.X, + }), + (byte)(_direction switch + { + Direction.Up => oldHead.Y == 0 ? FrameBuffer.Height - 1 : oldHead.Y - 1, + Direction.Down => (oldHead.Y + 1) % FrameBuffer.Height, + _ => oldHead.Y, + }), + DirectionToChar(_direction, _direction) + ); + + oldHead = new Part(oldHead.X, oldHead.Y, DirectionToChar(_oldDirection, _direction)); + + bool result = true; + + for (int i = 0; i < _length - 1; i++) + { + Part current = Part.Unpack(_body[i]); + if (current.X == newHead.X && current.Y == newHead.Y) + result = false; + } + + _body[0] = oldHead.Pack(); + + for (int i = _length - 2; i >= 0; i--) + { + _body[i + 1] = _body[i]; + } + + _body[0] = newHead.Pack(); + + _oldDirection = _direction; + + return result; + } + + public unsafe readonly void Draw(ref FrameBuffer fb) + { + for (int i = 0; i < _length; i++) + { + Part p = Part.Unpack(_body[i]); + fb.SetPixel(p.X, p.Y, p.Character); + } + } + + public bool Extend() + { + if (_length < MaxLength) + { + _length += 1; + return true; + } + return false; + } + + public unsafe readonly bool HitTest(int x, int y) + { + for (int i = 0; i < _length; i++) + { + Part current = Part.Unpack(_body[i]); + if (current.X == x && current.Y == y) + return true; + } + + return false; + } + + private static char DirectionToChar(Direction oldDirection, Direction newDirection) + { + const string DirectionChangeToChar = "│┌?┐┘─┐??└│┘└?┌─"; + return DirectionChangeToChar[(int)oldDirection * 4 + (int)newDirection]; + } + + // Helper struct to pack and unpack the packed integer in _body. + readonly struct Part + { + public readonly byte X, Y; + public readonly char Character; + + public Part(byte x, byte y, char c) + { + X = x; + Y = y; + Character = c; + } + + public int Pack() => X << 24 | Y << 16 | Character; + public static Part Unpack(int packed) => new Part((byte)(packed >> 24), (byte)(packed >> 16), (char)packed); + } + + public enum Direction + { + Up, Right, Down, Left + } +} diff --git a/MiniBCL.cs b/MiniBCL.cs new file mode 100644 index 0000000..f4bf895 --- /dev/null +++ b/MiniBCL.cs @@ -0,0 +1,106 @@ +namespace System +{ + public class Object + { + // The layout of object is a contract with the compiler. + public IntPtr m_pEEType; + } + public struct Void { } + + // The layout of primitive types is special cased because it would be recursive. + // These really don't need any fields to work. + public struct Boolean { } + public struct Char { } + public struct SByte { } + public struct Byte { } + public struct Int16 { } + public struct UInt16 { } + public struct Int32 { } + public struct UInt32 { } + public struct Int64 { } + public struct UInt64 { } + public struct IntPtr { } + public struct UIntPtr { } + public struct Single { } + public struct Double { } + + public abstract class ValueType { } + public abstract class Enum : ValueType { } + + public struct Nullable where T : struct { } + + public sealed class String + { + // The layout of the string type is a contract with the compiler. + public readonly int Length; + public char _firstChar; + + public unsafe char this[int index] + { + [System.Runtime.CompilerServices.Intrinsic] + get + { + return Internal.Runtime.CompilerServices.Unsafe.Add(ref _firstChar, index); + } + } + } + public abstract class Array { } + public abstract class Delegate { } + public abstract class MulticastDelegate : Delegate { } + + public struct RuntimeTypeHandle { } + public struct RuntimeMethodHandle { } + public struct RuntimeFieldHandle { } + + public class Attribute { } +} + +namespace System.Runtime.CompilerServices +{ + internal sealed class IntrinsicAttribute : Attribute { } + + public class RuntimeHelpers + { + public static unsafe int OffsetToStringData => sizeof(IntPtr) + sizeof(int); + } +} + +namespace System.Runtime.InteropServices +{ + public enum CharSet + { + None = 1, + Ansi = 2, + Unicode = 3, + Auto = 4, + } + + public sealed class DllImportAttribute : Attribute + { + public string EntryPoint; + public CharSet CharSet; + public DllImportAttribute(string dllName) { } + } + + public enum LayoutKind + { + Sequential = 0, + Explicit = 2, + Auto = 3, + } + + public sealed class StructLayoutAttribute : Attribute + { + public StructLayoutAttribute(LayoutKind layoutKind) { } + } +} +namespace Internal.Runtime.CompilerServices +{ + public static unsafe partial class Unsafe + { + // The body of this method is generated by the compiler. + // It will do what Unsafe.Add is expected to do. It's just not possible to express it in C#. + [System.Runtime.CompilerServices.Intrinsic] + public static extern ref T Add(ref T source, int elementOffset); + } +} diff --git a/MiniRuntime.cs b/MiniRuntime.cs new file mode 100644 index 0000000..cb013fc --- /dev/null +++ b/MiniRuntime.cs @@ -0,0 +1,89 @@ +namespace Internal.Runtime.CompilerHelpers +{ + // A class that the compiler looks for that has helpers to initialize the + // process. The compiler can gracefully handle the helpers not being present, + // but the class itself being absent is unhandled. Let's add an empty class. + class StartupCodeHelpers + { + [System.Runtime.RuntimeExport("RhpReversePInvoke2")] + static void RhpReversePInvoke2(System.IntPtr frame) { } + [System.Runtime.RuntimeExport("RhpReversePInvokeReturn2")] + static void RhpReversePInvokeReturn2(System.IntPtr frame) { } + [System.Runtime.RuntimeExport("RhpPInvoke")] + static void RhpPinvoke(System.IntPtr frame) { } + [System.Runtime.RuntimeExport("RhpPInvokeReturn")] + static void RhpPinvokeReturn(System.IntPtr frame) { } + } +} + +namespace System +{ + class Array : Array { } +} + +namespace System.Runtime +{ + // Custom attribute that the compiler understands that instructs it + // to export the method under the given symbolic name. + internal sealed class RuntimeExportAttribute : Attribute + { + public RuntimeExportAttribute(string entry) { } + } +} + +namespace System.Runtime.InteropServices +{ + // Custom attribute that marks a class as having special "Call" intrinsics. + internal class McgIntrinsicsAttribute : Attribute { } +} + +namespace System.Runtime.CompilerServices +{ + // A class responsible for running static constructors. The compiler will call into this + // code to ensure static constructors run and that they only run once. + [System.Runtime.InteropServices.McgIntrinsics] + internal static class ClassConstructorRunner + { + private static unsafe IntPtr CheckStaticClassConstructionReturnNonGCStaticBase(ref StaticClassConstructionContext context, IntPtr nonGcStaticBase) + { + CheckStaticClassConstruction(ref context); + return nonGcStaticBase; + } + + private static unsafe void CheckStaticClassConstruction(ref StaticClassConstructionContext context) + { + // Very simplified class constructor runner. In real world, the class constructor runner + // would need to be able to deal with potentially multiple threads racing to initialize + // a single class, and would need to be able to deal with potential deadlocks + // between class constructors. + + if (context.initialized == 1) + return; + + context.initialized = 1; + + // Run the class constructor. + Call(context.cctorMethodAddress); + } + + // This is a special compiler intrinsic that calls method pointed to by pfn. + [System.Runtime.CompilerServices.Intrinsic] + private static extern T Call(System.IntPtr pfn); + } + + // This data structure is a contract with the compiler. It holds the address of a static + // constructor and a flag that specifies whether the constructor already executed. + [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + public struct StaticClassConstructionContext + { + // Pointer to the code for the static class constructor method. This is initialized by the + // binder/runtime. + public IntPtr cctorMethodAddress; + + // Initialization state of the class. This is initialized to 0. Every time managed code checks the + // cctor state the runtime will call the classlibrary's CheckStaticClassConstruction with this context + // structure unless initialized == 1. This check is specific to allow the classlibrary to store more + // than a binary state for each cctor if it so desires. + public int initialized; + } +} diff --git a/Pal/Console.Windows.cs b/Pal/Console.Windows.cs new file mode 100644 index 0000000..30a23a0 --- /dev/null +++ b/Pal/Console.Windows.cs @@ -0,0 +1,200 @@ +using System.Runtime.InteropServices; + +namespace System +{ + public enum ConsoleColor + { + Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, + Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White + } + + public enum ConsoleKey + { + Escape = 27, + LeftArrow = 37, + UpArrow = 38, + RightArrow = 39, + DownArrow = 40, + } + + public readonly struct ConsoleKeyInfo + { + public ConsoleKeyInfo(char keyChar, ConsoleKey key, bool shift, bool alt, bool control) + { + Key = key; + } + + public readonly ConsoleKey Key; + } + + static class Console + { + private enum BOOL : int + { + FALSE = 0, + TRUE = 1, + } + + [DllImport("api-ms-win-core-processenvironment-l1-1-0")] + private static unsafe extern IntPtr GetStdHandle(int c); + + private readonly static IntPtr s_outputHandle = GetStdHandle(-11); + + private readonly static IntPtr s_inputHandle = GetStdHandle(-10); + + [DllImport("api-ms-win-core-console-l2-1-0.dll", EntryPoint = "SetConsoleTitleW")] + private static unsafe extern BOOL SetConsoleTitle(char* c); + + public static unsafe string Title + { + set + { + fixed (char* c = value) + SetConsoleTitle(c); + } + } + + [StructLayout(LayoutKind.Sequential)] + struct CONSOLE_CURSOR_INFO + { + public uint Size; + public BOOL Visible; + } + + [DllImport("api-ms-win-core-console-l2-1-0")] + private static unsafe extern BOOL SetConsoleCursorInfo(IntPtr handle, CONSOLE_CURSOR_INFO* cursorInfo); + + public static unsafe bool CursorVisible + { + set + { + CONSOLE_CURSOR_INFO cursorInfo = new CONSOLE_CURSOR_INFO + { + Size = 1, + Visible = value ? BOOL.TRUE : BOOL.FALSE + }; + SetConsoleCursorInfo(s_outputHandle, &cursorInfo); + } + } + + [DllImport("api-ms-win-core-console-l2-1-0")] + private static unsafe extern BOOL SetConsoleTextAttribute(IntPtr handle, ushort attribute); + + public static ConsoleColor ForegroundColor + { + set + { + SetConsoleTextAttribute(s_outputHandle, (ushort)value); + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct KEY_EVENT_RECORD + { + public BOOL KeyDown; + public short RepeatCount; + public short VirtualKeyCode; + public short VirtualScanCode; + public short UChar; + public int ControlKeyState; + } + + [StructLayout(LayoutKind.Sequential)] + private struct INPUT_RECORD + { + public short EventType; + public KEY_EVENT_RECORD KeyEvent; + } + + [DllImport("api-ms-win-core-console-l1-2-0", EntryPoint = "PeekConsoleInputW", CharSet = CharSet.Unicode)] + private static unsafe extern BOOL PeekConsoleInput(IntPtr hConsoleInput, INPUT_RECORD* lpBuffer, uint nLength, uint* lpNumberOfEventsRead); + + public static unsafe bool KeyAvailable + { + get + { + uint nRead; + INPUT_RECORD buffer; + while (true) + { + PeekConsoleInput(s_inputHandle, &buffer, 1, &nRead); + + if (nRead == 0) + return false; + + if (buffer.EventType == 1 && buffer.KeyEvent.KeyDown != BOOL.FALSE) + return true; + + ReadConsoleInput(s_inputHandle, &buffer, 1, &nRead); + } + } + } + + [DllImport("api-ms-win-core-console-l1-2-0", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)] + private static unsafe extern BOOL ReadConsoleInput(IntPtr hConsoleInput, INPUT_RECORD* lpBuffer, uint nLength, uint* lpNumberOfEventsRead); + + public static unsafe ConsoleKeyInfo ReadKey(bool intercept) + { + uint nRead; + INPUT_RECORD buffer; + do + { + ReadConsoleInput(s_inputHandle, &buffer, 1, &nRead); + } + while (buffer.EventType != 1 || buffer.KeyEvent.KeyDown == BOOL.FALSE); + + return new ConsoleKeyInfo((char)buffer.KeyEvent.UChar, (ConsoleKey)buffer.KeyEvent.VirtualKeyCode, false, false, false); + } + + struct SMALL_RECT + { + public short Left, Top, Right, Bottom; + } + + [DllImport("api-ms-win-core-console-l2-1-0")] + private static unsafe extern BOOL SetConsoleWindowInfo(IntPtr handle, BOOL absolute, SMALL_RECT* consoleWindow); + + public static unsafe void SetWindowSize(int x, int y) + { + SMALL_RECT rect = new SMALL_RECT + { + Left = 0, + Top = 0, + Right = (short)(x - 1), + Bottom = (short)(y - 1), + }; + SetConsoleWindowInfo(s_outputHandle, BOOL.TRUE, &rect); + } + + [StructLayout(LayoutKind.Sequential)] + struct COORD + { + public short X, Y; + } + + [DllImport("api-ms-win-core-console-l2-1-0")] + private static unsafe extern BOOL SetConsoleScreenBufferSize(IntPtr handle, COORD size); + + public static void SetBufferSize(int x, int y) + { + SetConsoleScreenBufferSize(s_outputHandle, new COORD { X = (short)x, Y = (short)y }); + } + + [DllImport("api-ms-win-core-console-l2-1-0")] + private static unsafe extern BOOL SetConsoleCursorPosition(IntPtr handle, COORD position); + + public static void SetCursorPosition(int x, int y) + { + SetConsoleCursorPosition(s_outputHandle, new COORD { X = (short)x, Y = (short)y }); + } + + [DllImport("api-ms-win-core-console-l1-2-0", EntryPoint = "WriteConsoleW")] + private static unsafe extern BOOL WriteConsole(IntPtr handle, void* buffer, int numChars, int* charsWritten, void* reserved); + + public static unsafe void Write(char c) + { + int dummy; + WriteConsole(s_outputHandle, &c, 1, &dummy, null); + } + } +} diff --git a/Pal/Environment.Windows.cs b/Pal/Environment.Windows.cs new file mode 100644 index 0000000..318a238 --- /dev/null +++ b/Pal/Environment.Windows.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace System +{ + static class Environment + { + [DllImport("api-ms-win-core-sysinfo-l1-1-0")] + private static extern long GetTickCount64(); + + public static long TickCount64 => GetTickCount64(); + } +} diff --git a/Pal/Thread.Windows.cs b/Pal/Thread.Windows.cs new file mode 100644 index 0000000..be7072a --- /dev/null +++ b/Pal/Thread.Windows.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace System.Threading +{ + static class Thread + { + [DllImport("api-ms-win-core-synch-l1-2-0")] + public static extern void Sleep(int delayMs); + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c71699f --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# A self-contained C# game in 8 kB + +This repo is a complement to my article on building an 8 kB self-contained game in C#. By self-contained I mean _this 8 kB C# game binary doesn't need a .NET runtime to work_. See the article on how that's done. + +The project files and scripts in this repo build the same game (Snake clone) in several different configurations, each with a different size of the output. + +![Snake game](SeeSharpSnake.gif) + +## Building + +### To build the 65 MB version of the game + +``` +dotnet publish -r win-x64 -c Release +``` + +### To build the 25 MB version of the game + +``` +dotnet publish -r win-x64 -c Release /p:PublishTrimmed=true +``` + +### ⚠️ WARNING: additional requirements needed for the below configuration + +Make sure you have Visual Studio 2019 installed (Community edition is free) and include C/C++ development tools with Windows SDK (we need a tiny fraction of that - the platform linker and Win32 import libraries). + +### To build the 4.7 MB version of the game + +``` +dotnet publish -r win-x64 -c Release /p:Mode=CoreRT +``` + +### To build the 4.3 MB version of the game + +``` +dotnet publish -r win-x64 -c Release /p:Mode=CoreRT-Moderate +``` + +### To build the 3.0 MB version of the game + +``` +dotnet publish -r win-x64 -c Release /p:Mode=CoreRT-High +``` + +### To build the 1.2 MB version of the game + +``` +dotnet publish -r win-x64 -c Release /p:Mode=CoreRT-ReflectionFree +``` + +### To build the 8 kB version of the game + +1. Open "x64 Native Tools Command Prompt for VS 2019" (it's in your Start menu) +2. CD into the repo root directory + +``` +csc.exe /debug /O /noconfig /nostdlib /runtimemetadataversion:v4.0.30319 MiniRuntime.cs MiniBCL.cs Game\FrameBuffer.cs Game\Random.cs Game\Game.cs Game\Snake.cs Pal\Thread.Windows.cs Pal\Environment.Windows.cs Pal\Console.Windows.cs /out:zerosnake.ilexe /langversion:latest /unsafe +``` + +Find ilc.exe (the [CoreRT](http://github.com/dotnet/corert) ahead of time compiler) on your machine. If you completed any of the above steps that produce outputs <= 4.7 MB, ilc.exe will be in your NuGet package cache (somewhere like `%USERPROFILE%\.nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler\1.0.0-alpha-27402–01\tools`). + +``` +[PATH_TO_ILC_EXE]\ilc.exe zerosnake.ilexe -o zerosnake.obj --systemmodule zerosnake --Os -g +``` + +``` +link.exe /debug:full /subsystem:console zerosnake.obj /entry:__managed__Main kernel32.lib ucrt.lib /merge:.modules=.rdata /merge:.pdata=.rdata /incremental:no /DYNAMICBASE:NO /filealign:16 /align:16 +``` + +## Contributing +Contributions are welcome, but I would like to keep the game simple and small. If you would like to add features like levels or achievements, you might want to just fork this repo. + +In general, I welcome: + +* Making the 8 kB version of the game run on Linux and macOS (the bigger versions of the game should work just fine on Unixes, but the tiny version that p/invokes into the platform APIs is OS specific) +* Adding a configuration that builds the game as an [EFI boot application](https://github.com/MichalStrehovsky/zerosharp/tree/master/efi-no-runtime) so that it can run without an OS +* Bug fixes +* Making the CSPROJ also handle the 8 kB case so that we don't need to mess with the command prompt +* Small experience improvements (e.g. handling ESC key to exit the game) diff --git a/SeeSharpSnake.csproj b/SeeSharpSnake.csproj new file mode 100644 index 0000000..5738c29 --- /dev/null +++ b/SeeSharpSnake.csproj @@ -0,0 +1,49 @@ + + + + Exe + netcoreapp3.0 + true + true + false + + + + + + + + + + + + + + + + + + + + + false + Size + + + + false + true + true + + + + + + + + + true + + + + diff --git a/SeeSharpSnake.gif b/SeeSharpSnake.gif new file mode 100644 index 0000000000000000000000000000000000000000..5d41386c4aa92d8871611554a458e3e8d903490f GIT binary patch literal 14329 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 literal 0 HcmV?d00001 diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..59f9c3a --- /dev/null +++ b/nuget.config @@ -0,0 +1,9 @@ + + + + + + + + +