DrawFiddle▶ RunEdit in Fiddle
// RODENT'S REVENGE — push blocks, box the cats in, eat the cheese they leave behind.
// Click the board first (that hands the keyboard to the canvas), then arrows or WASD.

// ---- sprites -----------------------------------------------------------------------------
const string MouseSvg =
    "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>" +
    "<path d='M5 25c-3 1-4 4-1 6' fill='none' stroke='#6A7078' stroke-width='2' stroke-linecap='round'/>" +
    "<path d='M8 28c-4 0-6-3-6-7 0-6 6-10 13-10 8 0 14 4 14 9 0 5-5 8-11 8z' fill='#B9BEC6' stroke='#5A6069' stroke-width='1.6'/>" +
    "<circle cx='10' cy='13' r='5' fill='#D6DAE0' stroke='#5A6069' stroke-width='1.5'/>" +
    "<circle cx='24' cy='19' r='1.7' fill='#23262B'/>" +
    "<circle cx='29' cy='22' r='2' fill='#F09BB0' stroke='#C5788C' stroke-width='0.8'/>" +
    "<path d='M22 24h8M22 26l7 2' fill='none' stroke='#8E959D' stroke-width='0.9'/></svg>";

const string CatSvg =
    "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>" +
    "<path d='M6 13 7 4l7 5zM26 13l-1-9-7 5z' fill='#E39A4A' stroke='#7A4A17' stroke-width='1.4' stroke-linejoin='round'/>" +
    "<path d='M16 8c7 0 12 5 12 11 0 6-5 9-12 9S4 25 4 19c0-6 5-11 12-11z' fill='#E8A759' stroke='#7A4A17' stroke-width='1.5'/>" +
    "<path d='M11 12l1 5M16 11v5M21 12l-1 5' fill='none' stroke='#B8752C' stroke-width='1.4' stroke-linecap='round'/>" +
    "<circle cx='12' cy='20' r='2.6' fill='#F7F3E8'/><circle cx='20' cy='20' r='2.6' fill='#F7F3E8'/>" +
    "<circle cx='12.4' cy='20' r='1.3' fill='#22201C'/><circle cx='19.6' cy='20' r='1.3' fill='#22201C'/>" +
    "<path d='M16 23.5l-1.6 1.6h3.2z' fill='#C0603F'/>" +
    "<path d='M3 22h6M3 25h6M23 22h6M23 25h6' fill='none' stroke='#F2E3CE' stroke-width='0.9'/></svg>";

const string CheeseSvg =
    "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>" +
    "<path d='M3 25 20 6l9 6v13z' fill='#F5C542' stroke='#A97C14' stroke-width='1.6' stroke-linejoin='round'/>" +
    "<circle cx='11' cy='21' r='2.6' fill='#D9A62B'/><circle cx='22' cy='17' r='2.1' fill='#D9A62B'/>" +
    "<circle cx='19' cy='22.5' r='1.7' fill='#D9A62B'/></svg>";

// ---- board -------------------------------------------------------------------------------
// The board is derived, not typed in: mouse in the centre cell, SlabReach blocks around it, a
// Lane-wide corridor for the cats, then the wall ring. That keeps it square, keeps the mouse
// centred, and makes both margins impossible to get wrong.
const int SlabReach = 3;   // blocks between the mouse and the corridor, in every direction
const int Lane = 2;        // corridor the cats prowl, on all four sides
const int Cols = (SlabReach + Lane + 1) * 2 + 1, Rows = Cols, MaxCats = 6;
const double Cell = 28;
const int CellEmpty = 0, CellBlock = 1, CellWall = 2, CellCheese = 3;

var grid = new int[Cols * Rows];
var shown = new int[Cols * Rows];
var cats = new List<(int x, int y, int lx, int ly)>();   // lx/ly = the step it just took
var spawns = new (int x, int y)[]
{
    (1, 1), (Cols - 2, Rows - 2), (Cols - 2, 1), (1, Rows - 2), (Cols / 2, 1), (Cols / 2, Rows - 2),
};

