// Breakout — the brick wall spells DRAWN, broken bricks drop powerups
const int cols = 34, rows = 20;
const double brickW = 10, brickH = 8;
const double wallTop = 26;
const double fieldW = cols * brickW;
const double fieldH = 440;
const double basePaddleW = 64, paddleH = 8, paddleY = 400;
const double ballR = 4;
const double baseSpeed = 260;
const double dropSpeed = 130;
const double dropChance = 0.66; // powerup drop rate per destroyed brick, 0..1
const int maxBalls = 24;
// 5x7 pixel font: the wall letters plus the powerup captions
var glyphs = new Dictionary<char, string[]>();
glyphs['D'] = new[] { "1111.", "1...1", "1...1", "1...1", "1...1", "1...1", "1111." };
glyphs['R'] = new[] { "1111.", "1...1", "1...1", "1111.", "1..1.", "1...1", "1...1" };
glyphs['A'] = new[] { ".111.", "1...1", "1...1", "11111", "1...1", "1...1", "1...1" };
glyphs['W'] = new[] { "1...1", "1...1", "1...1", "1.1.1", "1.1.1", "11111", "1...1" };
glyphs['N'] = new[] { "1...1", "11..1", "11..1", "1.1.1", "1..11", "1..11", "1...1" };
glyphs['3'] = new[] { "1111.", "....1", "....1", ".111.", "....1", "....1", "1111." };
glyphs['x'] = new[] { ".....", ".....", "1...1", ".1.1.", "..1..", ".1.1.", "1...1" };
glyphs['+'] = new[] { ".....", "..1..", "..1..", "11111", "..1..", "..1..", "....." };
var word = "DRAWN";
var strength = new int[cols, rows];
var balls = new List<double[]>(); // x, y, vx, vy
var drops = new List<double[]>(); // x, y, kind: 0 = x3, 1 = +3, 2 = W
var alive = 0;
var score = 0;
var lives = 3;
var over = false;
var won = false;
var wideLeft = 0.0; // seconds of wide paddle left
double paddleW = basePaddleW;
double paddleX = fieldW / 2, paddleTo = fieldW / 2;
var keyDir = 0; // -1 / +1 while an arrow key is held
var rnd = new Random();
SkiaLabel hud = null;
SkiaLabel status = null;
// angle 0 = straight up, positive = to the right
void AddBall(double x, double y, double angle)
{
if (balls.Count >= maxBalls) return;
balls.Add(new[] { x, y, Math.Sin(angle) * baseSpeed, -Math.Cos(angle) * baseSpeed });
}
void BuildWall()
{
for (int c = 0; c < cols; c++)
for (int r = 0; r < rows; r++)
strength[c, r] = 1;
var wordCols = word.Length * 6 - 1;
var c0 = (cols - wordCols) / 2;
var r0 = 9;
for (int i = 0; i < word.Length; i++)
{
var g = glyphs[word[i]];
for (int gr = 0; gr < 7; gr++)
for (int gc = 0; gc < 5; gc++)
if (g[gr][gc] == '1')
strength[c0 + i * 6 + gc, r0 + gr] = 2; // letters take two hits
}
alive = cols * rows;
}
void Restart()
{
BuildWall();
balls.Clear();
drops.Clear();
AddBall(fieldW / 2, paddleY - 40, (rnd.NextDouble() - 0.5) * 1.2);
score = 0;
lives = 3;
over = false;
won = false;
wideLeft = 0;
paddleW = basePaddleW;
paddleX = fieldW / 2;
paddleTo = paddleX;
keyDir = 0;
}
void Apply(int kind)
{
if (kind == 0) // x3 — every ball splits into three
{
foreach (var b in balls.ToList())
{
var a = Math.Atan2(b[2], -b[3]);
AddBall(b[0], b[1], a - 0.4);
AddBall(b[0], b[1], a + 0.4);
}
}
else if (kind == 1) // +3 — three fresh balls off the paddle
{
for (int i = -1; i <= 1; i++)
AddBall(paddleX, paddleY - ballR - 1, i * 0.4);
}
else // W — wider paddle for 5s
{
wideLeft = 5;
}
}
bool HitCell(double x, double y)
{
var c = (int)Math.Floor(x / brickW);
var r = (int)Math.Floor((y - wallTop) / brickH);
if (c < 0 || c >= cols || r < 0 || r >= rows) return false;
if (strength[c, r] <= 0) return false;
strength[c, r]--;
score += 10;
if (strength[c, r] == 0)
{
alive--;
if (rnd.NextDouble() < dropChance)
drops.Add(new double[] { c * brickW + brickW / 2, wallTop + r * brickH, rnd.Next(3) });
}
return true;
}
void Step(double dt)
{
if (over || won) return;
if (dt > 0.05) dt = 0.05;
// held arrow key drives the target at a constant speed, key repeat is not involved
if (keyDir != 0)
paddleTo += keyDir * 380 * dt;
wideLeft = Math.Max(0, wideLeft - dt);
paddleW = wideLeft > 0 ? basePaddleW * 1.7 : basePaddleW;
paddleTo = Math.Clamp(paddleTo, paddleW / 2, fieldW - paddleW / 2);
// pointer events land ~16/s, the canvas draws 60/s: chase the target so the paddle glides
paddleX += (paddleTo - paddleX) * Math.Min(1.0, dt * 22);
for (int i = drops.Count - 1; i >= 0; i--)
{
var d = drops[i];
d[1] += dropSpeed * dt;
if (d[1] > paddleY - 7 && d[1] < paddleY + paddleH + 7 && Math.Abs(d[0] - paddleX) < paddleW / 2 + 12)
{
Apply((int)d[2]);
drops.RemoveAt(i);
}
else if (d[1] > fieldH)
{
drops.RemoveAt(i);
}
}
for (int i = balls.Count - 1; i >= 0; i--)
{
var b = balls[i];
var dist = Math.Sqrt(b[2] * b[2] + b[3] * b[3]) * dt;
var steps = Math.Max(1, (int)Math.Ceiling(dist / 2.0)); // small steps, no tunneling
var sdt = dt / steps;
var lost = false;
for (int s = 0; s < steps; s++)
{
b[0] += b[2] * sdt;
b[1] += b[3] * sdt;
if (b[0] < ballR) { b[0] = ballR; b[2] = Math.Abs(b[2]); }
if (b[0] > fieldW - ballR) { b[0] = fieldW - ballR; b[2] = -Math.Abs(b[2]); }
if (b[1] < ballR) { b[1] = ballR; b[3] = Math.Abs(b[3]); }
if (b[3] > 0 && b[1] + ballR >= paddleY && b[1] - ballR <= paddleY + paddleH
&& b[0] >= paddleX - paddleW / 2 - ballR && b[0] <= paddleX + paddleW / 2 + ballR)
{
b[1] = paddleY - ballR;
var ang = Math.Clamp((b[0] - paddleX) / (paddleW / 2), -1.0, 1.0); // edge = steeper
var sp = Math.Sqrt(b[2] * b[2] + b[3] * b[3]);
b[2] = Math.Sin(ang) * sp;
b[3] = -Math.Cos(ang) * sp;
}
if (HitCell(b[0] + Math.Sign(b[2]) * ballR, b[1])) b[2] = -b[2];
else if (HitCell(b[0], b[1] + Math.Sign(b[3]) * ballR)) b[3] = -b[3];
if (alive <= 0) { won = true; return; }
if (b[1] - ballR > fieldH) { lost = true; break; }
}
if (lost) balls.RemoveAt(i);
}
if (balls.Count == 0)
{
lives--;
if (lives <= 0) over = true;
else AddBall(fieldW / 2, paddleY - 40, (rnd.NextDouble() - 0.5) * 1.2);
}
}
Restart();
return new SkiaStack()
{
Spacing = 10,
BackgroundColor = Color.Parse("#12161F"),
Padding = new Thickness(16),
Children = new List<SkiaControl>()
{
new SkiaLayer()
{
Children = new List<SkiaControl>()
{
new SkiaStack()
{
Spacing = 2,
Children = new List<SkiaControl>()
{
new SkiaLabel("Breakout")
{
FontSize = 17,
FontWeight = 700,
TextColor = Color.Parse("#E8EDF6"),
},
new SkiaLabel("drag or arrow keys, catch the drops")
{
FontSize = 11,
TextColor = Color.Parse("#6B7789"),
},
}
}.StartX().CenterY(),
new SkiaLabel("Score 0 Lives 3")
{
FontSize = 12,
TextColor = Color.Parse("#FFD43B"),
}
.Assign(out hud)
.EndX().CenterY(),
}
}.FillX(),
// the frame paints the board background, the child layer paints the game on top
new SkiaShape()
{
CornerRadius = 10,
BackgroundColor = Color.Parse("#0B0E14"),
WidthRequest = fieldW,
HeightRequest = fieldH,
Children = new List<SkiaControl>()
{
new SkiaLayer()
{
Children = new List<SkiaControl>()
{
new SkiaLabel("")
{
FontSize = 14,
FontWeight = 700,
TextColor = Color.Parse("#E8EDF6"),
IsVisible = false,
}
.Assign(out status)
.Center(),
}
}
.Fill()
.WhenPaint((me, ctx) =>
{
var canvas = ctx.Context.Canvas;
var scale = ctx.Scale;
var dest = ctx.Destination;
float X(double v) => (float)(dest.Left + v * scale);
float Y(double v) => (float)(dest.Top + v * scale);
float S(double v) => (float)(v * scale);
using var paint = new SKPaint { IsAntialias = true, Style = SKPaintStyle.Fill };
void Glyph(char ch, float gx, float gy, float ps)
{
var g = glyphs[ch];
for (int r = 0; r < 7; r++)
for (int c = 0; c < 5; c++)
if (g[r][c] == '1') canvas.DrawRect(gx + c * ps, gy + r * ps, ps, ps, paint);
}
var yellow = SKColor.Parse("#FFD43B");
var steel = SKColor.Parse("#4A5568");
for (int c = 0; c < cols; c++)
{
for (int r = 0; r < rows; r++)
{
var st = strength[c, r];
if (st <= 0) continue;
paint.Color = st > 1 ? steel : yellow;
canvas.DrawRect(new SKRect(
X(c * brickW + 1), Y(wallTop + r * brickH + 1),
X(c * brickW + brickW - 1), Y(wallTop + r * brickH + brickH - 1)), paint);
}
}
foreach (var d in drops)
{
var kind = (int)d[2];
paint.Color = kind == 0 ? SKColor.Parse("#5EE7C4")
: kind == 1 ? SKColor.Parse("#2E7BF6") : yellow;
canvas.DrawRoundRect(new SKRect(
X(d[0] - 13), Y(d[1] - 8), X(d[0] + 13), Y(d[1] + 8)), S(4), S(4), paint);
var label = kind == 0 ? "x3" : kind == 1 ? "+3" : "W";
var ps = S(1.5);
paint.Color = SKColor.Parse("#0B0E14");
var gx = X(d[0]) - (label.Length * 6 * ps - ps) / 2;
for (int i = 0; i < label.Length; i++) Glyph(label[i], gx + i * 6 * ps, Y(d[1]) - 3.5f * ps, ps);
}
paint.Color = SKColor.Parse("#FF6B4A");
canvas.DrawRoundRect(new SKRect(
X(paddleX - paddleW / 2), Y(paddleY),
X(paddleX + paddleW / 2), Y(paddleY + paddleH)), S(4), S(4), paint);
paint.Color = SKColors.White;
foreach (var b in balls) canvas.DrawCircle(X(b[0]), Y(b[1]), S(ballR), paint);
})
.WithGestures((me, args, apply) =>
{
if (args.Type == TouchActionResult.Down || args.Type == TouchActionResult.Panning)
{
if (over || won)
{
Restart();
return me;
}
var local = (args.Event.Location.X - me.DrawingRect.Left) / me.RenderingScale;
paddleTo = Math.Clamp(local, paddleW / 2, fieldW - paddleW / 2);
return me;
}
return null;
})
.Animate(1.0, (me, animator, value, dt) =>
{
Step(dt);
var text = "Score " + score + " Lives " + lives;
if (hud != null && hud.Text != text) hud.Text = text;
if (status != null)
{
var msg = won ? "YOU WIN — tap or press Space" : (over ? "GAME OVER — tap or press Space" : "");
if (status.Text != msg)
{
status.Text = msg;
status.IsVisible = msg.Length > 0;
}
}
me.Update();
}, repeat: -1),
}
}
.CenterX(),
}
}
.OnKeyDown((me, key) =>
{
if (over || won)
{
if (key == InputKey.Space || key == InputKey.Enter) Restart();
return;
}
if (key == InputKey.ArrowLeft) keyDir = -1;
else if (key == InputKey.ArrowRight) keyDir = 1;
})
.OnKeyUp((me, key) =>
{
if (key == InputKey.ArrowLeft && keyDir < 0) keyDir = 0;
else if (key == InputKey.ArrowRight && keyDir > 0) keyDir = 0;
});