<> <> DIRECTORY Convert USING [ValueToRope], Graphics USING [ black, Box, Context, DrawBox, DrawChar, DrawRope, FontRef, GetBounds, MakeFont, SetColor, SetCP, SetPaintMode, Translate, white], GraphicsOps USING [BitmapRef, DrawBitmap, NewBitmap, NewContextFromBitmap], GVNames USING [GetConnect], Icons USING [DrawIcon, IconFlavor, NewIconFromFile], InputFocus USING [SetInputFocus], IO USING [ Close, CreateOutputStreamToRope, GetOutputStreamRope, Handle, int, Put, PutF, rope, STREAM, Value], Menus USING [AppendMenuEntry, ClickProc, CreateEntry, CreateMenu, Menu], MessageWindow USING [Append, Blink], Process USING [Detach, SetTimeout], PupDefs USING [GetLocalPupAddress, PupAddress], Real USING [RoundI], RealFns USING [SinDeg, CosDeg], Rope USING [Cat, ROPE], RPC USING [CallFailed, ImportFailed], TankInternal USING [StartServer, StopServer], TankMaster USING [ AddPlayer, GetPlayer, Machine, maxPlayers, RemovePlayer, Tank, Torp, ScoreHit, Update, WorldState, WorldStateRec], TankMasterRpcControl USING [ImportInterface, UnimportInterface], TIPUser USING [InstantiateNewTIPTable, TIPTable], UserExec USING [AcquireStreams, CommandProc, ExecHandle, GetExecHandle, GetNameAndPassword, RegisterCommand, ReleaseStreams], ViewerClasses USING [ DestroyProc, ModifyProc, NotifyProc, PaintProc, ScrollProc, Viewer, ViewerClass, ViewerClassRec], ViewerOps USING [ BlinkIcon, CreateViewer, InvisiblePaint, PaintViewer, RegisterViewerClass, SetMenu], VFonts USING [EstablishFont, GraphicsFont]; TankViewer: CEDAR MONITOR IMPORTS Convert, Graphics, GraphicsOps, GVNames, InputFocus, Icons, IO, Menus, MessageWindow, Process, PupDefs, Real, RealFns, Rope, RPC, TankInternal, TankMasterRpcControl, TankMaster, TIPUser, UserExec, ViewerOps, VFonts SHARES ViewerOps = BEGIN OPEN IO, Rope, TankMaster; tank1, tank2: ViewerClasses.Viewer _ NIL; menu: Menus.Menu; tankFont: Graphics.FontRef; timesRoman: Graphics.FontRef; tankIcon, treadIcon: Icons.IconFlavor _ document; Initialize: PROCEDURE = BEGIN tankFont _ Graphics.MakeFont["tankFont.strike"]; timesRoman _ VFonts.GraphicsFont[VFonts.EstablishFont["TimesRoman", 10]]; <> tankIcon _ Icons.NewIconFromFile["Tank.icons", 0]; FOR i: CARDINAL IN [1..turrets] DO [] _ Icons.NewIconFromFile["Tank.icons", i]; ENDLOOP; treadIcon _ Icons.NewIconFromFile["Tank.icons", turrets+1]; FOR i: CARDINAL IN [turrets+2..turrets+3] DO [] _ Icons.NewIconFromFile["Tank.icons", i]; ENDLOOP; menu _ Menus.CreateMenu[]; Menus.AppendMenuEntry[menu, Menus.CreateEntry["Help", Help]]; Menus.AppendMenuEntry[menu, Menus.CreateEntry["Players", Players]]; InitWalls[]; MakeTankViewerClass[]; UserExec.RegisterCommand["Tank", Create, "Tank arcade game."]; END; <<******************************************************************>> <> <<******************************************************************>> MakeTankViewerClass: PROCEDURE = BEGIN tipTable: TIPUser.TIPTable _ TIPUser.InstantiateNewTIPTable["Tank.tip"]; viewerClass: ViewerClasses.ViewerClass _ NEW [ViewerClasses.ViewerClassRec _ [paint: PaintMe, -- called whenever the Viewer should repaint notify: TipMe, -- TIP input events modify: Noop, -- InputFocus changes reported through here destroy: DestroyMe, -- called before Viewer structures freed on destroy op copy: NIL, -- copy data to new Viewer set: NIL, -- set the viewer contents get: NIL, -- get the viewer contents init: NIL, -- called on creation or reset to init data save: NIL, -- requests client to write contents to disk scroll: ScrollMe, -- document scrolling icon: document, -- picture to display when small tipTable: tipTable, -- could be moved into Viewer instance if needed cursor: crossHairsCircle -- standard cursor when mouse is in viewer ]]; ViewerOps.RegisterViewerClass[$TankViewer, viewerClass]; END; Noop: ViewerClasses.ModifyProc = {}; TipMe: ViewerClasses.NotifyProc = BEGIN tankData: TankData _ NARROW[self.data, TankData]; <<[self: Viewer, input: LIST OF REF ANY]>> <> InputFocus.SetInputFocus[self]; {OPEN tankData; FOR input _ input, input.rest DO IF input = NIL THEN EXIT; SELECT input.first FROM <> $Forward => me.move _ forward; $HalfSpeed => me.move _ halfspeed; -- halfspeed $Stop => me.move _ stop; $Reverse => me.move _ reverse; <> $Left => me.turn _ left; $SlowLeft => me.turn _ slowLeft; $NotLeft => IF me.turn = left OR me.turn = slowLeft THEN me.turn _ straight; $Right => me.turn _ right; $SlowRight => me.turn _ slowRight; $NotRight => IF me.turn = right OR me.turn = slowRight THEN me.turn _ straight; <> $Shoot => IF myTorp.state = 0 THEN { myTorp.x _ me.x; myTorp.y _ me.y; [myTorp.dx, myTorp.dy] _ Delta[me.angle, torpSpeed]; myTorp.state _ 50}; ENDCASE; ENDLOOP}; END; DestroyMe: ViewerClasses.DestroyProc = TRUSTED { Process.Detach[FORK Remove[NARROW[self.data, TankData]]]}; Remove: PROC[tankData: TankData] = BEGIN tankData.quit _ TRUE; WHILE tankData.quit DO Wait[tankData]; ENDLOOP; TRUSTED {TankMaster.RemovePlayer[tankData.me.id ! RPC.CallFailed => CONTINUE]}; TankInternal.StopServer[]; END; ScrollMe: ViewerClasses.ScrollProc = BEGIN -- self: Viewer, amount: INTEGER, op: {up, down, thumb} tankData: TankData _ NARROW[self.data, TankData]; {OPEN tankData; SELECT op FROM up => offset _ MIN[-vy1 + 8, offset + amount]; down => offset _ MAX[self.ch - vy2 + 8, offset - amount]; < offset _ (amount*vy2)/self.wh;>> ENDCASE => RETURN; ViewerOps.PaintViewer[self, client]; }; END; PaintMe: ViewerClasses.PaintProc = BEGIN -- self: Viewer, context: Graphics.Context, whatChanged: REF ANY, tankData: TankData _ NARROW[self.data, TankData]; {OPEN tankData; score: ROPE; box: Graphics.Box; IF self.iconic THEN {PaintIcon[tankData, context]; RETURN}; Graphics.Translate[context, 0, offset]; IF clear THEN { [] _ Graphics.SetPaintMode[context, opaque]; [] _ Graphics.SetColor[context, Graphics.white]; Graphics.DrawBox[context, Graphics.GetBounds[context]]; [] _ Graphics.SetColor[context, Graphics.black]; -- IF HitWall[Real.RoundI[me.x], Real.RoundI[me.y], 13, tankData] -- THEN RandomPosition[tankData]; <> FOR i: CARDINAL IN [0..wallLength) DO IF LastWall[i] THEN EXIT; box.xmin _ wall[i].x1; box.xmax _ wall[i].x2; box.ymin _ wall[i].y1 - 8; box.ymax _ wall[i].y2 - 8; Graphics.DrawBox[context, box]; ENDLOOP; <> FOR i: CARDINAL IN [0..mineLength) DO IF mine[i] = [0,0] THEN EXIT; Graphics.SetCP[context, mine[i].x, mine[i].y]; Graphics.DrawChar[context, 132C, tankFont]; ENDLOOP; <> FOR i: CARDINAL IN [0..maxPlayers) DO IF enemy.tank[i].angle > 360 THEN LOOP; DrawTank[context, enemy.tank[i], i = me.id]; ENDLOOP; <> FOR i: CARDINAL IN [0..maxPlayers) DO IF enemy.torp[i].state = 0 THEN LOOP; DrawTorp[context, enemy.torp[i]]; ENDLOOP}; <> IF NOT clear THEN { FOR i: CARDINAL IN [0..maxPlayers) DO IF enemy.tank[i].angle = last.tank[i].angle AND enemy.tank[i].x = last.tank[i].x AND enemy.tank[i].y = last.tank[i].y AND enemy.tank[i].dead = last.tank[i].dead THEN LOOP; [] _ Graphics.SetColor[context, Graphics.white]; DrawTank[context, last.tank[i], i = me.id]; IF enemy.tank[i].angle > 360 THEN LOOP; [] _ Graphics.SetColor[context, Graphics.black]; DrawTank[context, enemy.tank[i], i = me.id]; ENDLOOP; FOR i: CARDINAL IN [0..maxPlayers) DO IF enemy.torp[i].state = last.torp[i].state AND enemy.torp[i].x = last.torp[i].x AND enemy.torp[i].y = last.torp[i].y THEN LOOP; [] _ Graphics.SetColor[context, Graphics.white]; DrawTorp[context, last.torp[i]]; [] _ Graphics.SetColor[context, Graphics.black]; DrawTorp[context, enemy.torp[i]]; ENDLOOP}; IF repaint THEN { -- erase old score box.xmin _ wall[14].x1; box.xmax _ wall[14].x2; box.ymin _ wall[14].y1 - 8; box.ymax _ wall[14].y2 - 8; Graphics.DrawBox[context, box]}; IF clear OR repaint THEN {-- draw new Graphics.SetColor[context, Graphics.white]; score _ PutFToRope["Self: %g, Enemy: %g", IO.int[scored], IO.int[destroyed]]; Graphics.SetCP[context, wall[14].x1+5, wall[14].y1-4]; Graphics.DrawRope[context, score, , , timesRoman]; repaint _ FALSE}}; END; turrets: CARDINAL = 12; treadPause: CARDINAL _ 6; turretPause: CARDINAL _ 3; turretStop: CARDINAL _ 100; PaintIcon: PROCEDURE[data: TankData, context: Graphics.Context] = BEGIN oldTread: CARDINAL _ data.tread; oldTurret: CARDINAL _ data.turret; <> IF data.treadPause # 0 THEN data.treadPause _ data.treadPause - 1; IF data.treadPause = 0 THEN { data.tread _ (data.tread + 1) MOD 3; data.treadPause _ treadPause}; IF data.turretPause # 0 THEN data.turretPause _ data.turretPause - 1; IF data.turretPause = 0 THEN IF data.turretRight THEN { data.turretPause _ turretPause; data.turret _ data.turret + 1; IF data.turret = turrets THEN {data.turretPause _ turretStop; data.turretRight _ FALSE}} ELSE { data.turretPause _ turretPause; data.turret _ data.turret - 1; IF data.turret = 0 THEN {data.turretPause _ turretStop; data.turretRight _ TRUE}}; <> IF data.tread = oldTread AND data.turret = oldTurret THEN RETURN; [] _ Graphics.SetPaintMode[data.ctx, opaque]; Icons.DrawIcon[LOOPHOLE[LOOPHOLE[tankIcon, CARDINAL] + data.turret], data.ctx]; [] _ Graphics.SetPaintMode[data.ctx, transparent]; Icons.DrawIcon[LOOPHOLE[LOOPHOLE[treadIcon, CARDINAL] + data.tread], data.ctx]; Graphics.SetCP[context, 0, 64]; GraphicsOps.DrawBitmap[context, data.icon, 64, 64]; END; PutFToRope: PROC [format: ROPE _ NIL, v1, v2, v3, v4, v5: Value _ [null[]]] RETURNS [r: ROPE] = INLINE {h: Handle _ CreateOutputStreamToRope[]; h.PutF[format, v1, v2, v3, v4, v5]; r _ h.GetOutputStreamRope[]; h.Close[]}; DrawTank: PROCEDURE[context: Graphics.Context, tank: Tank, me: BOOLEAN] = BEGIN char: CHARACTER; IF context = NIL THEN RETURN; char _ LOOPHOLE[tank.angle/4]; IF me THEN char _ char + 96; Graphics.SetCP[context, tank.x, tank.y]; Graphics.DrawChar[context, char, tankFont]; END; DrawTorp: PROCEDURE[context: Graphics.Context, torp: Torp] = BEGIN char: CHARACTER; IF context = NIL THEN RETURN; char _ SELECT torp.state FROM 4 => 134C, 3 => 135C, 2 => 136C, 1 => 137C, 0 => 000C, ENDCASE => 133C; IF char = 000C THEN RETURN; Graphics.SetCP[context, torp.x, torp.y]; Graphics.DrawChar[context, char, tankFont]; END; <<>> <<******************************************************************>> <> <<******************************************************************>> -- my state -- TankData: TYPE = REF TankDataRec; TankDataRec: TYPE = RECORD[ user: CONDITION, quit: BOOL _ FALSE, offset: INTEGER _ 0, destroyed: INTEGER _ 0, scored: INTEGER _ 0, icon: GraphicsOps.BitmapRef, ctx: Graphics.Context, turret: CARDINAL _ 0, turretPause: CARDINAL _ 3, tread: CARDINAL _ 0, treadPause: CARDINAL _ 3, repaint: BOOLEAN _ FALSE, turretRight: BOOLEAN _ TRUE, me: MyTank _ [], myTorp: MyTorp _ [], last: WorldState _ NIL, enemy: WorldState _ NIL]; MyTank: TYPE = RECORD[ id: CARDINAL _ 0, state: TankState _ alive, time: CARDINAL _ 0, -- time remaining in current state angle: INTEGER _ 0, -- current facing x, y: REAL _ 0, -- current position turn: Turn _ straight, -- turning direction move: Move _ stop]; -- moving direction MyTorp: TYPE = RECORD[ state: CARDINAL _ 0, -- [50..5] = fired, [4..1] = exploding, 0 = loaded x, y: REAL _ 0, -- current position dx, dy: REAL _ 0]; -- delta to new position TankState: TYPE = {alive, dead}; Turn: TYPE = {left, slowLeft, straight, slowRight, right}; Move: TYPE = {forward, halfspeed, stop, reverse}; -- procedures -- Create: UserExec.CommandProc = BEGIN id: CARDINAL; tankData: TankData; success: BOOLEAN _ FALSE; viewer: ViewerClasses.Viewer; myAddr: PupDefs.PupAddress; IF ~bound THEN Bind[]; TRUSTED {myAddr _ PupDefs.GetLocalPupAddress[[0,0], NIL]}; TRUSTED {[success, id] _ TankMaster.AddPlayer[ UserExec.GetNameAndPassword[].name, [myAddr.net, myAddr.host] ! RPC.CallFailed => {Bind[]; RETRY}]}; IF ~success THEN RETURN[FALSE, "Sorry, game is already full."]; tankData _ NEW[TankDataRec _ []]; tankData.last _ NEW[WorldStateRec _ []]; tankData.enemy _ NEW[WorldStateRec _ []]; tankData.icon _ GraphicsOps.NewBitmap[64,64]; tankData.ctx _ GraphicsOps.NewContextFromBitmap[tankData.icon]; tankData.me.id _ id; viewer _ ViewerOps.CreateViewer[ flavor: $TankViewer, info: [ name: "Tank", icon: private, iconic: FALSE, data: tankData, scrollable: TRUE]]; ViewerOps.SetMenu[viewer, menu]; RandomPosition[tankData]; TRUSTED {Process.SetTimeout[@tankData.user, 1]}; TRUSTED {Process.Detach[FORK Main[viewer, tankData]]}; END; bound, rebind: BOOL _ FALSE; Bind: PROC[wait: INTEGER _ 1] = BEGIN InternalBind: ENTRY PROC = BEGIN IF bound AND rebind THEN { -- only want to rebind once per error. TankMasterRpcControl.UnimportInterface[]; MessageWindow.Append["Please wait-- binding to new Tank server.", TRUE]; MessageWindow.Blink[]; rebind _ bound _ FALSE}; IF bound THEN RETURN; WHILE TRUE DO bound _ TRUE; IF wait <= 0 THEN TankInternal.StartServer[]; TankMasterRpcControl.ImportInterface [ interfaceName: [instance: "TankMaster.auto"] ! RPC.ImportFailed => {bound _ FALSE; CONTINUE}]; IF bound THEN RETURN; IF wait > 0 THEN wait _ wait - 1 ELSE ERROR; ENDLOOP; END; IF bound THEN rebind _ TRUE; InternalBind[]; END; Rank: PROC[world: WorldState _ NIL, me: NAT] RETURNS[rank: INTEGER _ 0] = BEGIN myAddress: Machine; IF world = NIL THEN RETURN[1]; myAddress _ world.address[me]; FOR i: NAT IN [0..world.players) DO IF ~world.tank[i].playing THEN LOOP; IF world.address[i].net > myAddress.net THEN rank _ rank+1; IF world.address[i].net = myAddress.net THEN { IF world.address[i].host > myAddress.host THEN rank _ rank+1; IF world.address[i].host = myAddress.host AND i >= me THEN rank _ rank+1}; ENDLOOP; END; <<>> Help: Menus.ClickProc = BEGIN stream: IO.STREAM; exec: UserExec.ExecHandle; exec _ UserExec.GetExecHandle["A"]; stream _ UserExec.AcquireStreams[exec].out; stream.Put[rope["\nTank commands: \n"]]; stream.Put[rope[" MouseLeft, S => Left.\n"]]; stream.Put[rope[" MouseMiddle, D => Forward.\n"]]; stream.Put[rope[" MouseRight, F => Right.\n"]]; stream.Put[rope[" Space => Fire.\n"]]; stream.Put[rope[" A => Reverse.\n"]]; stream.Put[rope[" SHIFT => Turn at quarter speed.\n"]]; UserExec.ReleaseStreams[exec]; END; Players: Menus.ClickProc = BEGIN score: INTEGER; address: Machine; stream: IO.STREAM; addressRope: ROPE; name, server: ROPE; exec: UserExec.ExecHandle; Octal: PROC[int: INT] RETURNS[rope: Rope.ROPE] = INLINE { RETURN[Convert.ValueToRope[[signed[int, 8]]]]}; exec _ UserExec.GetExecHandle["A"]; stream _ UserExec.AcquireStreams[exec].out; server _ GVNames.GetConnect["TankMaster.auto"].connect; stream.Put[rope["\nCurrent Tank players: \n"]]; FOR i: CARDINAL IN [0..maxPlayers) DO TRUSTED {[name, address, score] _ TankMaster.GetPlayer[i ! RPC.CallFailed => {Bind[]; RETRY}]}; IF name = NIL THEN LOOP; stream.Put[rope["\t"], rope[name], rope[" ("]]; addressRope _ Rope.Cat[Octal[address.net], "#", Octal[address.host], "#): "]; stream.Put[rope[addressRope], int[score], rope[".\n"]]; ENDLOOP; stream.Put[rope["Server: "], rope[server], rope[".\n"]]; UserExec.ReleaseStreams[exec]; END; <<******************************************************************>> <> <<******************************************************************>> wait: BOOLEAN _ TRUE; -- setting wait to FALSE lets the Spy measure things Wait: ENTRY PROCEDURE[data: TankData] = {IF wait THEN WAIT data.user}; Main: PROCEDURE[viewer: ViewerClasses.Viewer, data: TankData] = BEGIN OPEN data; ENABLE ViewerOps.InvisiblePaint => CONTINUE; tank: Tank; torp: Torp; paintAll: BOOLEAN; DO Wait[data]; IF data.quit THEN {data.quit _ FALSE; EXIT}; <> paintAll _ FALSE; SELECT me.state FROM alive => MoveTank[data]; dead => { me.time _ me.time - 1; IF me.time = 0 THEN { me.state _ alive; RandomPosition[data]; paintAll _ TRUE}}; ENDCASE; IF myTorp.state # 0 THEN MoveTorp[data]; <> tank _ [FALSE, TRUE, me.angle, Real.RoundI[me.x], Real.RoundI[me.y]]; torp _ [myTorp.state, Real.RoundI[myTorp.x], Real.RoundI[myTorp.y]]; last^ _ enemy^; TRUSTED {enemy^ _ TankMaster.Update[me.id, tank, torp ! RPC.CallFailed => {Bind[Rank[last, me.id]-1]; RETRY}]}; IF enemy.tank[me.id].dead THEN {-- I must have been hit destroyed _ destroyed + 1; me.state _ dead; me.time _ 5}; IF viewer.iconic AND paintAll THEN ViewerOps.BlinkIcon[viewer]; ViewerOps.PaintViewer[ viewer: viewer, hint: IF viewer.iconic THEN all ELSE client, clearClient: paintAll]; ENDLOOP; END; -- general procedures -- tankSpeed: INTEGER = 6; tankWidth: INTEGER = 6; torpSpeed: INTEGER = 20; torpWidth: INTEGER = 2; mineWidth: INTEGER = 6; MoveTank: PROC[td: TankData] = BEGIN t, min: REAL _ 1; dx, dy: REAL; x, y, newX, newY: INTEGER; hit: {tank, mine, wall}; <> SELECT td.me.turn FROM left => td.me.angle _ Mod[(td.me.angle + 4), 360]; slowLeft => td.me.angle _ Mod[(td.me.angle + 1), 360]; slowRight => td.me.angle _ Mod[(td.me.angle - 1), 360]; right => td.me.angle _ Mod[(td.me.angle - 4), 360]; ENDCASE; IF td.me.move = stop THEN RETURN; SELECT td.me.move FROM forward => [dx, dy] _ Delta[td.me.angle, tankSpeed]; halfspeed => [dx, dy] _ Delta[td.me.angle, (tankSpeed+1)/2]; reverse => [dx, dy] _ Delta[td.me.angle, -(tankSpeed+1)/2]; stop => RETURN; ENDCASE; x _ Real.RoundI[td.me.x + 8]; y _ Real.RoundI[td.me.y + 8]; newX _ Real.RoundI[td.me.x + 8 + dx]; newY _ Real.RoundI[td.me.y + 8 + dy]; <> FOR i: CARDINAL IN [0..mineLength) DO IF mine[i] = [0,0] THEN EXIT; t _ Intersect[x, y, newX, newY, tankWidth, mine[i].x+8-mineWidth, mine[i].y+8-mineWidth, mine[i].x+8+mineWidth, mine[i].y+8+mineWidth]; IF t < min THEN {min _ t; hit _ mine}; ENDLOOP; <> FOR i: CARDINAL IN [0..maxPlayers) DO IF i = td.me.id THEN LOOP; -- never hit yourself! IF td.enemy.tank[i].angle > 360 THEN LOOP; t _ Intersect[x, y, newX, newY, tankWidth, td.enemy.tank[i].x+8-tankWidth, td.enemy.tank[i].y+8-tankWidth, td.enemy.tank[i].x+8+tankWidth, td.enemy.tank[i].y+8+tankWidth]; IF t < min THEN {min _ t; hit _ tank}; ENDLOOP; <> FOR i: CARDINAL IN [0..wallLength) DO IF LastWall[i] THEN EXIT; t _ Intersect[x, y, newX, newY, tankWidth, wall[i].x1, wall[i].y1, wall[i].x2, wall[i].y2]; IF t < min THEN {min _ t; hit _ wall}; ENDLOOP; <> IF min < 1 THEN { dx _ min*dx; dy _ min*dy; IF hit = mine THEN TRUSTED {TankMaster.ScoreHit[maxPlayers, td.me.id ! RPC.CallFailed => {Bind[]; RETRY}]}; <> <> }; td.me.x _ td.me.x + dx; td.me.y _ td.me.y + dy; END; MoveTorp: PROCEDURE[td: TankData] = BEGIN tankID: INTEGER; t, min: REAL _ 1; hit: {tank, wall} _ wall; dx, dy: REAL; x, y, newX, newY: INTEGER; td.myTorp.state _ td.myTorp.state - 1; IF td.myTorp.state IN [0..4] THEN RETURN; dx _ td.myTorp.dx; dy _ td.myTorp.dy; x _ Real.RoundI[td.myTorp.x + 8]; y _ Real.RoundI[td.myTorp.y + 8]; -- hot point is the lower right hand corner newX _ Real.RoundI[td.myTorp.x + 8 + td.myTorp.dx]; newY _ Real.RoundI[td.myTorp.y + 8 + td.myTorp.dy]; <> FOR i: CARDINAL IN [0..maxPlayers) DO IF i = td.me.id THEN LOOP; -- never hit yourself! IF td.enemy.tank[i].angle > 360 THEN LOOP; t _ Intersect[x, y, newX, newY, torpWidth, td.enemy.tank[i].x+8-tankWidth, td.enemy.tank[i].y+8-tankWidth, td.enemy.tank[i].x+8+tankWidth, td.enemy.tank[i].y+8+tankWidth]; IF t < min THEN {min _ t; hit _ tank; tankID _ i}; ENDLOOP; <> FOR i: CARDINAL IN [0..wallLength) DO IF LastWall[i] THEN EXIT; t _ Intersect[x, y, newX, newY, torpWidth, wall[i].x1, wall[i].y1, wall[i].x2, wall[i].y2]; IF t < min THEN {min _ t; hit _ wall}; ENDLOOP; <> IF min < 1 THEN { dx _min*dx; dy _ min*dy; IF hit = tank THEN { TRUSTED {TankMaster.ScoreHit[td.me.id, tankID ! RPC.CallFailed => {Bind[]; RETRY}]}; td.scored _ td.scored + 1; td.repaint _ TRUE}; td.myTorp.state _ 4}; td.myTorp.x _ td.myTorp.x + dx; td.myTorp.y _ td.myTorp.y + dy; END; Intersect: PROC[x1, y1, x2, y2, w: INTEGER, tx1, ty1, tx2, ty2: INTEGER] RETURNS[nt: REAL] = BEGIN t, x, y: REAL; bx1, by1, bx2, by2: REAL; nt _ t _ 1; <> IF MIN[x1, x2]-w > tx2 THEN RETURN; IF MIN[y1, y2]-w > ty2 THEN RETURN; IF MAX[x1, x2]+w < tx1 THEN RETURN; IF MAX[y1, y2]+w < ty1 THEN RETURN; <> bx1 _ tx1-w; bx2 _ tx2+w; by1 _ ty1-w; by2 _ ty2+w; <> SELECT TRUE FROM x1 = x2 => NULL; x1 < x2 AND x1 <= bx1 AND bx1 <= x2 => { t _ (bx1-x1)/(x2-x1); y _ y1 + t*(y2-y1); IF by1 <= y AND y < by2 THEN nt _ MIN[nt, t]}; x1 > x2 AND x2 <= bx2 AND bx2 <= x1 => { t _ (bx2-x1)/(x2-x1); y _ y1 + t*(y2-y1); IF by1 <= y AND y < by2 THEN nt _ MIN[nt, t]}; ENDCASE; <> SELECT TRUE FROM y1 = y2 => NULL; y1 < y2 AND y1 <= by1 AND by1 <= y2 => { t _ (by1-y1)/(y2-y1); x _ x1 + t*(x2-x1); IF bx1 <= x AND x < bx2 THEN nt _ MIN[nt, t]}; y1 > y2 AND y2 <= by2 AND by2 <= y1 => { t _ (by2-y1)/(y2-y1); x _ x1 + t*(x2-x1); IF bx1 <= x AND x < bx2 THEN nt _ MIN[nt, t]}; ENDCASE; x _ 0; -- a place to set a break point END; Mod: PROCEDURE[a, b: INTEGER] RETURNS[c: INTEGER] = INLINE {c _ a MOD b; IF c < 0 THEN c _ c + b}; RandomPosition: PROCEDURE[data: TankData] = BEGIN OPEN data; x, y: INTEGER; DO [x, y] _ RandomPoint[]; IF x <= vx1+w OR x >= vx2-w-10 THEN LOOP; IF y <= vy1+w OR y >= vy2-w-10 THEN LOOP; IF HitWall[x, y, 13, data] THEN LOOP; IF HitTank[x, y, 13, data] THEN LOOP; IF HitMine[x, y, 13] THEN LOOP; me.x _ x; me.y _ y; EXIT; ENDLOOP; END; HitWall: PROCEDURE[x, y: INTEGER, width: INTEGER, tankData: TankData] RETURNS[BOOLEAN] = BEGIN x _ x + (15 - width)/2; y _ y + (15 - width)/2; <= viewer.cw - 5 OR y <= tankData.offset >> <= viewer.cw - 30 + tankData.offset THEN RETURN[TRUE];>> FOR i: CARDINAL IN [0..wallLength) DO IF LastWall[i] THEN EXIT; IF x > wall[i].x2 THEN LOOP; IF y > wall[i].y2 THEN LOOP; IF x + width < wall[i].x1 THEN LOOP; IF y + width < wall[i].y1 THEN LOOP; RETURN[TRUE]; ENDLOOP; RETURN[FALSE]; END; HitMine: PROCEDURE[x, y: INTEGER, width: INTEGER] RETURNS[BOOLEAN] = BEGIN x _ x + (15 - width)/2; y _ y + (15 - width)/2; FOR i: CARDINAL IN [0..mineLength) DO IF mine[i] = [0,0] THEN EXIT; IF x > mine[i].x+8+mineWidth THEN LOOP; IF y > mine[i].y+8+mineWidth THEN LOOP; IF x + width < mine[i].x+8-mineWidth THEN LOOP; IF y + width < mine[i].y+8-mineWidth THEN LOOP; RETURN[TRUE]; ENDLOOP; RETURN[FALSE]; END; HitTank: PROCEDURE[x, y: INTEGER, width: INTEGER, data: TankData] RETURNS[BOOLEAN] = BEGIN OPEN data; x _ x + (15 - width)/2; y _ y + (15 - width)/2; FOR i: CARDINAL IN [0..maxPlayers) DO IF i = me.id THEN LOOP; -- never hit yourself! IF enemy.tank[i].angle > 360 THEN LOOP; IF x > enemy.tank[i].x+8+tankWidth THEN LOOP; IF y > enemy.tank[i].y+8+tankWidth THEN LOOP; IF x + width < enemy.tank[i].x+8-tankWidth THEN LOOP; IF y + width < enemy.tank[i].y+8-tankWidth THEN LOOP; RETURN[TRUE]; ENDLOOP; RETURN[FALSE]; END; RandomPoint: PUBLIC PROCEDURE RETURNS[x, y: INTEGER] = INLINE BEGIN c: INTEGER _ 7; randomX _ Mod[randomX*c, 606]; randomY _ Mod[randomY*c, 808]; RETURN[randomX, randomY]; END; Delta: PROCEDURE[angle, d:INTEGER] RETURNS[x, y: REAL] = --0 deg is =>, 90 is ^ INLINE BEGIN x _ RealFns.CosDeg[angle]*d; y _ RealFns.SinDeg[angle]*d; END; InitWalls: PROCEDURE = BEGIN wall _ [[117, 292, 133, 404], [133, 86, 148, 140], [176, 371, 193, 518], [178, 167, 192, 280], [314, 678, 333, 784], [453, 379, 470, 536], [476, 169, 496, 237], [286, 87, 408, 105], [ 89, 124, 133, 140], [192, 167, 292, 181], [391, 169, 476, 183], [483, 297, 590, 318], [193, 500, 268, 518], [382, 520, 453, 536], [256, 583, 396, 600], [106, 642, 221, 662], [430, 649, 556, 665], [vx1, vy1, vx1+w, vy2], [vx1, vy1, vx2, vy1+w], [vx2-w, vy1, vx2, vy2], [vx1, vy2-w, vx2, vy2]]; END; LastWall: PROCEDURE[i: CARDINAL] RETURNS[BOOLEAN] = INLINE {RETURN[wall[i].x1=0 AND wall[i].y1=0 AND wall[i].x2=0 AND wall[i].y2=0]}; randomX, randomY: INTEGER _ 7; vx1: INTEGER _ 0; vx2: INTEGER _ 590; vy1: INTEGER _ 35; vy2: INTEGER _ 732; w: INTEGER _ 10; wallLength: CARDINAL = 21; wall: ARRAY [0..wallLength) OF RECORD[x1, y1, x2, y2: INTEGER]; mineLength: CARDINAL = 15; mine: ARRAY [0..mineLength) OF RECORD[x, y: INTEGER] _ [ [249, 237], [212, 385], [229, 324], [266, 407], [286, 385], [290, 237], [307, 452], [311, 200], [318, 320], [350, 247], [356, 398], [390, 427], [390, 306], [408, 351], [421, 250]]; Initialize[]; END... pause1: CARDINAL _ 3; pause2: CARDINAL _ 100; TurnTurret: PROCEDURE[v: ViewerClasses.Viewer, data: TankData] = BEGIN IF data.ticks # 0 THEN data.ticks _ data.ticks - 1; IF data.ticks # 0 THEN RETURN; IF data.turretRight THEN { data.ticks _ pause1; v.icon _ LOOPHOLE[LOOPHOLE[v.icon, CARDINAL] + 1]; IF v.icon = LOOPHOLE[LOOPHOLE[tankIcon, CARDINAL] + turrets] THEN {data.ticks _ pause2; data.turretRight _ FALSE}} ELSE { data.ticks _ pause1; v.icon _ LOOPHOLE[LOOPHOLE[v.icon, CARDINAL] - 1]; IF v.icon = tankIcon THEN {data.ticks _ pause2; data.turretRight _ TRUE}}; ViewerOps.PaintViewer[v, all]; END;