var mouseX = Cols / 2;
var mouseY = Rows / 2;
var lives = 3;
var score = 0;
var level = 1;
var gameOver = false;
var helpOpen = false;   // the cats hold still while the rules are up
var stunned = false;    // the beat after a cat lands on you, before the board resets
double dragX = 0, dragY = 0;   // anchor of the current drag, in board points
var dragStepped = false;

var cellViews = new SkiaShape[Cols * Rows];
var catViews = new SkiaSvg[MaxCats];
var cheeseViews = new SkiaSvg[MaxCats];
SkiaSvg mouseView = null;
SkiaLabel headline = null;
SkiaLabel note = null;
SkiaLayer help = null;
SkiaLayer caught = null;
SkiaLabel caughtText = null;

var blockColor = Color.Parse("#B07C43");
var wallColor = Color.Parse("#5C3E22");

int At(int x, int y) => grid[y * Cols + x];
void Put(int x, int y, int what) => grid[y * Cols + x] = what;
bool Inside(int x, int y) => x >= 0 && x < Cols && y >= 0 && y < Rows;
bool CatOn(int x, int y) => cats.Any(c => c.x == x && c.y == y);
// a cat may step only onto bare floor — cheese and blocks are what boxes it in
bool Free(int x, int y) => Inside(x, y) && At(x, y) == CellEmpty && !CatOn(x, y);

// Cats walk on eight directions like the original, so both their steps and the "is it boxed in"
// test use all eight neighbours — and distance is Chebyshev, not Manhattan: a diagonal step
// closes both axes at once and Manhattan would price it the same as a sideways shuffle.
var around = new (int sx, int sy)[]
{
    (1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, 1), (-1, -1),
};
int Reach(int x, int y) => Math.Max(Math.Abs(mouseX - x), Math.Abs(mouseY - y));

void PlaceActors()
{
    mouseX = Cols / 2;
    mouseY = Rows / 2;
    Put(mouseX, mouseY, CellEmpty);

    cats.Clear();
    for (int i = 0; i < Math.Min(1 + level, MaxCats); i++)
    {
        var spot = spawns[i];
        Put(spot.x, spot.y, CellEmpty);   // never spawn a cat inside a block it pushed nowhere
        cats.Add((spot.x, spot.y, 0, 0));
    }
}

void NewLevel()
{
    for (int y = 0; y < Rows; y++)
        for (int x = 0; x < Cols; x++)
            Put(x, y, x == 0 || y == 0 || x == Cols - 1 || y == Rows - 1 ? CellWall : CellEmpty);

    // the classic slab: 3 blocks from the mouse in every direction, built off the centre cell so
    // it stays square and the open lane around it is the same width on all four sides
    for (int y = Rows / 2 - SlabReach; y <= Rows / 2 + SlabReach; y++)
        for (int x = Cols / 2 - SlabReach; x <= Cols / 2 + SlabReach; x++)
            Put(x, y, CellBlock);

    PlaceActors();
}

// Getting caught is the moment of the game — hold it for a beat instead of teleporting the mouse
// back. Everything freezes on the stun flag, the board flashes red, the mouse spins off its feet.
async void Caught()
{
    if (stunned) return;

    stunned = true;
    lives--;

    caughtText.Text = lives <= 0 ? "GAME OVER" : "GOTCHA!";
    caught.IsVisible = true;
    caught.AnimateOpacity(0.25, 1.0, 0.3, repeat: 4, pingPong: true);
    _ = caughtText.ScaleToAsync(1.25, 1.25, 260, Easing.SpringOut);
    _ = mouseView.RotateToAsync(720, 1100);
    Refresh();

    await System.Threading.Tasks.Task.Delay(1600);

    caught.IsVisible = false;
    caught.Opacity = 1;
    caughtText.ScaleX = caughtText.ScaleY = 1;
    mouseView.Rotation = 0;

    if (lives <= 0)
        gameOver = true;
    else
        NewLevel();   // a life costs you the whole board: blocks back in the slab, cheese gone

    stunned = false;
    Refresh();
}

