News:

Welcome to RetroCoders Community

Main Menu

Paul Doe Game Challenge

Started by ron77, Feb 12, 2023, 02:48 PM

Previous topic - Next topic

ron77

Paul Doe Game Challenge from 2020:

Rules:

1. "Simple graphic game"
2. No global variables.
3. Solo (high score beating) or multiplayer (leaderboard).
4. Players eat small green boxes
5. Green boxes don't move. Eating one will respawn it at another random location.
6. Black boxes are bad.
7. Black boxes move randomly, bouncing around the screen.
8. Eating green boxes makes the player bigger.
9. Getting hit by a black box kills the player. Score is reset to zero and size
is reset to original size.
10. Boxes cannot escape the screen.
Paul Doe solution:

#include once "fbgfx.bi"

const as ulong _
  FONT_WIDTH => 8, _
  FONT_HEIGHT => 16

enum NumberOfPlayers
  One => 1
  Two
  Three
  Four
end enum

enum Colors
  Text       => rgba( 45, 62, 80, 255 )
  TextShadow => rgba( 0, 0, 0, 230 )
  Highlight  => rgba( 244, 244, 244, 255 )
  Background => rgba( 240, 240, 240, 255 )
  Overlay    => rgba( 45, 62, 80, 128 )
  Black      => rgba( 0, 0, 0, 255 )
end enum

enum Difficulty
  Easy => 1
  Hard
end enum

function _
  rangeRnd overload( _
    byval aMin as integer, _
    byval aMax as integer ) _
  as integer
  
  return( int( rnd() * ( ( aMax + 1 ) - aMin ) + aMin ) )
end function

function _
  rangeRnd( _
    byval aMin as double, _
    byval aMax as double ) _
  as double
  
  return( rnd() * ( aMax - aMin ) + aMin )
end function

type _
  Point2
  
  as single _
    x, y
end type

type _
  BoundingBox
  
  declare constructor()
  declare constructor( _
    byval as single, _
    byval as single, _
    byval as single, _
    byval as single )
  
  declare sub _
    centerAt( _
      byval as single, _
      byval as single )
  declare function _
    inside( _
      byval as single, _
      byval as single ) as boolean
  declare function _
    outside( _
      byval as single, _
      byval as single ) as boolean
  declare function _
    overlapsWith( _
      byref as BoundingBox ) as boolean
  declare function _
    insideOf( _
      byref as BoundingBox ) as boolean
  declare function _
    outsideAmount( _
      byref as BoundingBox ) _
    as Point2
    
  as single _
    x, y, _
    width, height
end type

constructor _
  BoundingBox()
end constructor

constructor _
  BoundingBox( _
    byval aX as single, _
    byval aY as single, _
    byval aWidth as single, _
    byval aHeight as single )
  
  x => aX
  y => aY
  this.width => aWidth
  height => aHeight
end constructor

sub _
  BoundingBox.centerAt( _
    byval aX as single, _
    byval aY as single )
  
  dim as single _
    hw => this.width * 0.5!, _
    hh => height * 0.5!
  
  x => aX - hw
  y => aY - hh
end sub

function _
  BoundingBox.inside( _
    byval pX as single, _
    byval pY as single ) as boolean
  
  return( cbool( _
    pX >= x andAlso pX <= x + width - 1 andAlso _
    pY >= y andAlso pY <= y + height - 1 ) )
end function

function _
  BoundingBox.outside( _
    byval pX as single, _
    byval pY as single ) as boolean
  
  return( not inside( pX, pY ) )
end function

function _
  BoundingBox.overlapsWith( _
    byref another as BoundingBox ) as boolean
  
  return( cbool( _
    x + width - 1 >= another.x andAlso _
    y + height - 1 >= another.y andAlso _
    x <= another.x + another.width - 1 andAlso _
    y <= another.y + another.height - 1 ) )
end function

