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 ) )
paul_doe_game.png