/// A cat with no bare floor left around it turns into cheese. The mouse standing next to it
/// counts as an escape route, otherwise you could trap a cat by walking up to it.
void Boxed()
{
    for (int i = cats.Count - 1; i >= 0; i--)
    {
        var (cx, cy, _, _) = cats[i];
        var escapes = around.Any(d => Free(cx + d.sx, cy + d.sy)) || Reach(cx, cy) == 1;
        if (escapes) continue;

        Put(cx, cy, CellCheese);
        cats.RemoveAt(i);
        score += 200;
    }

    if (cats.Count == 0)
    {
        level++;
        score += 500;
        NewLevel();
    }
}

bool Step(int dx, int dy)
{
    if (gameOver || helpOpen || stunned) return false;

    int nx = mouseX + dx, ny = mouseY + dy;
    if (!Inside(nx, ny) || At(nx, ny) == CellWall) return false;

    if (CatOn(nx, ny))
    {
        Caught();
        return true;
    }

    if (At(nx, ny) == CellBlock)
    {
        // push the whole run of blocks if there is bare floor behind it
        int rx = nx, ry = ny;
        while (Inside(rx, ry) && At(rx, ry) == CellBlock) { rx += dx; ry += dy; }
        if (!Free(rx, ry)) return false;

        Put(rx, ry, CellBlock);
        Put(nx, ny, CellEmpty);
    }
    else if (At(nx, ny) == CellCheese)
    {
        Put(nx, ny, CellEmpty);
        score += 100;
    }

    mouseX = nx;
    mouseY = ny;
    if (dx != 0) mouseView.ScaleX = dx > 0 ? 1 : -1;
    Boxed();
    return true;
}

// Phones have no arrow keys: tap a square and the mouse takes one step towards it, along the
// axis you are furthest off. If that way is blocked it tries the other one, so a tap next to a
// wall still does the obvious thing.
void TapTowards(int cellX, int cellY)
{
    int dx = Math.Sign(cellX - mouseX), dy = Math.Sign(cellY - mouseY);
    if (dx == 0 && dy == 0) return;

    var horizontalFirst = Math.Abs(cellX - mouseX) >= Math.Abs(cellY - mouseY);
    var first = horizontalFirst ? (dx, 0) : (0, dy);
    var second = horizontalFirst ? (0, dy) : (dx, 0);

    if (first != (0, 0) && Step(first.Item1, first.Item2)) { Refresh(); return; }
    if (second != (0, 0) && Step(second.Item1, second.Item2)) Refresh();
}

void CatsStep()
{
    for (int i = 0; i < cats.Count; i++)
    {
        var (cx, cy, lx, ly) = cats[i];
        var best = int.MaxValue;
        (int sx, int sy) pick = (0, 0);

        // Take the open neighbour that gets closest to the mouse. Turning back the way it came
        // costs 3 — without that penalty a cat aligned with the mouse and blocked by the slab
        // just bounces between two squares forever instead of walking around it.
        foreach (var (sx, sy) in around)
        {
            int tx = cx + sx, ty = cy + sy;

            if (tx == mouseX && ty == mouseY)
            {
                Caught();
                return;
            }
            if (!Free(tx, ty)) continue;

            // Chebyshev decides how many steps are left; Manhattan breaks the ties in favour of
            // the diagonal, which closes both axes at once instead of walking an L.
            var cost = Reach(tx, ty) * 2 + Math.Abs(mouseX - tx) + Math.Abs(mouseY - ty)
                       + (sx == -lx && sy == -ly ? 3 : 0);
            if (cost < best)
            {
                best = cost;
                pick = (sx, sy);
            }
        }

        if (pick != (0, 0))
            cats[i] = (cx + pick.sx, cy + pick.sy, pick.sx, pick.sy);
    }
    Boxed();
}

