module Worms /* Worms Demonstration Game for the Clean Game Library Version: 3.0 (March 20, 2000) Author: Mike Wiering (mike.wiering@cs.kun.nl) Note: To play Worms with background music, copy your favourite MIDI file to Worms.mid in the music\ subdirectory. */ import StdEnv, StdIO, StdGameDef, StdGame, StdGSt, GameFunctions, Random, notes import WormsGfx /* generated by Tile Studio */ :: GameState = { curlevel :: !Int , maxlevel :: !Int , quit :: !Bool , wormlist :: ![RealXY] , gameover :: !Bool , timecounter :: !Int , randseed :: !RandomSeed } initialGameState = { curlevel = 0 , maxlevel = 7 , quit = False , wormlist = [] , gameover = False , timecounter = 0 , randseed = nullRandomSeed } /* move with speed V */ V = 3.0 /* 24 rem V must be 0 */ /* ---------- main program: load game definition and start the game! ---------- */ Start :: *World -> *World Start world # (seed, world) = getNewRandomSeed world = startGame WormsDemo {initialGameState & randseed = seed} [ScreenSize {w = 640, h = 480}, ColorDepth 16] world /* ---------- the complete game definition ---------- */ WormsDemo :: (Game GameState) WormsDemo = { levels = [ blankScreen , GameLevel1 , blankScreen , GameLevel2 , blankScreen , GameLevel3 , blankScreen ] , quitlevel = accGSt WormsQuitFunction , nextlevel = accGSt WormsNextLevelFunction , textitems = accGSt WormsTextItems } /* if the quit function returns true, the game engine quit the level */ WormsQuitFunction :: GameState -> *(Bool, GameState) WormsQuitFunction gst = (gst.quit, gst) /* function that returns the next level to run, 0 = end game */ WormsNextLevelFunction :: GameState -> *(Int, GameState) WormsNextLevelFunction gst =: {curlevel, maxlevel, gameover} = (nextLevel, {gst & curlevel = nextLevel , quit = False , timecounter = (time nextLevel) }) where nextLevel = if (gameover || (curlevel + 1 > maxlevel)) 0 (curlevel + 1) time l = if (l == maxlevel) 500 (if (l rem 2 == 1) 100 (~1)) /* function that returns text to be displayed */ WormsTextItems :: GameState -> *([GameText], GameState) WormsTextItems gst =: {curlevel, gameover, timecounter, maxlevel} # gst = {gst & timecounter = timecounter - 1} | gst.timecounter == 0 = ([], {gst & quit = True}) | gameover | timecounter < 0 = ([GameOver], {gst & timecounter = 250}) = ([GameOver], gst) = if (curlevel == maxlevel) ([Ending1 timecounter, Ending2 timecounter], gst) (if (curlevel rem 2 == 1) ([LevelStat ((curlevel + 1) / 2)], gst) ([], gst) ) /* ---------- definitions of the levels ---------- */ /* block size */ W :== 24 H :== 24 DEFAULT_SIZE :== {w = W, h = H} /* ---------- levels ---------- */ GameLevel1 = { DefaultLevel & boundmap = Level1Map1BoundMap , layers = [Level1Map1Layer] , music = Just BackgroundMusic } GameLevel2 = { DefaultLevel & boundmap = Level2Map1BoundMap , layers = [Level2Map1Layer] } GameLevel3 = { DefaultLevel & boundmap = Level3Map1BoundMap , layers = [Level3Map1Layer] } DefaultLevel = { blankScreen & initpos = {x = W - 8, y = 0} , objects = ObjectList , music = Nothing , soundsamples = SoundSampleList , leveloptions = { fillbackground = Nothing , escquit = False , debugscroll = False , fadein = False , fadeout = False } } /* ---------- objects ---------- */ /* object codes (corresponds with level map) */ OBJ_WORMHEAD :== 0x10 OBJ_WORMSEGMENT :== 0x11 OBJ_FOOD :== 0x12 OBJ_WALL :== 0x80 /* user events */ EV_STOP :== 0 EV_DIE :== 1 EV_GAMEOVER :== 2 EV_END_LEVEL :== 3 /* ---------- worm object ---------- */ /* worm head sprites */ SPR_WH_UP :== 1 SPR_WH_LEFT :== 2 SPR_WH_DOWN :== 3 SPR_WH_RIGHT :== 4 ObjectList = [GameObjectLS WormHead, GameObjectLS WormSegment, GameObjectLS Food, GameObjectLS Wall] /* bounds */ BND_WORMHEAD :== 0x0001 BND_WORMSEGMENT :== 0x0002 BND_FOOD :== 0x0004 BND_WALL :== 0x0008 :: WormState = { xv :: !Real /* next x-speed */ , yv :: !Real /* next y-speed */ , count :: !Int /* current length of the worm */ , add :: !Int /* number of segments to add when worm eats food */ , more :: !Int /* number of segments that still have to be added */ , lastpoint :: !RealXY /* position of last segment */ , next :: !Int /* next sprite */ } /* layer height, to display some objects in front of others */ NormalLayer = 1 TopLayer = 2 WormHead # obj = defaultGameObject OBJ_WORMHEAD sz wormstate # obj = { obj & sprites = [ WormHeadSprite1 , WormHeadSprite2 , WormHeadSprite3 , WormHeadSprite4 ] , init = (newInit sz wormstate) , keydown = newKeyDown , move = newMove , collide = newCollide , userevent = newUserEvent , touchbound = newTouchBound } = obj where sz = DEFAULT_SIZE wormstate = { xv = V , yv = 0.0 , count = 3 , add = 1 , more = 0 , lastpoint = {rx = 0.0, ry = 0.0} , next = 0 } newInit size state subcode pos time gs # (objrec, gs) = defaultObjectRec subcode pos size time gs # (_, gs) = createObjectFocus zero gs # state = { state & next = SPR_WH_RIGHT } # gs = setwormlist [defSpeed, defSpeed, defSpeed] gs # (_, gs) = createNewGameObject OBJ_WORMSEGMENT 1 {objrec.pos & x = objrec.pos.x - 1 * W} gs # (_, gs) = createNewGameObject OBJ_WORMSEGMENT 2 {objrec.pos & x = objrec.pos.x - 2 * W} gs # lastx = objrec.pos.x - 3 * W lasty = objrec.pos.y # state = {state & lastpoint = {state.lastpoint & rx = toReal lastx, ry = toReal lasty}, more = 0} # (_, gs) = createNewGameObject OBJ_FOOD 0 {x = 0, y = 0} gs # objrec = { objrec & ownbounds = BND_WORMHEAD , collidebounds = BND_WORMSEGMENT + BND_FOOD + BND_STATIC_BOUNDS , offset = {x = ~2, y = ~2} , size = {w = W, h = H} , options = {objrec.options & checkkeyboard = True} , skipmove = 0 , layer = AtLayer TopLayer , currentsprite = SPR_WH_RIGHT } = {st=state, or=objrec, gs=gs} newKeyDown GK_LEFT objst=:{st} = {objst & st = {st & xv = (~V), yv = 0.0, next = SPR_WH_LEFT}} newKeyDown GK_RIGHT objst=:{st} = {objst & st = {st & xv = V, yv = 0.0, next = SPR_WH_RIGHT}} newKeyDown GK_UP objst=:{st} = {objst & st = {st & xv = 0.0, yv = (~V), next = SPR_WH_UP}} newKeyDown GK_DOWN objst=:{st} = {objst & st = {st & xv = 0.0, yv = V, next = SPR_WH_DOWN}} newKeyDown GK_ESCAPE objst = newTouchBound 0 0 objst newKeyDown _ objst = objst newMove objst=:{st, or, gs} # nextspeed = {rx = st.xv, ry = st.yv} # (wormlist, gs) = getwormlist gs # gs = setwormlist (take st.count [nextspeed:wormlist]) gs # or = {or & speed = nextspeed , currentsprite = st.next , skipmove = (24 / (toInt V)) - 1 } | st.more == 0 # last = wormlist!!((length wormlist) - 1) # newx = last.rx * (24.0 / V) + st.lastpoint.rx newy = last.ry * (24.0 / V) + st.lastpoint.ry # st = {st & lastpoint = {rx = newx, ry = newy}} = {st=st, or=or, gs=gs} # (_, gs) = createNewGameObject OBJ_WORMSEGMENT (st.count - st.more) {x = toInt st.lastpoint.rx, y = toInt st.lastpoint.ry} gs # st = {st & more = st.more - 1} = {st=st, or=or, gs=gs} newCollide bounds othertype _ objst=:{st, or, gs} | othertype == OBJ_FOOD # (_, gs) = playSoundSample SND_FOOD (MAX_VOLUME * 7 / 8) PAN_CENTER (getnotefreq (67 + st.add * 2)) 0 gs # (_, gs) = playSoundSample SND_FOOD (MAX_VOLUME * 7 / 8) PAN_CENTER (getnotefreq (71 + st.add * 2)) 2 gs # st = {st & count = st.count + st.add , more = st.more + st.add , add = st.add + 1 } | st.add == 10 # (_,gs) = createUserGameEvent EV_END_LEVEL 0 0 Self ANY_SUBTYPE 3 gs = {st=st, or=or, gs=gs} = {st=st, or=or, gs=gs} | othertype == OBJ_WORMSEGMENT # or = {or & ownbounds = 0, collidebounds = 0} = newTouchBound bounds 0 {st=st, or=or, gs=gs} = {st=st, or=or, gs=gs} newTouchBound _ _ objst=:{st, or, gs} # gs = setgameover True gs # (_,gs) = createUserGameEvent EV_STOP 0 0 AllObjects ANY_SUBTYPE 0 gs # (_,gs) = createUserGameEvent EV_GAMEOVER 0 0 Self ANY_SUBTYPE 350 gs = {st=st, or=or, gs=gs} defSpeed = {rx = V, ry = 0.0} newUserEvent ev par1 par2 objst=:{st, or, gs} | ev == EV_STOP # (_, gs) = playSoundSample SND_HIT MAX_VOLUME PAN_CENTER (getnotefreq 85) 0 gs # (_, gs) = playSoundSample SND_HIT (MAX_VOLUME * 18 / 19) PAN_CENTER (getnotefreq 81) 3 gs # (_, gs) = playSoundSample SND_HIT (MAX_VOLUME * 17 / 19) PAN_CENTER (getnotefreq 77) 6 gs # (_, gs) = playSoundSample SND_HIT (MAX_VOLUME * 16 / 19) PAN_CENTER (getnotefreq 73) 10 gs # (rnd, gs) = Rnd gs # (_, gs) = createUserGameEvent EV_DIE 0 0 Self ANY_SUBTYPE (rnd rem 150) gs = {st=st, or={or & speed = {rx = 0.0, ry = 0.0} , skipmove = ~1 , options = {or.options & checkkeyboard = False} }, gs=gs} | ev == EV_DIE = {st=st, or = {or & currentsprite = SPR_INVISIBLE}, gs=gs} | ev == EV_GAMEOVER = {st=st, or=or, gs=gs} | ev == EV_END_LEVEL = quitlevel {st=st, or=or, gs=gs} = {st=st, or=or, gs=gs} WormSegment # obj = defaultGameObject OBJ_WORMSEGMENT sz Void # obj = { obj & sprites = [WormSegmentSprite1] , init = (newInit sz Void) , move = newMove , userevent = newUserEvent } = obj where sz = DEFAULT_SIZE newInit size state subtype pos time gs # (objrec, gs) = defaultObjectRec subtype pos size time gs # objrec = { objrec & ownbounds = if (subtype > 1) BND_WORMSEGMENT 0 , collidebounds = 0 , skipmove = 0 // toInt (2.0 / V) -1 , layer = AtLayer NormalLayer } = {st=state, or=objrec, gs=gs} newMove objst=:{st, or, gs} # (wormlist, gs) = getwormlist gs # or = { or & skipmove = (24 / (toInt V)) - 1 } | or.subcode + 1 > length wormlist = {st=st, or=or, gs=gs} # or = {or & speed = wormlist!!(or.subcode)} = {st=st, or=or, gs=gs} newUserEvent ev par1 par2 objst=:{st, or, gs} | ev == EV_STOP # (rnd, gs) = Rnd gs # (_,gs) = createUserGameEvent EV_DIE 0 0 Self ANY_SUBTYPE (rnd rem 150) gs # or = {or & speed = {rx = 0.0, ry = 0.0} , skipmove = ~1 , options = {or.options & checkkeyboard = False}} = {st=st, or=or, gs=gs} | ev == EV_DIE # or = {or & currentsprite = SPR_INVISIBLE} = {st=st, or=or, gs=gs} = {st=st, or=or, gs=gs} /* ---------- wall object ---------- */ Wall # obj = defaultGameObject OBJ_WALL sz Void # obj = {obj & init = (newInit sz Void)} = obj where sz = DEFAULT_SIZE newInit size state subcode pos time gs # (objrec, gs) = defaultObjectRec subcode pos size time gs # objrec = { objrec & ownbounds = BND_WALL } = {st=state, or=objrec, gs=gs} /* ---------- food object ---------- */ SPR_INVISIBLE = 0 SPR_VISIBLE = 1 Food # obj = defaultGameObject OBJ_FOOD sz Void # obj = { obj & sprites = [FoodSprite1] , init = (newInit sz Void) , move = newMove , touchbound = newTouchBound , collide = newCollide , userevent = newUserEvent } = obj where sz = DEFAULT_SIZE newInit size state subtype pos time gs # (objrec, gs) = defaultObjectRec subtype pos size time gs # (p, gs) = newPos gs # objrec = { objrec & ownbounds = 0 , collidebounds = BND_WORMHEAD + BND_WORMSEGMENT + BND_WALL , skipmove = 50 , layer = AtLayer NormalLayer , pos = p , currentsprite = SPR_INVISIBLE } = {st=state, or=objrec, gs=gs} newPos gs # (rnd1, gs) = Rnd gs # (rnd2, gs) = Rnd gs = ( { x = W * (2 + (rnd1 rem 24)) , y = H * (1 + (rnd2 rem 18))}, gs) newCollide bnd ot _ objst = newTouchBound bnd 0 objst newTouchBound bnd mapcode objst=:{st, or, gs} # (p, gs) = newPos gs # or = {or & pos = p , ownbounds = 0 , skipmove = 50 , currentsprite = SPR_INVISIBLE } = {st=st, or=or, gs=gs} newMove objst=:{or} # or = {or & currentsprite = SPR_VISIBLE , ownbounds = BND_FOOD , skipmove = ~1 } = {objst & or = or} newUserEvent ev par1 par2 objst=:{st, or, gs} | ev == EV_STOP # (rnd, gs) = Rnd gs # (_,gs) = createUserGameEvent EV_DIE 0 0 Self ANY_SUBTYPE (rnd rem 150) gs # or = {or & speed = {rx = 0.0, ry = 0.0} , skipmove = ~1 , options = {or.options & checkkeyboard = False}} = {st=st, or=or, gs=gs} | ev == EV_DIE # or = {or & currentsprite = SPR_INVISIBLE} = {st=st, or=or, gs=gs} = {st=st, or=or, gs=gs} /* ---------- useful functions for objects ---------- */ /* quit the level */ quitlevel objst=:{st, or, gs} # gs = appGSt setQuit gs = {st=st, or=or, gs=gs} where setQuit :: GameState -> GameState setQuit gst = {gst & quit = True} /* wormlist functions */ setwormlist l gs = appGSt (setgstwormlist l) gs setgstwormlist :: [RealXY] GameState -> GameState setgstwormlist l gst = {gst & wormlist = l} getwormlist gs = accGSt getgstwormlist gs getgstwormlist :: GameState -> *([RealXY], GameState) getgstwormlist gst = (gst.wormlist, gst) /* gameover functions */ setgameover b gs = appGSt (setgstgameover b) gs setgstgameover :: Bool GameState -> GameState setgstgameover b gst = {gst & gameover = b} getgameover gs = accGSt getgstgameover gs getgstgameover :: GameState -> *(Bool, GameState) getgstgameover gst = (gst.gameover, gst) /* ---------- music ---------- */ BackgroundMusic = { musicfile = "music\\Worms.mid" , restart = True , continue = True } /* ---------- text items ---------- */ BigStyle = { fontname = "Arial" , fontsize = 48 , bold = True , italic = False } GameOver = { format = "GAME OVER" , value = Nothing , position = {x = 0, y = 0} , style = BigStyle , color = White , shadow = Just (MakeShadow 3 Grey) , alignment = alignCentered } LevelStat n = { format = "LEVEL %d" , value = Just n , position = {x = 0, y = 0} , style = {BigStyle & fontsize = 32} , color = White , shadow = Just (MakeShadow 2 Grey) , alignment = alignCentered } MakeShadow offset color = { shadowpos = {x = offset, y = offset} , shadowcolor = color } Ending1 n # n = n * 2 = { format = "CONGRATULATIONS!!!" , value = Nothing , position = {x = 0, y = ~30} , style = {BigStyle & fontsize = 34} , color = RGB { r = 240 + (n rem 32) , g = 224 + (n rem 32) * 2 , b = 196 + (n rem 32) * 3} , shadow = Just (MakeShadow 2 Grey) , alignment = alignCentered } Ending2 n = { format = "You have completed all levels!" , value = Nothing , position = {x = 0, y = 30} , style = {BigStyle & fontsize = 28, italic = True} , color = RGB { r = 224 + (n rem 32) , g = 224 + (n rem 32) , b = 224 + (n rem 32) * 2} , shadow = Just (MakeShadow 2 Grey) , alignment = alignCentered } Rnd gs = accGSt gsrand gs gsrand gs=:{randseed} # (x, newseed) = random randseed = (x, {gs & randseed = newseed}) /* ---------- sound samples ---------- */ SND_FOOD :== 1 SND_HIT :== 2 SoundSampleList = [ { soundid = SND_FOOD, soundfile = "sounds\\FOOD.WAV", soundbuffers = 3 } , { soundid = SND_HIT, soundfile = "sounds\\HIT.WAV", soundbuffers = 5 } ]