function _
  BoundingBox.insideOf( _
    byref another as BoundingBox ) _
  as boolean
  
  return( cbool( _
    x - another.x >= 0 andAlso _
    x + width - another.x <= another.width andAlso _
    y - another.y >= 0 andAlso _
    y + height - another.y <= another.height ) )
end function

function _
  BoundingBox.outsideAmount( _
    byref bb as BoundingBox ) _
  as Point2
  
  return( type <Point2>( _
    iif( x - bb.x >= 0 andAlso _
      x + width - bb.x <= bb.width, _
        0, iif( bb.x - x < -width, _
          ( bb.x + bb.width ) - ( x + width ), _
          bb.x - x ) ), _
    iif( y - bb.y >= 0 andAlso _
      y + height - bb.y <= bb.height, _
        0, iif( bb.y - y < -height, _
          ( bb.y + bb.height ) - ( y + height ), _
          bb.y - y ) ) ) )
end function

type _
  PlayerControl
  
  as long _
    up, down, left, right
end type

type _
  PlayerControls
  
  declare constructor()
  declare destructor()
  
  as PlayerControl _
    control( 0 to 3 )
end type

constructor _
  PlayerControls()
  
  control( 0 ) => type <PlayerControl>( _
    Fb.SC_UP, Fb.SC_DOWN, Fb.SC_LEFT, Fb.SC_RIGHT )
  control( 1 ) => type <PlayerControl>( _
    Fb.SC_W, Fb.SC_S, Fb.SC_A, Fb.SC_D )
  control( 2 ) => type <PlayerControl>( _
    Fb.SC_T, Fb.SC_G, Fb.SC_F, Fb.SC_H )
  control( 3 ) => type <PlayerControl>( _
    Fb.SC_I, Fb.SC_K, Fb.SC_J, Fb.SC_L )
end constructor

destructor _
  PlayerControls()
end destructor

type _
  Player
  
  declare constructor()
  declare constructor( _
    byval as single, _
    byval as single, _
    byval as ulong )
  declare destructor()
  
  declare sub _
    moveTo( _
      byval as single, _
      byval as single )
  declare sub _
    spawnAt( _
      byref as BoundingBox )
    
  as BoundingBox _
    bb
  as single _
    x, y, _
    size, _
    speed
  as ulong _
    color
end type

constructor _
  Player()
end constructor

constructor _
  Player( _
    byval aSize as single, _
    byval aSpeed as single, _
    byval aColor as ulong )
  
  size => aSize
  speed => aSpeed
  color => aColor
  
  bb.width => aSize
  bb.height => aSize
  
  bb.centerAt( x, y )
end constructor

destructor _
  Player()
end destructor

sub _
  Player.moveTo( _
    byval nX as single, _
    byval nY as single )
  
  x => nX
  y => nY
  
  bb.centerAt( x, y )
end sub

sub _
  Player.spawnAt( _
    byref playArea as BoundingBox )
  
  dim as single _
    hs => size * 0.5!
  
  x => rangeRnd( _
    playArea.x + hs, _
    playArea.x + playArea.width - 1 - hs )
  y => rangeRnd( _
    playArea.y + hs, _
    playArea.y + playArea.height - 1 - hs )
  
  bb.width => size
  bb.height => size
  bb.centerAt( x, y )
end sub

type _
  Enemy
  
  declare constructor()
  declare constructor( _
    byval as single, _
    byval as single, _
    byval as ulong )
  declare destructor()
  
  declare sub _
    moveTo( _
      byval x as single, _
      byval y as single )
  declare sub _
    spawnAt( _
      byref as BoundingBox, _
      byval as single, _
      byval as single )
    
  as BoundingBox _
    bb
  as single _
    x, y, _
    dx, dy, _
    size, _
    speed
  as ulong _
    color
end type

constructor _
  Enemy()
end constructor

constructor _
  Enemy( _
    byval aSize as single, _
    byval aSpeed as single, _
    byval aColor as ulong )
  
  size => aSize
  speed => aSpeed
  color => aColor
  
  bb.width => aSize
  bb.height => aSize
  
  bb.centerAt( x, y )
