' Pomppu Paavo
'
' This program is free software: released under Creative Commons Zero (CC0) license
' by Svjatoslav Agejenko.
' Email: svjatoslav@svjatoslav.eu
' Homepage: http://www.svjatoslav.eu
'
' Changelog:
' 1998, Initial version
' 2025, Improved program readability

DECLARE SUB DisplayGameStatistics ()
DECLARE SUB LoadCurrentLevel (levelNumber%)
DECLARE SUB RenderSpriteFromFile (xPosition%, yPosition%, spriteID%, animationFrame%)

DEFINT A-Z
DIM SHARED AsciiLevelData(1 TO 20) AS STRING * 31
DIM SHARED Companion1VerticalPosition%, Companion1HorizontalPosition%, Companion2VerticalPosition%, Companion2HorizontalPosition%
DIM SHARED LivesRemaining%, CoinsCollected%
DIM SHARED DoorEntryX%, DoorEntryY%
DIM SHARED DoorExitX%, DoorExitY%
DIM SHARED CurrentLevelNumber%
DIM SHARED TerrainGrid(0 TO 34, -10 TO 20) AS STRING * 1
DIM SHARED ObjectGrid(0 TO 34, -10 TO 20) AS STRING * 1

' Full scale sprites (mostly 20x20 pixels)
DIM SHARED SolidTerrainSprite(51)
DIM SHARED EmptySpaceSprite(51)
DIM SHARED BoxSprite(51)
DIM SHARED PlayerSpriteBuffer(70)
DIM SHARED CloudSprite(209)
DIM SHARED StarSprite(50)
DIM SHARED HedgehogSprite(30)
DIM SHARED HedgehogSprite1(30)
DIM SHARED HedgehogSprite2(30)
DIM SHARED CoinSprite(30)
DIM SHARED BushSprite(100)
DIM SHARED WindowSprite(100)
DIM SHARED TrampolineSprite(50)
DIM SHARED IntroScreen(6000)
DIM SHARED TreeSprite(3000)
DIM SHARED RightArrowSprite(51)
DIM SHARED LeftArrowSprite(51)
DIM SHARED HoleSprite(51)
DIM SHARED PlayerRunRight1Sprite(51)
DIM SHARED PlayerRunRight2Sprite(51)
DIM SHARED PlayerRunLeft1Sprite(51)
DIM SHARED PlayerRunLeft2Sprite(51)
DIM SHARED PlayerJumpingSprite(51)
DIM SHARED DoorSprite(120)

' Define keyboard control sequences using special arrow key codes
leftArrowKey$ = CHR$(0) + "K"
rightArrowKey$ = CHR$(0) + "M"
upArrowKey$ = CHR$(0) + "H"
downArrowKey$ = CHR$(0) + "P"

LivesRemaining% = 10

SCREEN 1
CurrentLevelNumber% = 1

' Capture sprite images from screen drawing operations.
' This technique uses GET command to save drawn graphics directly into arrays.
GET (1, 1)-(20, 20), EmptySpaceSprite
RenderSpriteFromFile 0, 0, 1, 1
GET (1, 1)-(20, 20), SolidTerrainSprite
RenderSpriteFromFile 0, 0, 4, 1
GET (1, 1)-(20, 20), BoxSprite
PUT (1, 180), SolidTerrainSprite, PSET
PUT (300, 1), SolidTerrainSprite, PSET
PUT (300, 180), SolidTerrainSprite, PSET
RenderSpriteFromFile 30, 50, 2, 1
GET (31, 51)-(91, 76), CloudSprite
PUT (160, 10), CloudSprite, PSET
RenderSpriteFromFile 20, 100, 3, 1
GET (21, 101)-(31, 114), StarSprite
CLS
RenderSpriteFromFile 0, 0, 5, 1
GET (1, 1)-(11, 8), HedgehogSprite
CLS
RenderSpriteFromFile 0, 0, 6, 1
GET (1, 1)-(8, 10), CoinSprite
CLS
RenderSpriteFromFile 0, 0, 7, 1
GET (1, 1)-(31, 11), BushSprite
CLS
RenderSpriteFromFile 0, 0, 8, 1
GET (1, 1)-(26, 21), WindowSprite
CLS
RenderSpriteFromFile 0, 0, 9, 1
GET (1, 1)-(21, 11), TrampolineSprite
CLS
RenderSpriteFromFile 0, 0, 11, 1
GET (1, 1)-(51, 81), TreeSprite
CLS
RenderSpriteFromFile 0, 0, 12, 1
GET (1, 1)-(20, 20), RightArrowSprite
CLS
RenderSpriteFromFile 0, 0, 12, 50
GET (1, 1)-(20, 20), LeftArrowSprite
CLS
RenderSpriteFromFile 0, 0, 13, 1
GET (1, 1)-(20, 20), HoleSprite
CLS
RenderSpriteFromFile 0, 0, 14, 1
GET (1, 1)-(20, 20), PlayerRunRight1Sprite
CLS
RenderSpriteFromFile 0, 0, 15, 1
GET (1, 1)-(20, 20), PlayerRunRight2Sprite
CLS
RenderSpriteFromFile 0, 0, 14, 50
GET (1, 1)-(20, 20), PlayerRunLeft1Sprite
CLS
RenderSpriteFromFile 0, 0, 15, 50
GET (1, 1)-(20, 20), PlayerRunLeft2Sprite
CLS
RenderSpriteFromFile 0, 0, 16, 1
GET (1, 1)-(20, 20), PlayerJumpingSprite
CLS