void Refresh()
{
    for (int i = 0; i < grid.Length; i++)
    {
        if (shown[i] == grid[i]) continue;    // only squares that actually changed touch a control
        shown[i] = grid[i];

        var view = cellViews[i];
        view.IsVisible = grid[i] == CellBlock || grid[i] == CellWall;
        if (view.IsVisible)
            view.BackgroundColor = grid[i] == CellWall ? wallColor : blockColor;
    }

    var cheese = 0;
    for (int i = 0; i < grid.Length && cheese < MaxCats; i++)
    {
        if (grid[i] != CellCheese) continue;
        cheeseViews[cheese].Left = i % Cols * Cell + 1;
        cheeseViews[cheese].Top = i / Cols * Cell + 1;
        cheeseViews[cheese].IsVisible = true;
        cheese++;
    }
    for (int i = cheese; i < MaxCats; i++)
        cheeseViews[i].IsVisible = false;

    for (int i = 0; i < MaxCats; i++)
    {
        catViews[i].IsVisible = i < cats.Count;
        if (i >= cats.Count) continue;
        catViews[i].Left = cats[i].x * Cell + 1;
        catViews[i].Top = cats[i].y * Cell + 1;
    }

    mouseView.Left = mouseX * Cell + 1;
    mouseView.Top = mouseY * Cell + 1;
    mouseView.IsVisible = !gameOver;

    headline.Text = $"Level {level}    Score {score}    Lives {lives}";
    note.Text = gameOver
        ? "The cats got you — press New game"
        : cats.Count == 1 ? "1 cat left — box it in" : $"{cats.Count} cats — box them in";
}

// The cats tick on their own clock, so the board only repaints when something moved.
// An animator would repaint every frame instead; this loop dies with the control on recompile.
async void Prowl(SkiaControl root)
{
    while (!root.IsDisposed && !root.IsDisposing)
    {
        await System.Threading.Tasks.Task.Delay(Math.Max(170, 400 - level * 25));
        if (gameOver || helpOpen || stunned) continue;
        CatsStep();
        Refresh();
    }
}