end constructor

destructor _
  Enemy()
end destructor

sub _
  Enemy.moveTo( _
    byval nX as single, _
    byval nY as single )
  
  x => nX
  y => nY
  
  bb.centerAt( x, y )
end sub

sub _
  Enemy.spawnAt( _
    byref playArea as BoundingBox, _
    byval aMinSpeed as single, _
    byval aMaxSpeed as single )
  
  dim as single _
    hs => size * 0.5!
  
  x => rangeRnd( _
    playArea.x + hs, _
    playArea.x + playArea.width - 1 - hs )
  y => rangeRnd( _
    playArea.y + hs, _
    playArea.y + playArea.height - 1 - hs )
  
  dx => rangeRnd( -1.0!, 1.0! )
  dy => rangeRnd( -1.0!, 1.0! )
  
  dim as single _
    l => 1.0! / sqr( dx ^ 2 + dy ^ 2 )
  
  dx *=> l
  dy *=> l
  
  speed => rangeRnd( aMinSpeed, aMaxSpeed )
  
  bb.width => size
  bb.height => size
  bb.centerAt( x, y )
end sub

type _
  Pellet
  
  declare constructor()
  declare constructor( _
    byval as single, _
    byval as ulong )
  declare destructor()
  
  declare sub _
    moveTo( _
      byval as single, _
      byval as single )
  declare sub _
    spawnAt( _
      byref as BoundingBox )
    
  as BoundingBox _
    bb
  as single _
    x, y, _
    size
  as ulong _
    color
end type

constructor _
  Pellet()
end constructor

constructor _
  Pellet( _
    byval aSize as single, _
    byval aColor as ulong )
  
  size => aSize
  color => aColor
  
  bb.width => aSize
  bb.height => aSize
  
  bb.centerAt( x, y )
end constructor

destructor _
  Pellet()
end destructor

sub _
  Pellet.moveTo( _
    byval nX as single, _
    byval nY as single )
  
  x => nX
  y => nY
  
  bb.centerAt( x, y )
end sub

sub _
  Pellet.spawnAt( _
    byref playArea as BoundingBox )
  
  dim as single _
    hs => size * 0.5!
  
  x => rangeRnd( _
    playArea.x + hs, _
    playArea.x + playArea.width - 1 - hs )
  y => rangeRnd( _
    playArea.y + hs, _
    playArea.y + playArea.height - 1 - hs )
  
  bb.width => size
  bb.height => size
  bb.centerAt( x, y )
end sub

const as long _
  NoPlayer => -1

type _
  LeaderboardEntry
  
  as long _
    score, _
    playerId
end type

type _
  Leaderboard
  
  declare constructor()
  declare constructor( _
    byval as integer )
  declare destructor()
  
  declare sub _
    scoreChanged()
  declare function _
    findEntry( _
      byval as integer ) _
    byref as LeaderboardEntry
  
  as LeaderboardEntry _
    entry( any ), _
    highest
end type

constructor _
  Leaderboard()
  
  constructor( 1 )
end constructor

constructor _
  Leaderboard( _
    byval numPlayers as integer )
  
  redim entry( 0 to numPlayers - 1 )
  
  highest => type <LeaderBoardEntry>( _
    1000, NoPlayer )
end constructor

destructor _
  Leaderboard()
end destructor

sub _
  Leaderboard.scoreChanged()
  
  for _
    i as integer => 0 to ubound( entry )
    
    for _
      j as integer => 0 to ubound( entry ) - ( i + 1 )
      
      if( entry( j ).score < entry( j + 1 ).score ) then
        swap entry( j ), entry( j + 1 )
      end if
    next
    
    if( entry( i ).score > highest.score ) then
      highest.score => entry( i ).score
      highest.playerId => entry( i ).playerId
    end if
  next
end sub

function _
  Leaderboard.findEntry( _
    byval playerId as integer ) _
  byref as LeaderboardEntry
  
  for _
    i as integer => 0 _
    to ubound( entry )
    
    if( entry( i ).playerId = playerId ) then
      return( entry( i ) )
    end if
  next