' Capture the introductory screen image
RenderSpriteFromFile -1, -1, 10, 4
GET (1, 1)-(318, 124), IntroScreen

key$ = INPUT$(1)

LoadCurrentLevel 1
PlayerXPosition% = 50
PlayerYPosition% = 50
Companion1HorizontalSpeed% = 1
Companion2HorizontalSpeed% = 1
MainGameLoop:
keyboardInput$ = INKEY$
IF PlayerYPosition% > 0 THEN GET (PlayerXPosition%, PlayerYPosition%)-(PlayerXPosition% + 20, PlayerYPosition% + 20), PlayerSpriteBuffer

' Display appropriate player animation frame based on current movement state
IF PlayerYPosition% > 0 THEN IF PlayerAnimationState% = 1 THEN PUT (PlayerXPosition%, PlayerYPosition%), PlayerRunRight1Sprite, OR
IF PlayerYPosition% > 0 THEN IF PlayerAnimationState% = 2 THEN PUT (PlayerXPosition%, PlayerYPosition%), PlayerRunRight2Sprite, OR
IF PlayerYPosition% > 0 THEN IF PlayerAnimationState% = 10 THEN PUT (PlayerXPosition%, PlayerYPosition%), PlayerRunLeft1Sprite, OR
IF PlayerYPosition% > 0 THEN IF PlayerAnimationState% = 20 THEN PUT (PlayerXPosition%, PlayerYPosition%), PlayerRunLeft2Sprite, OR
IF PlayerYPosition% > 0 THEN IF PlayerAnimationState% = 3 THEN PUT (PlayerXPosition%, PlayerYPosition%), PlayerJumpingSprite, OR

' Draw first companion hedgehog to screen
GET (Companion1HorizontalPosition%, Companion1VerticalPosition%)-(Companion1HorizontalPosition% + 10, Companion1VerticalPosition% + 10), HedgehogSprite1
PUT (Companion1HorizontalPosition%, Companion1VerticalPosition%), HedgehogSprite, OR

' Draw second companion hedgehog to screen
GET (Companion2HorizontalPosition%, Companion2VerticalPosition%)-(Companion2HorizontalPosition% + 10, Companion2VerticalPosition% + 10), HedgehogSprite2
PUT (Companion2HorizontalPosition%, Companion2VerticalPosition%), HedgehogSprite, OR

' Create short delay using sound command (workaround for lack of built-in sub-second delay in QBasic)
' SOUND 0,0.8 produces an inaudible tone that takes approximately 0.8 milliseconds to process
SOUND 0, .8

