<<[Cedar]Top>MazeWar.df=>MazeWarPlayerImpl2.Mesa>> <> <> DIRECTORY Atom, Basics, BasicTime, Commander, Containers, FS, Imager, ImagerBackdoor, ImagerPath, ImagerPixelMap, IO, IOClasses, Labels, MazeWarFinder, MazeWarFinderRpcControl, MazeWarPlayer, MazeWarPlayerInsides, MazeWarPlayerRpcControl, MessageWindow, Process, Random, Rope, RPC, ThisMachine, UserCredentials, ViewerBlast, ViewerClasses, ViewerOps; MazeWarPlayerImpl2: CEDAR MONITOR LOCKS lock USING lock: REF MONITORLOCK IMPORTS Atom, BasicTime, Commander, Containers, FS, IO, Imager, ImagerBackdoor, ImagerPixelMap, IOClasses, Labels, MazeWarFinder, MazeWarFinderRpcControl, MazeWarPlayerInsides, MazeWarPlayerRpcControl, MessageWindow, Process, Random, Rope, RPC, ThisMachine, UserCredentials, ViewerBlast, ViewerOps EXPORTS MazeWarPlayer, MazeWarPlayerInsides = <> <> BEGIN OPEN MazeWarPlayerInsides; myself: PUBLIC Player _ NIL; playerNumber: PUBLIC CARDINAL _ 47; myInstance: PUBLIC ROPE _ "??"; topviews: PUBLIC ARRAY Angle OF Bitmap _ ALL[nilBitmap]; exportedPlayer: PUBLIC BOOLEAN _ FALSE; pieces: PUBLIC ARRAY [0 .. MaxDistance] OF ARRAY Piece OF LocatedBitmap _ ALL[ALL[[]]]; shownPlayers: PUBLIC PlayerList _ NIL; rs: Random.RandomStream _ Random.Create[seed: -1]; MakeNewPlayer: PUBLIC PROC [name, instance, picsSource, mazeSource: ROPE] RETURNS [errmsg: ROPE _ NIL] = BEGIN mv: MazeView; id: PlayerId _ rs.ChooseInt[FIRST[INTEGER], LAST[INTEGER]]; userName, password, mazeRope: ROPE; failure: BOOLEAN _ FALSE; pp: PlayerPics; mazeStreamIn, mazeStreamOut: IO.STREAM; mazeStreamIn _ FS.StreamOpen[fileName: mazeSource ! FS.Error => {errmsg _ IO.PutFR["Unable to open maze source because %g", IO.rope[error.explanation]]; failure _ TRUE; CONTINUE}]; IF failure THEN RETURN; [] _ mazeStreamIn.SkipWhitespace[flushComments: FALSE]; mazeStreamOut _ IO.ROS[]; IOClasses.Copy[from: mazeStreamIn, to: mazeStreamOut, closeFrom: TRUE, closeTo: FALSE]; mazeRope _ mazeStreamOut.RopeFromROS[]; pp _ GetPlayerPics[picsSource ! FS.Error => {errmsg _ error.explanation; failure _ TRUE; CONTINUE}]; IF failure THEN RETURN; [mv, errmsg] _ MazeFromRope[mazeRope]; IF errmsg.Length[] > 0 THEN RETURN; myself _ Plop[mv, id, name, instance, pp]; ViewMaze[myself, [iconic: TRUE, name: name]]; AddScoring[myself, myself]; [userName, password] _ UserCredentials.Get[]; TRUSTED {MazeWarPlayerRpcControl.ExportInterface[ interfaceName: [instance: instance], user: userName, password: RPC.MakeKey[password]]}; exportedPlayer _ TRUE; TRUSTED { Process.Detach[FORK StayAlive[myself]]; Process.Detach[FORK StatusBroadcaster[myself]]; }; END; debugImport: BOOLEAN _ FALSE; importFailures: LIST OF CARDINAL _ NIL; exportedFinder: BOOLEAN _ FALSE; disabled: BOOL _ FALSE; finderID: BasicPlayer; StayAlive: PROC [p: Player] = BEGIN ENABLE UNWIND => {IF p.destroyed THEN myself _ NIL}; ExportFinder: PROC = { IF exportedFinder THEN {MazeWarFinderRpcControl.UnexportInterface[]; exportedFinder _ FALSE}; exportedFinder _ TRUE; TRUSTED {MazeWarFinderRpcControl.ExportInterface[ interfaceName: [instance: "MazeWar.Auto"], user: "MazeWar.Auto", password: RPC.MakeKey["MazeWar"] ! RPC.ExportFailed => { MessageWindow.Append[ IO.PutFR["Tried to become Finder and failed! (at %g)", [time[BasicTime.Now[]]]], TRUE]; exportedFinder _ FALSE; CONTINUE}; ]}; }; TRUSTED {Process.SetPriority[Process.priorityBackground]}; ExportFinder[]; <> FOR age: CARDINAL _ 0, age + 1 WHILE NOT p.destroyed DO ok: BOOLEAN _ TRUE; IF NOT disabled THEN { MazeWarFinderRpcControl.ImportInterface[interfaceName: [instance: "MazeWar.Auto"] !RPC.ImportFailed => {ok _ FALSE; CONTINUE}]; IF debugImport THEN { MessageWindow.Append[IO.PutFR["Import %g at age %g", IO.bool[ok], IO.card[age]], TRUE]; IF NOT ok THEN importFailures _ CONS[age, importFailures]}; IF ok THEN { players: BasicPlayerList; TRUSTED { finderID _ MazeWarFinder.GetFinderID[!RPC.CallFailed => {ok _ FALSE; CONTINUE}]; IF ok THEN MazeWarFinder.TakePlayer[[myself.instanceName, myself.id] !RPC.CallFailed => {ok _ FALSE; CONTINUE}]; IF ok THEN players _ MazeWarFinder.ListPlayers[!RPC.CallFailed => {ok _ FALSE; CONTINUE}]}; IF ok THEN { FOR players _ players, players.rest WHILE players # NIL DO q: Player _ EnsurePlayer[players.first]; IF q # NIL AND NOT q.instanceName.Equal[players.first.instance] THEN MessageWindow.Append["Random number collision", TRUE]; ENDLOOP; }; TRUSTED {MazeWarFinderRpcControl.UnimportInterface[]}; } ELSE ExportFinder[]; }; Delay[age]; ENDLOOP; IF exportedFinder THEN {MazeWarFinderRpcControl.UnexportInterface[]; exportedFinder _ FALSE}; myself _ NIL; END; StatusBroadcaster: PROC [p: Player] = BEGIN TRUSTED {Process.SetPriority[Process.priorityForeground]}; DO row, col, score, angle: INTEGER; shot: BOOL; Wait: ENTRY PROC [lock: REF MONITORLOCK] RETURNS [exit: BOOL] = { ENABLE UNWIND => NULL; WHILE NOT (p.announce OR p.destroyed) DO WAIT p.announceChange ENDLOOP; p.announce _ FALSE; row _ p.row; col _ p.col; score _ p.score; angle _ p.angle; shot _ p.announceShot; p.announceShot _ FALSE; exit _ p.destroyed; }; IF Wait[p.announceLock] THEN EXIT; FOR pl: PlayerList _ p.mv.players, pl.rest WHILE pl # NIL DO ok: BOOLEAN _ TRUE; IF pl.first = p OR pl.first.ir = NIL THEN LOOP; TRUSTED {pl.first.ir.TakeStatus[id: p.id, row: row, col: col, score: score, angle: angle, shot: shot !RPC.CallFailed => {ok _ HandleCallFailure[pl.first, why]; CONTINUE}]}; NoticeCall[pl.first, ok]; ENDLOOP; ENDLOOP; END; AnnounceStatus: PUBLIC PROC [shot: BOOLEAN] = BEGIN Enter: ENTRY PROC [lock: REF MONITORLOCK] = { ENABLE UNWIND => NULL; myself.announceShot _ myself.announceShot OR shot; myself.announce _ TRUE; BROADCAST myself.announceChange; }; Enter[myself.announceLock]; END; WhoAmI: PROC [cmd: Commander.Handle] RETURNS [result: REF _ NIL, msg: ROPE _ NIL] --Commander.CommandProc-- = BEGIN p: Player _ myself; IF p = NIL THEN RETURN [$Failure, "Not playing"] ELSE cmd.out.PutF["\"%q\" %g\n", IO.rope[p.instanceName], IO.int[p.id]]; END; Contact: PROC [cmd: Commander.Handle] RETURNS [result: REF _ NIL, msg: ROPE _ NIL] --Commander.CommandProc-- = BEGIN Parse: PROC = { bp.instance _ in.GetRopeLiteral[]; bp.id _ in.GetInt[]; }; bp: BasicPlayer; in: IO.STREAM _ IO.RIS[cmd.commandLine]; ok: BOOL _ TRUE; IF myself = NIL THEN RETURN [$Failure, "Not playing"]; Parse[!IO.Error, IO.EndOfStream => { ok _ FALSE; CONTINUE}]; IF ok THEN [] _ EnsurePlayer[bp] ELSE RETURN [$Failure, "Syntax error; say: MazeWarContact \"nn#mmm#ss\" iiiii\n"]; END; Delay: PROC [age: CARDINAL] = BEGIN center: CARDINAL _ Process.SecondsToTicks[IF age < 5 THEN 4 ELSE IF age < 18 THEN 16 ELSE 180]; wait: CARDINAL _ rs.ChooseInt[center/2, 3*center/2]; Process.Pause[wait]; END; MazeFromRope: PROC [rope: ROPE] RETURNS [mv: MazeView, errmsg: ROPE _ NIL] = BEGIN Check: PROC [row, col, drow, dcol: INTEGER] RETURNS [bad: BOOLEAN _ FALSE] = BEGIN index: INTEGER _ Index[mv, row-drow, col-dcol]; di: INTEGER _ Direction[mv, drow, dcol]; IF NOT mv.squares[index].open THEN BEGIN d: INTEGER _ 0; FOR index _ index+di, index+di WHILE mv.squares[index].open DO d _ d + 1 ENDLOOP; IF d > MaxDistance+1 THEN {errmsg _ IO.PutFR["Maze has open distance too long (> %g)", IO.int[MaxDistance+1]]; RETURN[TRUE]}; END; END; rl: INT _ rope.Length[]; cols: INTEGER _ rope.Find["\n"]; rows: INTEGER _ rl/(cols+1); IF rows*(cols+1) # rl THEN RETURN [NIL, "Maze description not properly punctuated with newlines"]; mv _ NEW [MazeViewRep[rows*cols]]; mv.asRope _ rope; mv.rows _ rows; mv.cols _ cols; FOR row: INTEGER IN [0 .. rows) DO FOR col: INTEGER IN [0 .. cols) DO char: CHAR _ rope.Fetch[row*(cols+1)+col]; index: INTEGER _ Index[mv, row, col]; mv.squares[index] _ []; SELECT char FROM 'X => mv.squares[index].open _ FALSE; '. => mv.squares[index].open _ TRUE; ENDCASE => RETURN [NIL, IO.PutFR["Bad character in maze: 0%bC", IO.char[char]]]; IF mv.squares[index].open AND (row = 0 OR row = rows-1 OR col = 0 OR col = cols-1) THEN RETURN [NIL, IO.PutFR["Gap in border"]]; ENDLOOP; ENDLOOP; FOR row: INTEGER IN (0 .. rows-1) DO FOR col: INTEGER IN (0 .. cols-1) DO IF Check[row, col, 0, 1] THEN RETURN; IF Check[row, col, 1, 0] THEN RETURN; ENDLOOP; ENDLOOP; mv.topX _ innerBorder + w - tvd*cols/2; mv.topY _ innerBorder + tvd*rows; mv.inY _ w + hallTopSep; mv.inX _ tvd*cols/2; END; hallTopSep: INTEGER _ 3; scoreTopSep: INTEGER _ 5; scoreSep: INTEGER _ 1; innerBorder: INTEGER _ 2; Plop: PROC [mv: MazeView, id: PlayerId, name, instance: ROPE, pp: PlayerPics] RETURNS [p: Player] = BEGIN row, col: INTEGER; [row, col] _ PickAPlace[mv]; p _ AddPlayerAt[mv, NIL, instance, id, name, row, col, 0, 0, pp]; END; PickAPlace: PUBLIC PROC [mv: MazeView] RETURNS [row, col: INTEGER] = BEGIN me: INTEGER; DO row _ rs.ChooseInt[0, mv.rows-1]; col _ rs.ChooseInt[0, mv.cols-1]; me _ Index[mv, row, col]; IF mv.squares[me].open THEN RETURN; ENDLOOP; END; ViewMaze: PROC [p: Player, viewerInit: ViewerClasses.ViewerRec] = BEGIN IF viewerInit.icon = unInit THEN viewerInit.icon _ mazeWarriorIcon; p.mv.container _ Containers.Create[info: viewerInit, paint: FALSE]; p.mv.maze _ ViewerOps.CreateViewer[flavor: mazeViewerFlavor, info: [parent: p.mv.container, wx: 10, wy: 10, ww: 100, wh: 100, data: p, border: FALSE], paint: FALSE]; ViewerOps.MoveViewer[ viewer: p.mv.maze, x: p.mv.maze.wx, y: p.mv.maze.wy, w: 2*(w+innerBorder) + p.mv.maze.ww - p.mv.maze.cw, h: 2*(w+innerBorder) + hallTopSep + tvd*p.mv.rows + p.mv.maze.wh - p.mv.maze.ch, paint: FALSE]; END; IncrPaint: PUBLIC PROC [p: Player] = { IF p.shot THEN TRUSTED {Process.Detach[FORK ReallyIncrPaint[p]]} ELSE ReallyIncrPaint[p]}; paintlock: REF MONITORLOCK = NEW [MONITORLOCK]; ReallyIncrPaint: PROC [p: Player] = { InnerPaint: ENTRY PROC [lock: REF MONITORLOCK] = { ViewerOps.PaintViewer[viewer: p.mv.maze, hint: client, clearClient: FALSE, whatChanged: p]; }; IF p # myself OR NOT clippable THEN ViewerOps.PaintViewer[viewer: p.mv.maze, hint: client, clearClient: FALSE, whatChanged: p] ELSE { paints _ paints + 1; InnerPaint[paintlock !UNWIND => paints _ paints - 1]; paints _ paints - 1}; }; AddScoring: PUBLIC PROC [vp, p: Player] = BEGIN mv: MazeView _ p.mv; y: INTEGER _ vp.mv.maze.wy + vp.mv.maze.wh + scoreTopSep; p.nameLabel _ Labels.Create[info: [parent: mv.container, name: p.name, border: FALSE], paint: FALSE]; p.scoreLabel _ Labels.Create[info: [parent: mv.container, name: "-65,535", border: FALSE], paint: FALSE]; Labels.Set[p.scoreLabel, IO.PutFR["%6g", IO.int[p.score]], FALSE]; Labels.SetDisplayStyle[p.nameLabel, IF Visibility[vp, p] > 0 THEN $WhiteOnBlack ELSE $BlackOnWhite, FALSE]; FOR pl: PlayerList _ mv.players, pl.rest WHILE pl # NIL DO n: Viewer _ pl.first.nameLabel; v: Viewer _ pl.first.scoreLabel; ViewerOps.MoveViewer[viewer: n, x: mv.maze.wx, y: y, w: n.ww, h: n.wh, paint: FALSE]; ViewerOps.MoveViewer[viewer: v, x: mv.maze.wx + mv.maze.ww - v.ww, y: y, w: v.ww, h: v.wh, paint: FALSE]; y _ MAX[n.wy+n.wh, v.wy+v.wh] + scoreSep; ENDLOOP; END; DrawHall: PUBLIC PROC [context: Context, mv: MazeView, row, col, drow, dcol: INTEGER, angle: Angle] = BEGIN Line: PROC [a, b: Pt3] = INLINE {context.MaskVector[[a.x/a.z, a.y/a.z], [b.x/b.z, b.y/b.z]]}; DoIt: PROC = { Blt: PROC [piece: Piece] = { lb: LocatedBitmap _ pieces[d][piece]; ViewerBlast.DrawBitmap[context: context, bitmap: lb.bitmap, sOrigin: lb.sOrigin, fOrigin: lb.fOrigin]; }; DrawPlayers: PROC = { context.SetColor[ImagerBackdoor.invert]; FOR pl: PlayerList _ shownPlayers, pl.rest WHILE pl # NIL DO b: Bitmap _ pl.first.pics.pics[pl.first.sdistance][(pl.first.sangle+4-angle) MOD 4]; ViewerBlast.DrawBitmap[context: context, bitmap: b, fOrigin: b.fSize/2, sOrigin: b.sSize/2]; ENDLOOP; }; DrawBorder: ImagerPath.PathProc = { moveTo[[w, w]]; lineTo[[w, -w]]; lineTo[[-w, -w]]; lineTo[[-w, w]]; lineTo[[ w, w]]; }; me, di, left, right, d, crow, ccol: INTEGER; f, b: Number; mes, lefts, rights: Square; di _ Direction[mv, drow, dcol]; left _ Direction[mv, -dcol, drow]; right _ Direction[mv, dcol, -drow]; crow _ row; ccol _ col; mes _ mv.squares[me _ Index[mv, row, col]]; f _ z1; b _ hither; context.SetColor[Imager.white]; context.MaskRectangle[[-w, -w, 2*w, 2*w]]; context.SetColor[Imager.black]; context.MaskStroke[DrawBorder]; IF NOT mes.open THEN RETURN; lefts _ mv.squares[me + left]; rights _ mv.squares[me + right]; d _ 0; WHILE mes.open DO nu: INTEGER _ me + di; fls: Square _ mv.squares[nu + left]; frs: Square _ mv.squares[nu + right]; fs: Square _ mv.squares[nu]; r2: Number _ f*r/b; IF paints > 1 AND clippable THEN RETURN; IF NOT lefts.open THEN { IF blt THEN Blt[LeftFilled] ELSE { Line[[-r, r, f], [-r, r, b]]; Line[[-r, -r, f], [-r, -r, b]]}} ELSE IF NOT fls.open THEN { IF blt THEN Blt[LeftGap] ELSE { Line[[-r2, r, f], [-r, r, f]]; Line[[-r2, -r, f], [-r, -r, f]]}}; IF (IF fs.open THEN lefts.open # fls.open ELSE IF lefts.open THEN fls.open ELSE TRUE) THEN { IF blt THEN Blt[LeftEdge] ELSE Line[[-r, r, f], [-r, -r, f]]}; IF NOT rights.open THEN { IF blt THEN Blt[RightFilled] ELSE { Line[[ r, r, f], [ r, r, b]]; Line[[ r, -r, f], [ r, -r, b]]}} ELSE IF NOT frs.open THEN { IF blt THEN Blt[RightGap] ELSE { Line[[ r2, r, f], [ r, r, f]]; Line[[ r2, -r, f], [ r, -r, f]]}}; IF (IF fs.open THEN rights.open # frs.open ELSE IF rights.open THEN frs.open ELSE TRUE) THEN { IF blt THEN Blt[RightEdge] ELSE Line[[r, r, f], [r, -r, f]]}; b _ f; f _ f + dz; d _ d + 1; me _ nu; mes _ fs; lefts _ fls; rights _ frs; crow _ crow + drow; ccol _ ccol + dcol; ENDLOOP; d _ d - 1; IF blt THEN Blt[HorEdges] ELSE { Line[[-r, r, b], [r, r, b]]; Line[[-r, -r, b], [r, -r, b]]}; CallUnderMonitor1[DrawPlayers]; }; <> <> DoIt[]; <> END; <> <<>> <> <<>> <> <> <> <> <> AddPlayerAt: PUBLIC PROC [mv: MazeView, ir: PlayerInterface, instanceName: ROPE, id: PlayerId, name: ROPE, row, col: INTEGER, angle: Angle, score: INTEGER, pp: PlayerPics] RETURNS [p: Player] = BEGIN me: INTEGER _ Index[mv, row, col]; p _ NEW [PlayerRep _ [mv: mv, ir: ir, instanceName: instanceName, id: id, quiverTill: BasicTime.GetClockPulses[]-1, row: row, col: col, angle: angle, peek: 0, score: score, name: name, pics: pp, announceLock: NEW [MONITORLOCK]]]; mv.squares[me].occupants _ CONS[p, mv.squares[me].occupants]; mv.players _ CONS[p, mv.players]; TRUSTED {Process.InitializeCondition[@p.announceChange, Process.SecondsToTicks[5]]}; END; Setup: PROC = BEGIN f, b: Number; count: REF CARDINAL _ NARROW[Atom.GetProp[atom: $MazeWar, prop: $PlayerCount]]; playerRope: ROPE _ NIL; ArrowPath: ImagerPath.PathProc = { q: REAL = 2; q2: REAL = 5; moveTo[ [tvd/q, 0]]; lineTo[ [0, tvd/q]]; lineTo[ [0, tvd/q2]]; lineTo[ [-tvd/q, tvd/q2]]; lineTo[ [-tvd/q, -tvd/q2]]; lineTo[ [0, -tvd/q2]]; lineTo[ [0, -tvd/q]]; lineTo[ [tvd/q, 0]]; }; IF count = NIL THEN Atom.PutProp[atom: $MazeWar, prop: $PlayerCount, val: count _ NEW [CARDINAL _ 0]]; count^ _ playerNumber _ count^ + 1; myInstance _ IO.PutFR["%g%b", IO.rope[ThisMachine.Address[$Pup]], IO.card[playerNumber]]; IF playerNumber # 1 THEN playerRope _ IO.PutFR["%g", IO.card[playerNumber]]; FOR a: Angle IN Angle DO context: Context; topviews[a] _ ImagerPixelMap.Create[0, [0, 0, tvd, tvd]]; context _ ViewerBlast.PixelMapContext[topviews[a]]; context.SetColor[Imager.white]; context.MaskRectangle[[0, 0, tvd, tvd]]; context.SetColor[Imager.black]; context.TranslateT[[tvd/2, tvd/2]]; context.RotateT[a*-90]; context.MaskFill[ArrowPath]; ENDLOOP; f _ z1; b _ hither; FOR d: INTEGER IN [0 .. MaxDistance] DO context: Context; Start: PROC [piece: Piece] = { rmin: Number _ r/f; rmax: Number _ r/b; bounds: Box; bounds _ SELECT piece FROM LeftFilled => [-rmax, -rmax, -rmin, rmax], LeftGap => [-rmax, -rmin, -rmin, rmin], LeftEdge => [-rmin, -rmin, -rmin, rmin], RightFilled => [rmin, -rmax, rmax, rmax], RightGap => [rmin, -rmin, rmax, rmin], RightEdge => [rmin, -rmin, rmin, rmin], HorEdges => [-rmin, -rmin, rmin, rmin], ENDCASE => ERROR; bounds.xmin _ bounds.xmin - 1; bounds.ymin _ bounds.ymin - 1; bounds.xmax _ bounds.xmax + 1; bounds.ymax _ bounds.ymax + 1; pieces[d][piece] _ [ bitmap: ImagerPixelMap.Create[0, [ sMin: 0, fMin: 0, sSize: bounds.ymax-bounds.ymin, fSize: bounds.xmax-bounds.xmin]], fOrigin: -bounds.xmin, sOrigin: bounds.ymax]; context _ ViewerBlast.PixelMapContext[pieces[d][piece].bitmap]; context.TranslateT[[-bounds.xmin, -bounds.ymin]]; context.SetColor[Imager.white]; context.MaskBox[[bounds.xmin, bounds.ymin, bounds.xmax, bounds.ymax]]; context.SetColor[Imager.black]; }; Line: PROC [a, b: Pt3] = INLINE {context.MaskVector[[a.x/a.z, a.y/a.z], [b.x/b.z, b.y/b.z]]}; r2: Number _ f*r/b; Start[LeftFilled]; Line[[-r, r, f], [-r, r, b]]; Line[[-r, -r, f], [-r, -r, b]]; Start[LeftGap]; Line[[-r2, r, f], [-r, r, f]]; Line[[-r2, -r, f], [-r, -r, f]]; Start[LeftEdge]; Line[[-r, r, f], [-r, -r, f]]; Start[RightFilled]; Line[[ r, r, f], [ r, r, b]]; Line[[ r, -r, f], [ r, -r, b]]; Start[RightGap]; Line[[ r2, r, f], [ r, r, f]]; Line[[ r2, -r, f], [ r, -r, f]]; Start[RightEdge]; Line[[r, r, f], [r, -r, f]]; Start[HorEdges]; Line[[-r, r, f], [r, r, f]]; Line[[-r, -r, f], [r, -r, f]]; b _ f; f _ f + dz; ENDLOOP; ViewerOps.RegisterViewerClass[flavor: mazeViewerFlavor, class: mazeViewerClass]; Commander.Register[key: Rope.Cat["MazeWar", playerRope], proc: StartPlayer, doc: "starts playing maze war; see MazeWarDoc.Tioga for possible command line arguments"]; Commander.Register[key: Rope.Cat["MazeWarStop", playerRope], proc: StopPlayer, doc: "exits maze war game"]; Commander.Register[key: Rope.Cat["WhoAmI", playerRope], proc: WhoAmI, doc: "gives your MazeWar identification"]; Commander.Register[key: Rope.Cat["MazeWarContact", playerRope], proc: Contact, doc: "MazeWarContact \"nn#mmm#ss\" iiiii makes contact with that MazeWar player"]; END; Box: TYPE = RECORD [xmin, ymin, xmax, ymax: Number]; StupidFuckingWarnings: PUBLIC SIGNAL [ATOM] = CODE; Setup[]; END.