// ---- scene -------------------------------------------------------------------------------
return new SkiaShape
{
    CornerRadius = 14,
    BackgroundColor = Color.Parse("#241A12"),
    Padding = new Thickness(14),
    HorizontalOptions = LayoutOptions.Center,
    VerticalOptions = LayoutOptions.Center,
    Children = new List<SkiaControl>
    {
        new SkiaStack
        {
            Spacing = 7,
            HorizontalOptions = LayoutOptions.Center,
            Children = new List<SkiaControl>
            {
                new SkiaLabel("RODENT'S REVENGE")
                {
                    FontSize = 16,
                    FontWeight = 800,
                    TextColor = Color.Parse("#F5C542"),
                }.CenterX(),

                new SkiaLabel("")
                {
                    FontSize = 12,
                    TextColor = Color.Parse("#CDB894"),
                }.CenterX().Assign(out headline),

                new SkiaLayer
                {
                    WidthRequest = Cols * Cell,
                    HeightRequest = Rows * Cell,
                    BackgroundColor = Color.Parse("#DBC59A"),
                    HorizontalOptions = LayoutOptions.Center,
                    VerticalOptions = LayoutOptions.Start,
                    Children =
                        // one square per cell: blocks and walls just switch it on
                        Enumerable.Range(0, Cols * Rows).Select(i => (SkiaControl)new SkiaShape
                        {
                            CornerRadius = 3,
                            WidthRequest = Cell - 1,
                            HeightRequest = Cell - 1,
                            Left = i % Cols * Cell,
                            Top = i / Cols * Cell,
                            HorizontalOptions = LayoutOptions.Start,
                            VerticalOptions = LayoutOptions.Start,
                            BackgroundColor = blockColor,
                            StrokeWidth = 1,
                            StrokeColor = Color.Parse("#33FFFFFF"),
                            IsVisible = false,
                        }.Assign(out cellViews[i]))
                        // cheese first, then cats, then the mouse on top
                        .Concat(Enumerable.Range(0, MaxCats).Select(i => (SkiaControl)new SkiaSvg
                        {
                            SvgString = CheeseSvg,
                            WidthRequest = Cell - 2,
                            HeightRequest = Cell - 2,
                            HorizontalOptions = LayoutOptions.Start,
                            VerticalOptions = LayoutOptions.Start,
                            IsVisible = false,
                        }.Assign(out cheeseViews[i])))
                        .Concat(Enumerable.Range(0, MaxCats).Select(i => (SkiaControl)new SkiaSvg
                        {
                            SvgString = CatSvg,
                            WidthRequest = Cell - 2,
                            HeightRequest = Cell - 2,
                            HorizontalOptions = LayoutOptions.Start,
                            VerticalOptions = LayoutOptions.Start,
                            IsVisible = false,
                        }.Assign(out catViews[i])))
                        .Append((SkiaControl)new SkiaSvg
                        {
                            SvgString = MouseSvg,
                            WidthRequest = Cell - 2,
                            HeightRequest = Cell - 2,
                            HorizontalOptions = LayoutOptions.Start,
                            VerticalOptions = LayoutOptions.Start,
                        }.Assign(out mouseView))
                        // the "a cat got you" flash, over the board only
                        .Append((SkiaControl)new SkiaLayer
                        {
                            HorizontalOptions = LayoutOptions.Fill,
                            VerticalOptions = LayoutOptions.Fill,
                            BackgroundColor = Color.Parse("#66C7362F"),
                            ZIndex = 15,
                            IsVisible = false,
                            Children = new List<SkiaControl>
                            {
                                new SkiaLabel("GOTCHA!")
                                {
                                    FontSize = 30,
                                    FontWeight = 900,
                                    TextColor = Color.Parse("#FFE9E6"),
                                    DropShadowColor = Color.Parse("#8C1E18"),
                                    DropShadowSize = 2,
                                    DropShadowOffsetX = 0,
                                    DropShadowOffsetY = 2,
                                }.Center().Assign(out caughtText),
                            }
                        }.Assign(out caught))
                        .ToList()
                }
                // Touch: drag to walk (one step per cell of travel, like the match-3 swap drag), or
                // tap a square to step towards it. Keyboard still works for anyone who has one.
                .WithGestures((me, args, apply) =>
                {
                    var inside = me.GetOffsetInsideControlInPoints(args.Event.Location, apply.ChildOffset);

                    if (args.Type == TouchActionResult.Down)
                    {
                        dragX = inside.X;
                        dragY = inside.Y;
                        dragStepped = false;
                        return me;
                    }

                    if (args.Type == TouchActionResult.Panning)
                    {
                        // a fast drag can cross several cells between two events, so walk them off
                        for (int guard = 0; guard < Cols; guard++)
                        {
                            double dx = inside.X - dragX, dy = inside.Y - dragY;
                            if (Math.Abs(dx) < Cell * 0.6 && Math.Abs(dy) < Cell * 0.6)
                                break;

                            var horizontal = Math.Abs(dx) >= Math.Abs(dy);
                            var sx = horizontal ? Math.Sign(dx) : 0;
                            var sy = horizontal ? 0 : Math.Sign(dy);

                            dragX += sx * Cell;   // the anchor follows the finger cell by cell
                            dragY += sy * Cell;
                            dragStepped = true;

                            if (!Step(sx, sy))
                                break;            // ran into a wall: wait for the finger to turn
                            Refresh();
                        }
                        return me;
                    }

                    if (args.Type == TouchActionResult.Up)
                    {
                        if (!dragStepped)
                            TapTowards((int)(inside.X / Cell), (int)(inside.Y / Cell));
                        return me;
                    }

                    return null;
                }),

                new SkiaLabel("")
                {
                    FontSize = 12,
                    TextColor = Color.Parse("#A28D6C"),
                }.CenterX().Assign(out note),

                new SkiaRow
                {
                    Spacing = 8,
                    HorizontalOptions = LayoutOptions.Center,
                    Children = new List<SkiaControl>
                    {
                        new SkiaButton("New game")
                        {
                            WidthRequest = 110,
                            HeightRequest = 32,
                            CornerRadius = 8,
                            FontSize = 13,
                            BackgroundColor = Color.Parse("#3E2C1B"),
                            TextColor = Color.Parse("#F0E2C8"),
                        }.OnTapped(me =>
                        {
                            level = 1;
                            score = 0;
                            lives = 3;
                            gameOver = false;
                            NewLevel();
                            for (int i = 0; i < shown.Length; i++) shown[i] = -1;
                            Refresh();
                        }),

                        new SkiaButton("Help")
                        {
                            WidthRequest = 70,
                            HeightRequest = 32,
                            CornerRadius = 8,
                            FontSize = 13,
                            BackgroundColor = Color.Parse("#3E2C1B"),
                            TextColor = Color.Parse("#F0E2C8"),
                        }.OnTapped(me =>
                        {
                            helpOpen = true;
                            help.IsVisible = true;
                        }),

                        new SkiaLabel("drag or tap the board, or arrows / WASD")
                        {
                            FontSize = 11,
                            TextColor = Color.Parse("#8A7758"),
                            VerticalOptions = LayoutOptions.Center,
                        },
                    }
                },
            }
        },

        // rules card: sits over everything, holds the cats still, closes on any tap
        new SkiaLayer
        {
            HorizontalOptions = LayoutOptions.Fill,
            VerticalOptions = LayoutOptions.Fill,
            BackgroundColor = Color.Parse("#D2231A12"),
            ZIndex = 20,
            IsVisible = false,
            Children = new List<SkiaControl>
            {
                new SkiaShape
                {
                    CornerRadius = 12,
                    WidthRequest = 330,
                    BackgroundColor = Color.Parse("#2E2015"),
                    StrokeWidth = 1,
                    StrokeColor = Color.Parse("#5E452C"),
                    Padding = new Thickness(18, 16),
                    HorizontalOptions = LayoutOptions.Center,
                    VerticalOptions = LayoutOptions.Center,
                    Children = new List<SkiaControl>
                    {
                        new SkiaStack
                        {
                            Spacing = 9,
                            HorizontalOptions = LayoutOptions.Fill,
                            Children = new List<SkiaControl>
                            {
                                new SkiaLabel("HOW TO PLAY")
                                {
                                    FontSize = 14,
                                    FontWeight = 800,
                                    TextColor = Color.Parse("#F5C542"),
                                }.CenterX(),

                                new SkiaLabel(
                                    "You are the mouse. The cats want you.\n\n" +
                                    "• Walk into a block to push it — a whole line slides if there is floor behind it.\n" +
                                    "• Shut a cat in on all four sides and it turns into cheese.\n" +
                                    "• Walk over cheese to eat it. Cheese also blocks cats, so it helps you box the next one.\n" +
                                    "• A cat that reaches you costs a life. You have three.\n" +
                                    "• Box every cat to clear the level; the next one brings more cats and faster paws.")
                                {
                                    FontSize = 12,
                                    LineSpacing = 1.15,
                                    TextColor = Color.Parse("#DCC9A8"),
                                    HorizontalOptions = LayoutOptions.Fill,
                                },

                                new SkiaLabel("cheese 100    cat 200    level 500")
                                {
                                    FontSize = 12,
                                    FontWeight = 700,
                                    TextColor = Color.Parse("#C6A96F"),
                                }.CenterX(),

                                new SkiaLabel("Drag across the board to walk, or tap a square to step towards it — arrows / WASD work too")
                                {
                                    FontSize = 11,
                                    TextColor = Color.Parse("#9C8663"),
                                    HorizontalTextAlignment = DrawTextAlignment.Center,
                                    HorizontalOptions = LayoutOptions.Fill,
                                },

                                new SkiaButton("Got it")
                                {
                                    WidthRequest = 100,
                                    HeightRequest = 32,
                                    CornerRadius = 8,
                                    FontSize = 13,
                                    BackgroundColor = Color.Parse("#5A4128"),
                                    TextColor = Color.Parse("#F0E2C8"),
                                }
                                .CenterX()
                                .OnTapped(me =>
                                {
                                    helpOpen = false;
                                    help.IsVisible = false;
                                }),
                            }
                        }
                    }
                },
            }
        }
        .Assign(out help)
        .OnTapped(me =>
        {
            helpOpen = false;
            me.IsVisible = false;
        }),
    }
}
.OnKeyDown((me, key) =>
{
    if (key == InputKey.ArrowLeft || key == InputKey.KeyA) Step(-1, 0);
    else if (key == InputKey.ArrowRight || key == InputKey.KeyD) Step(1, 0);
    else if (key == InputKey.ArrowUp || key == InputKey.KeyW) Step(0, -1);
    else if (key == InputKey.ArrowDown || key == InputKey.KeyS) Step(0, 1);
    else return;
    Refresh();
})
.Initialize(me =>
{
    for (int i = 0; i < shown.Length; i++) shown[i] = -1;
    NewLevel();
    Refresh();
    Prowl(me);
});