' Check collisions with solid terrain ("z" character in grid)
' Right side collision detection - checks two points along right edge of player hitbox
' Player hitbox is slightly smaller than sprite (38 instead of full 40 width)
IF TerrainGrid((PlayerXPosition% + 38) \ 20, (PlayerYPosition% + 37) \ 20) = "z" THEN PlayerHorizontalSpeed% = -1: HorizontalAnimationCounter% = 9: GroundContactTime% = 0
IF TerrainGrid((PlayerXPosition% + 38) \ 20, (PlayerYPosition% + 22) \ 20) = "z" THEN PlayerHorizontalSpeed% = -1: HorizontalAnimationCounter% = 9: GroundContactTime% = 0

' Left side collision detection - checks two points along left edge of player hitbox
IF TerrainGrid((PlayerXPosition% + 21) \ 20, (PlayerYPosition% + 22) \ 20) = "z" THEN PlayerHorizontalSpeed% = 1: HorizontalAnimationCounter% = 9: GroundContactTime% = 0
IF TerrainGrid((PlayerXPosition% + 21) \ 20, (PlayerYPosition% + 37) \ 20) = "z" THEN PlayerHorizontalSpeed% = 1: HorizontalAnimationCounter% = 9: GroundContactTime% = 0

' Top collision detection - checks if player hits ceiling ("z" block above)
IF TerrainGrid((PlayerXPosition% + 22) \ 20, (PlayerYPosition% + 21) \ 20) = "z" THEN PlayerVerticalSpeed% = 0: CeilingContactCooldown% = 10: GroundContactTime% = 0
IF TerrainGrid((PlayerXPosition% + 37) \ 20, (PlayerYPosition% + 21) \ 20) = "z" THEN PlayerVerticalSpeed% = 0: CeilingContactCooldown% = 10: GroundContactTime% = 0

' Bottom collision detection (landing on ground) - checks if player lands on solid terrain
' When landing, sets vertical speed to negative (bounce effect), resets jump ability
IF TerrainGrid((PlayerXPosition% + 22) \ 20, (PlayerYPosition% + 38) \ 20) = "z" THEN PlayerVerticalSpeed% = -1: VerticalAnimationCounter% = 2: JumpReadinessCounter% = 1: GroundContactTime% = 0
IF TerrainGrid((PlayerXPosition% + 37) \ 20, (PlayerYPosition% + 38) \ 20) = "z" THEN PlayerVerticalSpeed% = -1: VerticalAnimationCounter% = 2: JumpReadinessCounter% = 1: GroundContactTime% = 0

HazardExposureTimer% = HazardExposureTimer% + 1: IF ObjectGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 30) \ 20) = "q" AND HazardExposureTimer% > 20 THEN HazardExposureTimer% = 1: LivesRemaining% = LivesRemaining% - 1:    _
PlayerHorizontalSpeed% = -10: PlayerVerticalSpeed% = -5: DisplayGameStatistics
IF HazardExposureTimer% > 100 THEN HazardExposureTimer% = 50

' Coin collection logic - when player overlaps coin position ("1" in grid)
IF TerrainGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 30) \ 20) = "1" THEN CoinsCollected% = CoinsCollected% + 1: CoinCollectedFlag% = 1: DisplayGameStatistics: TerrainGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 30) \ 20) = ""

' Trampoline effect - when player lands on trampoline ("v" marker), gives strong upward boost
IF TerrainGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 30) \ 20) = "v" THEN PlayerVerticalSpeed% = -8: PlayerAnimationState% = 3

' Conveyor belt movement - ">" and "<" markers in object grid push player horizontally
IF ObjectGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 38) \ 20) = ">" THEN PlayerHorizontalSpeed% = 3
IF ObjectGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 38) \ 20) = "<" THEN PlayerHorizontalSpeed% = -3

GroundContactTime% = GroundContactTime% + 1
HorizontalAnimationCounter% = HorizontalAnimationCounter% + 1: IF HorizontalAnimationCounter% > 10 THEN HorizontalAnimationCounter% = 0: IF PlayerHorizontalSpeed% > 0 THEN PlayerHorizontalSpeed% = PlayerHorizontalSpeed% - 1 ELSE IF  _
PlayerHorizontalSpeed% < 0 THEN PlayerHorizontalSpeed% = PlayerHorizontalSpeed% + 1
VerticalAnimationCounter% = VerticalAnimationCounter% + 1: IF VerticalAnimationCounter% > 3 THEN VerticalAnimationCounter% = 0: PlayerVerticalSpeed% = PlayerVerticalSpeed% + 1
JumpReadinessCounter% = JumpReadinessCounter% + 1: CeilingContactCooldown% = CeilingContactCooldown% - 1