end function

type _
  Defaults
  
  declare constructor()
  declare destructor()
  
  as ulong _
    playerColor( 0 to 3 ), _
    enemyColor, _
    pelletColor
  as single _
    playerSize, _
    playerSpeed, _
    enemySize, _
    enemyMinSpeed, _
    enemyMaxSpeed, _
    pelletSize
  as Difficulty _
    difficultyLevel
end type

constructor _
  Defaults()
  
  playerColor( 0 ) => rgba( 231, 76, 60, 255 )
  playerColor( 1 ) => rgba( 41, 127, 184, 255 )
  playerColor( 2 ) => rgba( 141, 68, 163, 255 )
  playerColor( 3 ) => rgba( 243, 156, 17, 255 )
  
  enemyColor => rgba( 52, 73, 84, 255 )
  pelletColor => rgba( 46, 204, 113, 255 )
  
  playerSize => 10.0!
  playerSpeed => 150.0!
  enemySize => 10.0!
  enemyMinSpeed => 100.0!
  enemyMaxSpeed => 300.0!
  pelletSize => 10.0!
  difficultyLevel => Difficulty.Easy
end constructor

destructor _
  Defaults()
end destructor

type _
  GameState
  
  declare constructor()
  declare constructor( _
    byval as long, _
    byval as long, _
    byval as long, _
    byref as BoundingBox )
  declare destructor()
  
  as BoundingBox _
    playArea
  
  as PlayerControls _
    controls
  
  as Defaults _
    default
  
  as Leaderboard _
    board
  
  as Player players( any )
  as Enemy enemies( any )
  as Pellet pellets( any )
  
  as long _
    playerCount, _
    enemyCount, _
    pelletCount
end type

constructor _
  GameState()
end constructor

constructor _
  GameState( _
    byval numPlayers as long, _
    byval numEnemies as long, _
    byval numPellets as long, _
    byref aPlayArea as BoundingBox )
  
  playerCount => numPlayers
  enemyCount => numEnemies
  pelletCount => numPellets
  
  redim _
    players( 0 to playerCount - 1 ), _
    enemies( 0 to enemyCount - 1 ), _
    pellets( 0 to pelletCount - 1 )
  
  playArea => aPlayArea
  board => Leaderboard( playerCount )
  
  for _
    i as integer => 0 _
    to playerCount - 1
    
    board.entry( i ).playerId => i
  next
end constructor

destructor _
  GameState()
end destructor

sub _
  grow( _
    byref state as GameState, _
    byref aPlayer as Player, _
    byval amount as single, _
    byval aMax as single )
  
  with aPlayer
    .size => iif( .size < aMax, _
      .size + amount * state.default.difficultyLevel, _
      .size )
    
    .bb.width => .size
    .bb.height => .size
    
    .bb.centerAt( .x, .y )
  end with
end sub

sub _
  pelletEaten( _
    byref state as GameState, _
    byref aPlayer as Player, _
    byval playerIndex as integer, _
    byref whichOne as Pellet )
  
  var byref _
    e => state.board.findEntry( playerIndex )
  
  e.score +=> _
    10 + 2 * ( aPlayer.size / 10.0! ) * _
      state.default.difficultyLevel
    
  state.board.scoreChanged()
  
  grow( _
    state, _
    aPlayer, _
    0.5! * state.default.difficultyLevel, _
    150.0! )
  
  whichOne.spawnAt( state.playArea )
end sub

sub _
  playerKilled( _
    byref state as GameState, _
    byref aPlayer as Player, _
    byval aPlayerId as integer )
  
  var byref _
    e => state.board.findEntry( aPlayerId )
  
  e.score => 0
  
  state.board.scoreChanged()
  
  aPlayer.size => state.default.playerSize
  aPlayer.spawnAt( state.playArea )
end sub

