DEV Community

Cover image for Azure Arcade: Deploy & Play a Modern Snake Game on App Service
Oladosu Ibrahim
Oladosu Ibrahim

Posted on

Azure Arcade: Deploy & Play a Modern Snake Game on App Service

Introduction

Azure App Service makes it ridiculously easy to host web apps without managing servers. In this hands-on guide, you’ll create a Windows-based App Service, deploy a modern Snake game directly from your browser using Advanced Tools (Kudu), and then practice Deployment Slots with traffic routing and swap, just like a real production rollout.

By the end, your app will be live on a public URL, you’ll have a safe “New Feature” slot for testing, and you’ll understand how to gradually send users to the new version before swapping it into production.

🎯 Skilling Objectives

  • Create and configure an Azure App Service (Windows) and Plan.
  • Use Advanced Tools (Kudu) to upload/edit web content in wwwroot.
  • Publish a modern HTML/CSS/JS game as your website.
  • Create a Deployment Slot, send 6% traffic to it, and swap safely.

Step 1: Create the Web App

💡 What is App Service?
Azure App Service is a PaaS offering for hosting web apps, APIs, and static sites with built-in scaling, deployment slots, and SSL.

Steps

  1. Log in to the Azure Portal.
  2. Search for App ServicesCreateWebApp
    Image 1
    Image 2

  3. Resource Group: IbrahimRG (create new if it doesn’t exist).

  4. Name: Console (or any globally unique name).

  5. Publish: Code

  6. Runtime stack: .NET 9 (STS)

  7. Operating System: Windows

  8. Region: your nearest region.
    Image 3

  9. App Service Plan: Create new → name it ConsolePlan.

  10. Click Review + createCreate.
    Image 4

  11. After deployment, click Go to resource.
    Image 5

✅ Your App Service is live with a default domain.

Step 2: Generate the Game Code

You’ll deploy a single-page app (index.html) that includes HTML, CSS, and JavaScript for an intuitive, interactive, and modern Snake game: keyboard & touch controls, pause, levels, high score, and a sleek UI.