' Breakable block interaction - hitting block from below ("o" character) destroys it
IF ObjectGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 21) \ 20) = "o" THEN TerrainGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 21) \ 20) = "": ObjectGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 21) \ 20) = "":    _
TopBreakableBlockClearedFlag% = 1

' Bottom breakable block interaction - similar to above but from top side
IF ObjectGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 38) \ 20) = "a" THEN TerrainGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 38) \ 20) = "": ObjectGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 38) \ 20) = "":    _
BottomBreakableBlockClearedFlag% = 1

' First companion hedgehog terrain collision checks
IF TerrainGrid((Companion1HorizontalPosition% + 25) \ 20, (Companion1VerticalPosition% + 25) \ 20) = "z" THEN Companion1VerticalSpeed% = -1
IF TerrainGrid((Companion1HorizontalPosition% + 30) \ 20, (Companion1VerticalPosition% + 10) \ 20) = "z" THEN Companion1HorizontalSpeed% = -1
IF TerrainGrid((Companion1HorizontalPosition% + 20) \ 20, (Companion1VerticalPosition% + 10) \ 20) = "z" THEN Companion1HorizontalSpeed% = 1

' Second companion hedgehog terrain collision checks
IF TerrainGrid((Companion2HorizontalPosition% + 25) \ 20, (Companion2VerticalPosition% + 25) \ 20) = "z" THEN Companion2VerticalSpeed% = -1
IF TerrainGrid((Companion2HorizontalPosition% + 30) \ 20, (Companion2VerticalPosition% + 10) \ 20) = "z" THEN Companion2HorizontalSpeed% = -1
IF TerrainGrid((Companion2HorizontalPosition% + 20) \ 20, (Companion2VerticalPosition% + 10) \ 20) = "z" THEN Companion2HorizontalSpeed% = 1

' Screen boundary checks for companions to keep them within play area
IF Companion1HorizontalPosition% > 300 THEN Companion1HorizontalSpeed% = -1
IF Companion1HorizontalPosition% < 3 THEN Companion1HorizontalSpeed% = 1
IF Companion2HorizontalPosition% > 300 THEN Companion2HorizontalSpeed% = -1
IF Companion2HorizontalPosition% < 3 THEN Companion2HorizontalSpeed% = 1

' Restore previous companion positions before moving them (erases old sprite)
PUT (Companion2HorizontalPosition%, Companion2VerticalPosition%), HedgehogSprite2, PSET
PUT (Companion1HorizontalPosition%, Companion1VerticalPosition%), HedgehogSprite1, PSET

' Restore previous player position (erases old sprite)
IF PlayerYPosition% > 0 THEN PUT (PlayerXPosition%, PlayerYPosition%), PlayerSpriteBuffer, PSET

' Handle sprite clearing after collecting coins or breaking blocks
IF TopBreakableBlockClearedFlag% = 1 THEN TopBreakableBlockClearedFlag% = 0: PUT (((PlayerXPosition% + 10) \ 20) * 20, (PlayerYPosition% \ 20) * 20), EmptySpaceSprite, PSET
IF CoinCollectedFlag% = 1 THEN CoinCollectedFlag% = 0: PUT (((PlayerXPosition% + 10) \ 20) * 20, ((PlayerYPosition% + 10) \ 20) * 20), EmptySpaceSprite, PSET
IF BottomBreakableBlockClearedFlag% = 1 THEN BottomBreakableBlockClearedFlag% = 0: PUT (((PlayerXPosition% + 10) \ 20) * 20, ((PlayerYPosition% + 28) \ 20) * 20), EmptySpaceSprite, PSET

IF PullBackwardFlag% = 1 THEN PullBackwardFlag% = 0: PlayerXPosition% = PlayerXPosition% - 20

ObjectGrid((Companion2HorizontalPosition% + 25) \ 20, (Companion2VerticalPosition% + 8) \ 20) = "": ObjectGrid((Companion1HorizontalPosition% + 25) \ 20, (Companion1VerticalPosition% + 8) \ 20) = ""