sub _
  updatePlayers( _
    byref state as GameState, _
    byval dt as double )
  
  for _
    i as integer => 0 to state.playerCount - 1
    
    var byref _
      c => state.controls.control( i )
    
    with state.players( i )
      if( multiKey( c.up ) ) then
        .y -=> .speed * dt
      end if
      
      if( multiKey( c.down ) ) then
        .y +=> .speed * dt
      end if
      
      if( multiKey( c.left ) ) then
        .x -=> .speed * dt
      end if
      
      if( multiKey( c.right ) ) then
        .x +=> .speed * dt
      end if
      
      .moveTo( .x, .y )
      
      var _
        offset => .bb.outsideAmount( state.playArea )
      
      .moveTo( .x + offset.x, .y + offset.y )
    end with
  next
end sub

sub _
  updateEnemies( _
    byref state as GameState, _
    byval dt as double )
  
  for _
    i as integer => 0 to state.enemyCount - 1
    
    with state.enemies( i )
      .moveTo( _
        .x + .dx * .speed * dt, _
        .y + .dy * .speed * dt )
      
      var _
        offset => .bb.outsideAmount( state.playArea )
      
      if( offset.x ) then
        .dx => -.dx
      end if
      
      if( offset.y ) then
        .dy => -.dy
      end if
      
      .moveTo( _
        .x + offset.x, _
        .y + offset.y )
      
      for _
        j as integer => 0 to state.playerCount - 1
        
        if( .bb.overlapsWith( state.players( j ).bb ) ) then
          playerKilled( _
            state, _
            state.players( j ), _
            j )
        end if
      next
    end with
  next
end sub

sub _
  updatePellets( _
    byref state as GameState, _
    byval dt as double )
  
  for _
    i as integer => 0 to state.pelletCount - 1
    
    for _
      j as integer => 0 to state.playerCount - 1
      
      var byref _
        p => state.pellets( i )
      
      with state.players( j )
        if( .bb.overlapsWith( p.bb ) ) then
          pelletEaten( _
            state, _
            state.players( j ), _
            j, _
            state.pellets( i ) )
        end if
      end with
    next
  next
end sub

sub _
  update( _
    byref state as GameState, _
    byval dt as double )
  
  updatePlayers( state, dt )
  updateEnemies( state, dt )
  updatePellets( state, dt )
end sub

function _
  alignedCenter( _
    byval x as single, _
    byval w as single ) _
  as single
  
  return( ( w - x ) * 0.5! )
end function

function _
  alignedRight( _
    byval x as single, _
    byval w as single ) _
  as single
  
  return( w - x )
end function

sub _
  renderText( _
    byref text as const string, _
    byval x as single, _
    byval y as single, _
    byval c as ulong )
  
  draw string _
    ( x, y + 1 ), _
    text, Colors.TextShadow
  draw string _
    ( x, y ), _
    text, c
end sub

sub _
  renderBox( _
    byref bb as BoundingBox, _
    byval aColor as ulong )
  
  line _
    ( bb.x, bb.y ) - _
    ( bb.x + bb.width - 1, bb.y + bb.height - 1 ), _
    aColor, bf
end sub

sub _
  renderPlayers( _
    byref state as GameState )
  
  for _
    i as integer => 0 to state.playerCount - 1
    
    with state.players( i )
      renderBox( .bb, .color )
    end with
  next
end sub

sub _
  renderEnemies( _
    byref state as GameState )
  
  for _
    i as integer => 0 to state.enemyCount - 1
    
    with state.enemies( i )
      renderBox( .bb, .color )
    end with
  next
end sub

sub _
  renderPellets( _
    byref state as GameState )
  
  for _
    i as integer => 0 to state.pelletCount - 1
    
    with state.pellets( i )
      renderBox( .bb, .color )
    end with
  next
end sub

