Skip to content

Commit

Permalink
Add zerolib (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
MichalStrehovsky committed Nov 18, 2022
1 parent d1417fa commit 262f4bd
Show file tree
Hide file tree
Showing 41 changed files with 2,063 additions and 28 deletions.
45 changes: 45 additions & 0 deletions samples/Snake/FrameBuffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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()
{
const ConsoleColor snakeColor = ConsoleColor.Green;

Console.ForegroundColor = snakeColor;

for (int i = 0; i < Area; i++)
{
if (i % Width == 0)
Console.SetCursorPosition(0, i / Width);

char c = _chars[i];

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);
}
}
}
119 changes: 119 additions & 0 deletions samples/Snake/Game.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System;
using System.Runtime.InteropServices;
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()
{
#if WINDOWS
Console.SetWindowSize(FrameBuffer.Width, FrameBuffer.Height + 1);
Console.SetBufferSize(FrameBuffer.Width, FrameBuffer.Height + 1);
Console.Title = "See Sharp Snake";
Console.CursorVisible = false;
#endif

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);
}
}
}
19 changes: 19 additions & 0 deletions samples/Snake/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# ZeroLib sample

Basides the .NET standard library, bflat also comes with a minimal standard library that is an very minimal subset of what's in .NET.

There is no garbage collector. No exception handling. No useful collection types. Just a couple things to run at least _some_ code.

Think of it more as an art project than something useful. Building something that fits the supported envelope is more an artistic expression than anything useful.

This directory contains a sample app (a snake game) that can be compiled both in the standard way (with a useful standard library) or with zerolib.

To build the sample with zerolib, run:

```console
$ bflat build --stdlib:zero
```

Most other `build` arguments still apply, so you can make things smaller with e.g. `--separate-symbols --no-pie`, or you can crosscompile with `--os:windows` and `--os:linux`.

You should see a fully selfcontained executable that is 10-50 kB in size, depending on the platform. That's the "art" part.
11 changes: 11 additions & 0 deletions samples/Snake/Random.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
struct Random
{
private uint _val;

public Random(uint seed)
{
_val = seed;
}

public uint Next() => _val = (1103515245 * _val + 12345) % 2147483648;
}
137 changes: 137 additions & 0 deletions samples/Snake/Snake.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 262f4bd

Please sign in to comment.