' Update companion positions based on their current velocities
Companion1HorizontalPosition% = Companion1HorizontalPosition% + Companion1HorizontalSpeed%
Companion1VerticalPosition% = Companion1VerticalPosition% + Companion1VerticalSpeed%
Companion2HorizontalPosition% = Companion2HorizontalPosition% + Companion2HorizontalSpeed%
Companion2VerticalPosition% = Companion2VerticalPosition% + Companion2VerticalSpeed%

' Mark new companion positions in object grid for collision detection
ObjectGrid((Companion2HorizontalPosition% + 25) \ 20, (Companion2VerticalPosition% + 8) \ 20) = "q": ObjectGrid((Companion1HorizontalPosition% + 25) \ 20, (Companion1VerticalPosition% + 8) \ 20) = "q"

' Apply gravity to companions (max downward speed capped at 2 pixels/frame)
Companion1VerticalSpeed% = Companion1VerticalSpeed% + 1: IF Companion1VerticalSpeed% > 2 THEN Companion1VerticalSpeed% = 2
Companion2VerticalSpeed% = Companion2VerticalSpeed% + 1: IF Companion2VerticalSpeed% > 2 THEN Companion2VerticalSpeed% = 2

' Update player position based on calculated velocities
PlayerXPosition% = PlayerXPosition% + PlayerHorizontalSpeed%
PlayerYPosition% = PlayerYPosition% + PlayerVerticalSpeed%

' Level transition when reaching right edge of screen
IF PlayerXPosition% > 297 THEN PlayerXPosition% = 2: CurrentLevelNumber% = CurrentLevelNumber% + 1: LoadCurrentLevel CurrentLevelNumber%: Companion1HorizontalSpeed% = 1: Companion2HorizontalSpeed% = 1

' Level transition when reaching left edge of screen
IF PlayerXPosition% < 1 THEN PlayerXPosition% = 296: IF CurrentLevelNumber% = 1 THEN LoadCurrentLevel CurrentLevelNumber% ELSE CurrentLevelNumber% = CurrentLevelNumber% - 1: LoadCurrentLevel CurrentLevelNumber%: IF GraphicsDisplayMode% = 2 THEN  _
PlayerXPosition% = 594

' Player falls off bottom of screen - lose life and restart level
IF PlayerYPosition% > 179 THEN LivesRemaining% = LivesRemaining% - 1: DisplayGameStatistics: CurrentLevelNumber% = CurrentLevelNumber% - 1: LoadCurrentLevel CurrentLevelNumber%: PlayerYPosition% = 100: PlayerXPosition% = 2: DisplayGameStatistics

' Teleportation triggers ("u" and "U" markers in level data create door pairs)
IF TerrainGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 30) \ 20) = "u" THEN PlayerXPosition% = DoorExitX% + 10: PlayerYPosition% = DoorExitY%: PlayerHorizontalSpeed% = 0
IF TerrainGrid((PlayerXPosition% + 30) \ 20, (PlayerYPosition% + 30) \ 20) = "U" THEN PlayerXPosition% = DoorEntryX% + 10: PlayerYPosition% = DoorEntryY%: PlayerHorizontalSpeed% = 0