sub _
  renderLeaderboard( _
    byref state as GameState )
  
  var _
    lb => BoundingBox( _
      state.playArea.width - 200, _
      state.playArea.y, _
      200, _
      FONT_HEIGHT * state.playerCount + 2 * FONT_HEIGHT )
  
  renderBox( _
    lb, Colors.Overlay )
  
  dim as string _
    msg => "BEST SCORE"
  
  with state.board
    renderText( _
      msg, _
      lb.x + alignedCenter( _
        len( msg ) * FONT_WIDTH, lb.width ), _
      lb.y, _
      Colors.Highlight )
    
    msg => iif( .highest.playerId <> NoPlayer, _
      "PLAYER " & ( .highest.playerId + 1 ) & " ", "" ) + _
      str( .highest.score )
    
    renderText( _
      msg, _
      lb.x + alignedCenter( _
        len( msg ) * FONT_WIDTH, lb.width ), _
      lb.y + FONT_HEIGHT, _
      iif( .highest.playerId <> NoPlayer, _
        state.default.playerColor( .highest.playerId ), _
        Colors.Text ) )
    
    for _
      i as integer => 0 _
      to state.playerCount - 1
      
      with .entry( i )
        msg => "PLAYER " & ( .playerId + 1 )
        
        renderText( _
          msg, _
          lb.x, lb.y + ( i + 2 ) * FONT_HEIGHT, _
          state.default.playerColor( .playerId ) )
        
        msg => str( .score )
        
        renderText( _
          msg, _
          lb.x + alignedRight( _
            len( msg ) * FONT_WIDTH, lb.width ), _
          lb.y + ( i + 2 ) * FONT_HEIGHT, _
          state.default.playerColor( .playerId ) )
      end with
    next
  end with
end sub

sub _
  render( _
    byref state as GameState )
  
  screenLock()
    cls()
    
    renderPellets( state )
    renderEnemies( state )
    renderPlayers( state )
    renderLeaderboard( state )
  screenUnlock()
end sub

function _
  init( _
    byval aWidth as integer, _
    byval aHeight as integer ) _
  as BoundingBox
  
  randomize()
  
  screenRes( _
    aWidth, aHeight, 32, , Fb.GFX_ALPHA_PRIMITIVES )
  color( Colors.Text, Colors.Background )
  width aWidth \ FONT_WIDTH, aHeight \ FONT_HEIGHT
  
  windowTitle( "Pellet Eater" )
  cls()
  
  return( type <BoundingBox>( _
    0, 0, aWidth, aHeight ) )
end function

function _
  initGameFor( _
    byval difficultyLevel as Difficulty, _
    byval numPlayers as NumberOfPlayers, _
    byref playArea as BoundingBox ) _
  as GameState
  
  var _
    state => GameState( _
      numPlayers, _
      10 + ( 5 * ( difficultyLevel - 1 ) ), _
      100, _
      playArea )
    
  with state
    for _
      i as integer => 0 to .playerCount - 1
      
      .players( i ) => Player( _
        .default.playerSize, _
        .default.playerSpeed, _
        .default.playerColor( i ) )
      .players( i ).spawnAt( .playArea )
    next
    
    for _
      i as integer => 0 to .enemyCount - 1
      
      .enemies( i ) => Enemy( _
        .default.enemySize, _
        .default.enemyMinSpeed, _
        .default.enemyColor )
      .enemies( i ).spawnAt( _
        .playArea, _
        .default.enemyMinSpeed, _
        .default.enemyMaxSpeed )
    next
    
    for _
      i as integer => 0 to .pelletCount - 1
      
      .pellets( i ) => Pellet( _
        .default.pelletSize, _
        .default.pelletColor )
      .pellets( i ).spawnAt( .playArea )
    next
    
    .default.difficultyLevel => difficultyLevel
  end with
  
  return( state )
end function

var _
  playArea => init( 800, 600 ), _
  state => initGameFor( _
    Difficulty.Easy, NumberOfPlayers.Four, playArea )
  
dim as double _
  dt

do
  update( state, dt )
  
  dt => timer()
  render( state )
  
  sleep( 1, 1 )
  dt => timer() - dt
loop until( multiKey( Fb.SC_ESCAPE ) )

You cannot view this attachment.