Copy everything below into a file named index.html (you’ll paste it in Step 3).

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Snake • Azure Arcade</title>
  <style>
    :root{
      --bg:#0f1222; --panel:#151935; --panel-2:#0b0e1e; --text:#e8ecff; --muted:#9aa3c7;
      --accent:#6ee7ff; --accent-2:#a78bfa; --good:#34d399; --bad:#ef4444;
      --grid: 24; /* tile size */
    }
    *{box-sizing:border-box}
    html,body{height:100%;margin:0;background:radial-gradient(1200px 700px at 20% -10%, #1b2046 0%, #0f1222 55%) fixed;color:var(--text);font-family:Inter,system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif}
    .wrap{max-width:1100px;margin:0 auto;padding:24px}
    header{display:flex;gap:12px;align-items:center;justify-content:space-between;margin-bottom:16px}
    .title{font-weight:800;letter-spacing:.3px;font-size:clamp(20px,3.5vw,28px)}
    .card{background:linear-gradient(180deg,var(--panel),var(--panel-2));border:1px solid rgba(255,255,255,.06);border-radius:18px;box-shadow:0 10px 30px rgba(0,0,0,.35)}
    .board{display:grid;grid-template-columns:1fr 360px;gap:18px}
    @media (max-width:960px){.board{grid-template-columns:1fr}}
    .canvasWrap{position:relative;padding:16px}
    canvas{display:block;margin:0 auto;background:linear-gradient(180deg,#0b1228,#0a0e20);border:1px solid rgba(255,255,255,.06);border-radius:14px}
    .hud{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;margin:12px 0}
    .pill{padding:8px 12px;border-radius:999px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08);font-weight:600}
    .accent{background:linear-gradient(90deg, rgba(110,231,255,.22), rgba(167,139,250,.22));border-color:rgba(255,255,255,.18)}
    .side{padding:18px}
    .side h3{margin:6px 0 10px;font-size:14px;color:var(--muted);text-transform:uppercase;letter-spacing:.12em}
    .btn{cursor:pointer;user-select:none;display:inline-flex;align-items:center;justify-content:center;padding:10px 14px;border-radius:12px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.04);font-weight:700;transition:transform .05s}
    .btn:hover{background:rgba(255,255,255,.08)}
    .btn:active{transform:translateY(1px)}
    .btn.primary{background:linear-gradient(90deg,#5dd8ff,#9b85ff);color:#051018;border:none}
    .grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
    .note{color:var(--muted);font-size:13px;line-height:1.5}
    .controls{display:grid;grid-template-columns:repeat(3,64px);gap:8px;justify-content:center;margin-top:10px}
    .pad{height:64px}
    .pad:nth-child(2){grid-column:2}
    .pad:nth-child(3){grid-column:1}
    .pad:nth-child(4){grid-column:2}
    .pad:nth-child(5){grid-column:3}
    .legend{display:flex;gap:10px;justify-content:center;font-size:12px;color:var(--muted);margin-top:8px}
    .foodDot,.snakeDot,.wallDot{width:10px;height:10px;border-radius:3px}
    .foodDot{background:var(--good)}
    .snakeDot{background:var(--accent)}
    .wallDot{background:#3b82f6}
  </style>
</head>
<body>
  <div class="wrap">
    <header>
      <div class="title">Azure Arcade — Advanced Snake</div>
      <div class="hud">
        <div class="pill accent">Score: <span id="score">0</span></div>
        <div class="pill">High: <span id="high">0</span></div>
        <div class="pill">Level: <span id="level">1</span></div>
        <button id="pauseBtn" class="btn">Pause</button>
        <button id="restartBtn" class="btn">Restart</button>
      </div>
    </header>

    <div class="board">
      <div class="card canvasWrap">
        <canvas id="game" width="720" height="480" aria-label="Snake game area"></canvas>
        <div class="legend">
          <div class="snakeDot"></div> Snake
          <div class="foodDot"></div> Food
          <div class="wallDot"></div> Wall
        </div>
      </div>

      <aside class="card side">
        <h3>How to play</h3>
        <div class="note">Use <b>Arrow keys / WASD</b> to move. Press <b>Space</b> to pause. On mobile, use the on-screen pad.</div>
        <div class="controls">
          <button class="btn pad" data-dir="up"></button>
          <button class="btn pad" data-dir="left"></button>
          <button class="btn pad" data-dir="down"></button>
          <button class="btn pad" data-dir="right"></button>
        </div>
        <h3>Game rules</h3>
        <ul class="note" style="margin:0 0 10px 18px">
          <li>Eat food to grow and score.</li>
          <li>Don’t hit walls or yourself.</li>
          <li>Speed increases every level (every 5 foods).</li>
        </ul>
        <div class="grid">
          <button id="slowBtn" class="btn">Slow</button>
          <button id="fastBtn" class="btn">Fast</button>
          <button id="toggleWalls" class="btn">Toggle Walls</button>
          <button id="resetHigh" class="btn">Reset High</button>
        </div>
      </aside>
    </div>
  </div>

  <script>
    // --- Config ---
    const tile = 24;            // px per tile
    const cols = 28;            // 28 * 24 = 672 wide area
    const rows = 18;            // 18 * 24 = 432 high area
    const border = 2 * tile;    // inner padding for walls
    const baseSpeed = 120;      // ms per tick (gets faster)
    const levelEvery = 5;       // foods per level

    // --- Canvas setup ---
    const canvas = document.getElementById('game');
    const ctx = canvas.getContext('2d');
    canvas.width  = cols * tile + border * 2;
    canvas.height = rows * tile + border * 2;

    // --- UI refs ---
    const scoreEl = document.getElementById('score');
    const highEl  = document.getElementById('high');
    const levelEl = document.getElementById('level');
    const pauseBtn = document.getElementById('pauseBtn');
    const restartBtn = document.getElementById('restartBtn');
    const slowBtn = document.getElementById('slowBtn');
    const fastBtn = document.getElementById('fastBtn');
    const toggleWallsBtn = document.getElementById('toggleWalls');
    const resetHighBtn = document.getElementById('resetHigh');
    document.querySelectorAll('[data-dir]').forEach(b => b.addEventListener('click', () => onDirection(b.dataset.dir)));

    // --- State ---
    let snake, dir, nextDir, food, score, level, wallsOn, tick, speed, paused, over;
    const highScore = () => Number(localStorage.getItem('snakeHigh') || 0);
    const setHighScore = (v) => localStorage.setItem('snakeHigh', String(v));

    function reset() {
      snake = [
        {x: Math.floor(cols/2), y: Math.floor(rows/2)},
        {x: Math.floor(cols/2)-1, y: Math.floor(rows/2)},
      ];
      dir = {x:1,y:0};
      nextDir = {...dir};
      score = 0;
      level = 1;
      speed = baseSpeed;
      wallsOn = true;
      paused = false;
      over = false;
      spawnFood();
      updateHUD();
      loop();
    }

    function updateHUD(){
      scoreEl.textContent = score;
      highEl.textContent = highScore();
      levelEl.textContent = level;
      pauseBtn.textContent = paused ? 'Resume' : 'Pause';
      toggleWallsBtn.textContent = wallsOn ? 'Disable Walls' : 'Enable Walls';
    }

    function spawnFood(){
      do {
        food = { x: Math.floor(Math.random()*cols), y: Math.floor(Math.random()*rows) };
      } while (snake.some(s => s.x===food.x && s.y===food.y));
    }

    function drawBoard(){
      // outer background already stylish; draw inner playfield
      const x0 = border, y0 = border, w = cols*tile, h = rows*tile;
      // playfield
      const grd = ctx.createLinearGradient(0,y0,0,y0+h);
      grd.addColorStop(0, '#0c1530'); grd.addColorStop(1, '#0a1026');
      ctx.fillStyle = grd; ctx.fillRect(x0, y0, w, h);

      // decorative grid dots
      ctx.fillStyle = 'rgba(255,255,255,.04)';
      for(let y=0;y<rows;y++){
        for(let x=0;x<cols;x++){
          ctx.fillRect(x0 + x*tile + tile/2 - 1, y0 + y*tile + tile/2 - 1, 2, 2);
        }
      }

      // walls
      if (wallsOn){
        ctx.fillStyle = '#3b82f6';
        ctx.fillRect(x0-4, y0-4, w+8, 4); // top
        ctx.fillRect(x0-4, y0+h, w+8, 4); // bottom
        ctx.fillRect(x0-4, y0, 4, h);     // left
        ctx.fillRect(x0+w, y0, 4, h);     // right
      }
    }

    function drawSnake(){
      for(let i=0;i<snake.length;i++){
        const s = snake[i];
        const px = border + s.x*tile;
        const py = border + s.y*tile;
        const head = i===0;
        if(head){
          // head gradient
          const g = ctx.createLinearGradient(px, py, px, py+tile);
          g.addColorStop(0, '#6ee7ff');
          g.addColorStop(1, '#a78bfa');
          ctx.fillStyle = g;
        } else {
          ctx.fillStyle = 'rgba(167,139,250,.85)';
        }
        ctx.beginPath();
        ctx.roundRect(px+3, py+3, tile-6, tile-6, 6);
        ctx.fill();
      }
    }

    function drawFood(){
      const px = border + food.x*tile + tile/2;
      const py = border + food.y*tile + tile/2;
      ctx.fillStyle = '#34d399';
      ctx.beginPath();
      ctx.arc(px, py, tile*0.32, 0, Math.PI*2);
      ctx.fill();
      // glow
      ctx.beginPath();
      ctx.arc(px, py, tile*0.55, 0, Math.PI*2);
      ctx.fillStyle = 'rgba(52,211,153,.14)';
      ctx.fill();
    }

    function step(){
      if (paused || over) return;
      // apply nextDir (prevent instant reversal)
      if ((nextDir.x !== -dir.x) || (nextDir.y !== -dir.y)) dir = nextDir;

      // new head
      const head = {x: snake[0].x + dir.x, y: snake[0].y + dir.y};

      // wrap or collide with walls
      if (wallsOn){
        if (head.x<0 || head.x>=cols || head.y<0 || head.y>=rows){
          return gameOver();
        }
      } else {
        head.x = (head.x + cols) % cols;
        head.y = (head.y + rows) % rows;
      }

      // self-collision
      if (snake.some((s, i) => i>0 && s.x===head.x && s.y===head.y)) return gameOver();

      snake.unshift(head);

      // eat or move
      if (head.x===food.x && head.y===food.y){
        score++;
        if (score > highScore()) setHighScore(score);
        // level up
        if (score % levelEvery === 0){
          level++;
          speed = Math.max(50, speed - 10);
        }
        spawnFood();
      } else {
        snake.pop();
      }

      draw();
      tick = setTimeout(step, speed);
    }

    function draw(){
      ctx.clearRect(0,0,canvas.width,canvas.height);
      drawBoard();
      drawFood();
      drawSnake();
      updateHUD();
    }

    function loop(){
      clearTimeout(tick);
      draw();
      tick = setTimeout(step, speed);
    }

    function onDirection(which){
      const map = {up:{x:0,y:-1},down:{x:0,y:1},left:{x:-1,y:0},right:{x:1,y:0}};
      nextDir = map[which] || nextDir;
    }

    function gameOver(){
      over = true;
      updateHUD();
      // overlay
      ctx.fillStyle = 'rgba(0,0,0,.55)';
      ctx.fillRect(0,0,canvas.width,canvas.height);
      ctx.fillStyle = '#fff';
      ctx.font = '700 28px Inter, system-ui, sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('Game Over', canvas.width/2, canvas.height/2 - 8);
      ctx.font = '500 16px Inter, system-ui, sans-serif';
      ctx.fillText('Press Restart to play again', canvas.width/2, canvas.height/2 + 18);
    }

    // --- Input ---
    window.addEventListener('keydown', (e)=>{
      const k = e.key.toLowerCase();
      if (k==='arrowup' || k==='w') onDirection('up');
      else if (k==='arrowdown' || k==='s') onDirection('down');
      else if (k==='arrowleft' || k==='a') onDirection('left');
      else if (k==='arrowright' || k==='d') onDirection('right');
      else if (k===' ') { paused = !paused; updateHUD(); if (!paused) loop(); }
    });

    pauseBtn.addEventListener('click', ()=>{ paused = !paused; updateHUD(); if (!paused) loop(); });
    restartBtn.addEventListener('click', ()=> reset());
    slowBtn.addEventListener('click', ()=>{ speed = Math.min(220, speed+20); });
    fastBtn.addEventListener('click', ()=>{ speed = Math.max(40, speed-20); });
    toggleWallsBtn.addEventListener('click', ()=>{ wallsOn = !wallsOn; updateHUD(); });
    resetHighBtn.addEventListener('click', ()=>{ setHighScore(0); updateHUD(); });

    // Polyfill for roundRect on older browsers
    if (!CanvasRenderingContext2D.prototype.roundRect) {
      CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
        if (w < 2 * r) r = w / 2;
        if (h < 2 * r) r = h / 2;
        this.beginPath();
        this.moveTo(x + r, y);
        this.arcTo(x + w, y, x + w, y + h, r);
        this.arcTo(x + w, y + h, x, y + h, r);
        this.arcTo(x, y + h, x, y, r);
        this.arcTo(x, y, x + w, y, r);
        this.closePath();
        return this;
      }
    }

    reset();
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Step 3: Deploy via Advanced Tools (Kudu)

💡 Why Kudu?
Kudu is the engine behind App Service that lets you browse/edit the site file system, run commands, and view logs—right from the browser.

Steps

  1. In your App Service, go to Development ToolsAdvanced ToolsGo
    Image 6

  2. A new Kudu tab opens. Select Debug consoleCMD
    Image 7

  3. Navigate: sitewwwroot
    Image 8
    Image 9

  4. If you see hostingstart.html, delete it (otherwise it may override index.html).

  5. Click the pencil icon to create/edit a file named index.html
    Image 10

  6. Paste the entire game code from Step 2, then Save.
    Image 11

✅ Your website now serves the Snake game from wwwroot/index.html.

Step 4: Open Your Public URL

  1. Go back to your App Service Overview page.
  2. Under Default domain, click the link (e.g., https://console.azurewebsites.net or similar)
    Image 12

  3. Your Snake game should load instantly. Play away!
    Image 13

🛠️ Troubleshooting

  • If you still see the Azure default page, make sure hostingstart.html is deleted and your file is named exactly index.html.
  • Clear cache or open in a private window if changes don’t appear.

Step 5: Use Deployment Slots (Blue-Green Lite)

💡 What are Deployment Slots?
Slots let you have a staging site running alongside production. You can route a small percentage of real traffic to test your changes, then swap when you’re confident.

Steps

  1. In your App Service, go to Deployment slots

  2. Click Add slot → Name: New FeatureAdd
    Image 14

  3. Back on Deployment slots, select Traffic and assign 6% to New Feature. Save

  • Now 6% of users see the slot version; the rest stay on production. Image 15
  1. When satisfied, choose Swap → source: New Feature → target: ConsoleStart swap Image 16 Image 17

✅ You safely validated the change, routed a small slice of traffic, and swapped the update into production without downtime.

Conclusion

You just:

  • Created a Windows App Service with .NET 9 (STS) and a custom plan.
  • Deployed a modern HTML/CSS/JS Snake game via Kudu.
  • Practiced progressive exposure using Deployment Slots, sending 6% of traffic before a swap.

This is the same muscle memory you’ll use for real apps—feature testing, safe rollouts, and fast rollbacks all without touching servers.

🔑 Key takeaway: With Azure App Service + Kudu + Deployment Slots, you ship faster and safer experiment in a slot, route a trickle of traffic, then swap with confidence.

Top comments (0)