' Handle keyboard input for movement controls
IF keyboardInput$ = rightArrowKey$ THEN PlayerHorizontalSpeed% = PlayerHorizontalSpeed% + 1: IF PlayerHorizontalSpeed% > 3 THEN PlayerHorizontalSpeed% = 3: HorizontalAnimationCounter% = 0 ELSE IF GroundContactTime% > 10 THEN PlayerHorizontalSpeed% = 5
IF keyboardInput$ = rightArrowKey$ THEN IF RunningAnimationStage% = 1 THEN PlayerAnimationState% = 1 ELSE PlayerAnimationState% = 2
IF keyboardInput$ = leftArrowKey$ THEN IF RunningAnimationStage% = 1 THEN PlayerAnimationState% = 10 ELSE PlayerAnimationState% = 20
IF keyboardInput$ = leftArrowKey$ THEN PlayerHorizontalSpeed% = PlayerHorizontalSpeed% - 1: IF PlayerHorizontalSpeed% < -3 THEN PlayerHorizontalSpeed% = -3: HorizontalAnimationCounter% = 0 ELSE IF GroundContactTime% > 10 THEN PlayerHorizontalSpeed% = -5
IF keyboardInput$ = upArrowKey$ AND JumpReadinessCounter% < 10 THEN PlayerVerticalSpeed% = PlayerVerticalSpeed% - 5: JumpReadinessCounter% = 20: VerticalAnimationCounter% = 0: PlayerAnimationState% = 3
IF keyboardInput$ = downArrowKey$ THEN PlayerVerticalSpeed% = PlayerVerticalSpeed% + 1
IF keyboardInput$ = "/" THEN PlayerXPosition% = 2: PlayerYPosition% = 50: CurrentLevelNumber% = CurrentLevelNumber% + 1: LoadCurrentLevel CurrentLevelNumber%: Companion1HorizontalSpeed% = 1: Companion2HorizontalSpeed% = 1
IF keyboardInput$ = "+" THEN PlayerXPosition% = 2: PlayerYPosition% = 50: CurrentLevelNumber% = CurrentLevelNumber% + 5: LoadCurrentLevel CurrentLevelNumber%: Companion1HorizontalSpeed% = 1: Companion2HorizontalSpeed% = 1
IF keyboardInput$ = "q" THEN END

' Cycle through running animation frames (alternates between two states)
RunningAnimationStage% = RunningAnimationStage% + 1
IF RunningAnimationStage% = 3 THEN RunningAnimationStage% = 1
GOTO MainGameLoop

SUB DisplayGameStatistics
'
' Updates and displays the game's status information on screen (coins collected, remaining lives)
' Handles game over condition when player runs out of lives.
'
' This subroutine is triggered whenever:
' - A coin is collected (incrementing coins counter)
' - The player loses a life due to hazards or falling off-screen
' - Periodically during gameplay for live status updates

LOCATE 1, 1
IF GraphicsDisplayMode% = 2 THEN GOTO SkipTextDisplay

' Clear previous statistics display to prevent overlapping text
PRINT "                "

' Award extra life every 10 coins collected as bonus
IF CoinsCollected% > 9 THEN
    CoinsCollected% = 0
    LivesRemaining% = LivesRemaining% + 1
END IF

' Display current game statistics in top-left corner of screen
LOCATE 1, 1
PRINT "Coins: "; CoinsCollected%; "  Lives: "; LivesRemaining%

SkipTextDisplay:
' Check if player has completely run out of lives (game over condition)
IF LivesRemaining% < 0 THEN END
END SUB

SUB LoadCurrentLevel (levelNumber%)
125
Companion1HorizontalPosition% = 0
Companion1VerticalPosition% = 0
Companion2HorizontalPosition = 0
Companion2VerticalPosition% = 0

' Clear terrain and object grids before loading new level data
FOR xIndex% = 1 TO 32
  FOR yIndex% = 1 TO 20
    TerrainGrid(xIndex%, yIndex%) = ""
    ObjectGrid(xIndex%, yIndex%) = ""
  NEXT yIndex%
NEXT xIndex%

' Initialize ASCII level data array
FOR lineCounter% = 1 TO 20
  AsciiLevelData(lineCounter%) = ""
NEXT lineCounter%

CLS
LOCATE 3, 10

' Determine which level file to load based on requested level number
IF levelNumber% >= 1 AND levelNumber% <= 18 THEN
  fileName$ = "lvl/" + LTRIM$(STR$(levelNumber%)) + ".lvl"
  OPEN fileName$ FOR INPUT AS #1
  INPUT #1, LevelNumberAdjustment%
  rowIndex% = 1
  WHILE NOT EOF(1)
    LINE INPUT #1, AsciiLevelData(rowIndex%)
    rowIndex% = rowIndex% + 1
  WEND
  CLOSE #1
  CurrentLevelNumber% = CurrentLevelNumber% + LevelNumberAdjustment%
ELSE
  ' Special handling for non-standard levels (intro screen, test levels)
  SELECT CASE levelNumber%
  CASE 19
    CLS
    RenderSpriteFromFile 1, 1, 10, 3
    LOCATE 20, 1
    PRINT "end"
    ' Wait for 50 frames before continuing (creates brief pause)
    FOR frameCounter% = 1 TO 50
      inputBuffer$ = INKEY$
    NEXT frameCounter%
    inputBuffer$ = INPUT$(1)
    CLS
    SCREEN 2
    END
  CASE 100
    ' Create empty level with 10 blank lines
    FOR lineIndex% = 1 TO 10
      AsciiLevelData(lineIndex%) = "                "
    NEXT lineIndex%
  CASE 101
    ' Create larger empty level with 19 blank lines
    FOR lineIndex% = 1 TO 19
      AsciiLevelData(lineIndex%) = "                               "
    NEXT lineIndex%
    GraphicsDisplayMode% = 2
  END SELECT
END IF

' Parse ASCII level representation into game world
FOR rowIndex% = 0 TO 9
  FOR columnIndex% = 0 TO 15
    ' Extract single character from level data at current position
    currentCharacter$ = RIGHT$(LEFT$(AsciiLevelData(rowIndex% + 1), columnIndex% + 1), 1)

    ' Interpret character codes to build terrain and place objects:
    ' "m" = solid block (terrain)
    IF currentCharacter$ = "m" THEN PUT (columnIndex% * 20, rowIndex% * 20), SolidTerrainSprite, PSET: TerrainGrid(columnIndex% + 1, rowIndex% + 1) = "z"

    ' "o" = breakable box (both terrain and object properties)
    IF currentCharacter$ = "o" THEN PUT (columnIndex% * 20, rowIndex% * 20), BoxSprite, PSET: TerrainGrid(columnIndex% + 1, rowIndex% + 1) = "z": ObjectGrid(columnIndex% + 1, rowIndex% + 1) = "o"

    ' "." = star decoration (cosmetic only)
    IF currentCharacter$ = "." THEN PUT (columnIndex% * 20, rowIndex% * 20), StarSprite, PSET

    ' "-" = cloud decoration (cosmetic only)
    IF currentCharacter$ = "-" THEN PUT (columnIndex% * 20, rowIndex% * 20), CloudSprite, PSET

    ' "x" = starting position for first companion hedgehog
    IF currentCharacter$ = "x" THEN Companion1HorizontalPosition% = columnIndex% * 20: Companion1VerticalPosition% = (rowIndex% + 1) * 20

    ' "y" = starting position for second companion hedgehog
    IF currentCharacter$ = "y" THEN Companion2HorizontalPosition% = columnIndex% * 20: Companion2VerticalPosition% = (rowIndex% + 1) * 20

    ' "1" = collectible coin
    IF currentCharacter$ = "1" THEN PUT (columnIndex% * 20, rowIndex% * 20), CoinSprite, PSET: TerrainGrid(columnIndex% + 1, rowIndex% + 1) = "1"

    ' "p" = decorative bush (rendered slightly lower)
    IF currentCharacter$ = "p" THEN PUT (columnIndex% * 20, (rowIndex% * 20) + 10), BushSprite, PSET

    ' "h" = window decoration
    IF currentCharacter$ = "h" THEN PUT (columnIndex% * 20, rowIndex% * 20), WindowSprite, PSET

    ' "v" = trampoline that boosts player upward when landed on
    IF currentCharacter$ = "v" THEN PUT (columnIndex% * 20, (rowIndex% * 20) + 10), TrampolineSprite, PSET: TerrainGrid(columnIndex% + 1, rowIndex% + 1) = "v"

    ' "t" = tree decoration
    IF currentCharacter$ = "t" THEN PUT (columnIndex% * 20, rowIndex% * 20), TreeSprite, PSET

    ' ">" = right-moving conveyor belt (overrides normal movement)
    IF currentCharacter$ = ">" THEN PUT (columnIndex% * 20, rowIndex% * 20), RightArrowSprite, PSET: TerrainGrid(columnIndex% + 1, rowIndex% + 1) = "z": ObjectGrid(columnIndex% + 1, rowIndex% + 1) = ">"

    ' "<" = left-moving conveyor belt (overrides normal movement)
    IF currentCharacter$ = "<" THEN PUT (columnIndex% * 20, rowIndex% * 20), LeftArrowSprite, PSET: TerrainGrid(columnIndex% + 1, rowIndex% + 1) = "z": ObjectGrid(columnIndex% + 1, rowIndex% + 1) = "<"

    ' "a" = bottom breakable block (similar to "o" but different behavior)
    IF currentCharacter$ = "a" THEN PUT (columnIndex% * 20, rowIndex% * 20), HoleSprite, PSET: TerrainGrid(columnIndex% + 1, rowIndex% + 1) = "z": ObjectGrid(columnIndex% + 1, rowIndex% + 1) = "a"

    ' "u" = entry door for teleportation system
    IF currentCharacter$ = "u" THEN PUT (columnIndex% * 20, rowIndex% * 20), DoorSprite, PSET: TerrainGrid(columnIndex% + 1, rowIndex% + 1) = "u": DoorEntryX% = columnIndex% * 20: DoorEntryY% = rowIndex% * 20

    ' "U" = exit door for teleportation system (pairs with "u")
    IF currentCharacter$ = "U" THEN PUT (columnIndex% * 20, rowIndex% * 20), DoorSprite, PSET: TerrainGrid(columnIndex% + 1, rowIndex% + 1) = "U": DoorExitX% = columnIndex% * 20: DoorExitY% = rowIndex% * 20
  NEXT columnIndex%
NEXT rowIndex%
END SUB

SUB RenderSpriteFromFile (x%, y%, spriteID%, animationFrame%)
' Renders a sprite on screen by loading pixel data directly from external file
' File format explanation:
' - First line contains height (number of pixel rows in sprite)
' - Subsequent lines contain strings of digit characters where:
'     '0' = transparent/background (no drawing)
'     '1'-'3' = color values (mapped to current palette)
' Example file for simple 3x2 sprite:
'     2
'     123
'     010
'
' Parameters:
' xPosition%, yPosition%   = Top-left drawing coordinates on screen
' spriteID%                = Which sprite file to load (references img/<id>.i01)
' animationFrame%          = Special rendering mode selector:
'                            1 = normal rendering
'                            50 = horizontally flipped version
'                            Other values = scaled rendering (value indicates scale factor)

DIM rowText AS STRING
fileName$ = "img/" + LTRIM$(STR$(spriteID%)) + ".i01"
OPEN fileName$ FOR INPUT AS #1
INPUT #1, spriteHeight%
DIM spritePixelRows(1 TO 100)    AS STRING
FOR rowIndex% = 1 TO spriteHeight%
    LINE INPUT #1, spritePixelRows(rowIndex%)
NEXT rowIndex%
CLOSE #1

' Handle special rendering modes based on animation frame parameter
IF animationFrame% = 50 THEN GOTO DrawFlippedSprite

' Normal rendering - draw pixels from top to bottom, left to right
FOR rowIndex% = 1 TO 100
    IF spritePixelRows(rowIndex%) = "" THEN GOTO FinishDrawing
    FOR pixelColumn% = 1 TO LEN(spritePixelRows(rowIndex%))
        ' Convert character digit to numeric color value
        ' ASCII '0'=48, so subtracting 48 gives 0-9 numeric value
        pixelColor% = ASC(RIGHT$(LEFT$(spritePixelRows(rowIndex%), pixelColumn%), 1)) - 48

        ' Only draw non-zero pixels (0 is treated as transparent)
        IF pixelColor% > 0 THEN PSET ((xPosition% + pixelColumn%), (yPosition% + rowIndex%)), pixelColor%
    NEXT pixelColumn%
NEXT rowIndex%
GOTO FinishDrawing

DrawFlippedSprite:
' Horizontally flipped rendering - mirror image for left-facing sprites
FOR rowIndex% = 1 TO 100
    IF spritePixelRows(rowIndex%) = "" THEN GOTO FinishDrawing
    FOR columnIndex% = 1 TO LEN(spritePixelRows(rowIndex%))
        ' Note: X position is mirrored (right-to-left)
        PSET ((x% + (LEN(spritePixelRows(rowIndex%)) - columnIndex% + 1)), (y% + rowIndex%)), ASC(RIGHT$(LEFT$(spritePixelRows(rowIndex%), columnIndex%), 1)) - 48
    NEXT columnIndex%
NEXT rowIndex%

FinishDrawing:
ERASE spritePixelRows
END SUB

