SHRIKE¶

top-down 3D space survival roguelike.

â–¶ Run in browser

Tags: 3d game roguelike survival procedural

SHRIKE¶

A top-down 3D space survival roguelike on the SimVX engine. Strip-mine dying sectors under a signature meter that your own greed fills, juggle fuel, oxygen, ammo and a starving capacitor, bank your haul or smelt it aboard a Refinery that lights you up like a flare, and flee the Shrike when the klaxon sounds; behind you a red front swallows the star chart, and ahead lie richer sectors, louder deaths, and eventually the night your build is strong enough to turn around and hunt it back to its roost. Every tuning number that shapes the game is in shrike/balance.py.

Run¶

simvx examples run demos_shrike          # play
simvx examples run demos_shrike --test   # headless self-check, exits on its own

--test mounts the entry scene, flies a fixed-seed slice of a whole run (mine a deposit, provoke the signature lock, survive a wave and an arrival, flee through the star chart, extract at the Deep Gate) and prints SELFTEST: PASS before exiting. It writes its saves to a throwaway directory, so it never touches a real profile.

Controls¶

The nose aims independently of the thrust vector: you fly one way and shoot another. On a pad the shield arc mirrors the right stick by default; on a keyboard Q and E rotate it continuously.

Action

Keyboard and mouse

Controller

Thrust

W A S D

Left stick

Aim

Mouse

Right stick

Fire primary

Left mouse

Right trigger

Mining beam

Right mouse

Left trigger

Rotate shield arc

Q / E

Left / right bumper (60 degree nudge; both re-centres on aim)

Afterburner

Shift (hold)

A (hold)

Interact

F

X

Jettison scrap

G

B

Silent running

C (toggle)

Y (toggle)

Solar wings

X

D-pad up

Generator

Z

D-pad down

Warp spool

R (toggle)

Left stick click (toggle)

Tractor scoop

Tab (toggle)

Right stick click (toggle)

Star chart

M

Back

Show or hide the controls bar

H

D-pad left

Pause menu

Esc

Start

Warp spool is a toggle rather than a hold: press once to start the channel, press again to cancel it. Loose salvage is scooped by flying through it, so a mote you touch is a mote you keep.

Interact is one button with three lengths, and the ship draws the hold as a radial fill:

Hold

Effect

Tap

Dock at a depot, grab salvage, trigger a signal event

1 second

Patch the nearest hull breach (costs scrap)

3 seconds

Start a Refinery batch, or sell your scrap while docked

Saves¶

Two files, both under the XDG data directory:

Path

Contents

$XDG_DATA_HOME/simvx/shrike/profile.sav

Cores, Fleet Doctrine, quills, fletchings, lure fragments, hulls, Hunt Rank, codex, milestones, per-hull bests

$XDG_DATA_HOME/simvx/shrike/suspend.sav

The single suspend slot, deleted as it is loaded

XDG_DATA_HOME falls back to ~/.local/share when unset or not absolute, so the usual location is ~/.local/share/simvx/shrike/. Setting SHRIKE_DATA_DIR overrides both, which is what the self-check and portable installs use. Nothing is ever written into the game directory.

Tests¶

uv run --package simvx-core pytest examples/demos/shrike/tests/test_foundation.py

Each module has its own test file under examples/demos/shrike/tests/; run one file at a time. They are headless logic tests: scenes run under SceneRunner and every player verb goes through InputSimulator, so the real action map and input pipeline are exercised rather than handlers called directly. No GPU, no windows.

tests/test_balance_bands.py is the tuning contract: it measures the design’s time-to-kill bands (mite under 0.5 s, lancer about 2 s, warden about 45 s, one Shrike fin about 15 s) by feeding each reference loadout’s DPS through the real damage router. A balance change that moves a band fails there first.

Source files¶

File

Summary

Lines

main.py

SHRIKE: top-down 3D space survival roguelike.

441

shrike/__init__.py

SHRIKE: a top-down 3D space survival roguelike on the SimVX engine.

20

shrike/artkit.py

Procedural art kit for SHRIKE: every mesh, material, sky and grade is generated.

2168

shrike/audio.py

SHRIKE’s audio: every sound synthesised at load, and the mix the power state drives.

886

shrike/balance.py

Every tuning number in SHRIKE, as named constants and small specs.

780

shrike/bounty.py

Notoriety, the run-level reputation dial, and the two set pieces it buys.

1012

shrike/chart.py

The star chart: a branching route eaten from behind, and the warp that pays for it.

1657

shrike/combat.py

The damage router and the collision-layer contract.

452

shrike/enemies/__init__.py

SHRIKE’s hostiles: shared steering, the enemy base, and the archetypes.

53

shrike/enemies/advanced.py

The advanced half of the enemy roster: support, siege, alarm, emplacement and herald.

1474

shrike/enemies/basic.py

The four common hostiles: Mites, Skimmers, Lancers and Mag-mines.

777

shrike/enemies/steering.py

Flow-field steering and the shared enemy base.

805

shrike/events.py

The signal-event deck: twelve cards a sector can deal you on arrival.

185

shrike/flow.py

The run lifecycle: the menu, the run, the Gate, the collapse and the ledger.

7524

shrike/hud.py

SHRIKE’s heads-up display: foveal arcs, a warming border and diegetic rings.

4480

shrike/hunter.py

The Shrike: the segmented hunter the whole economy is priced against.

974

shrike/juice.py

The feel layer: hit-stop, trauma-budgeted shake, punches and the scoop magnet.

689

shrike/meta.py

Fleet Doctrine, the milestone bounties, the hull trophies and the Hunt Ranks.

1500

shrike/modules.py

Hull sockets, the thirty-module catalogue, and the modules that act.

1290

shrike/onboarding.py

The staged first three runs: the HUD subtraction schedule and the safe arrival.

932

shrike/power.py

Power generation, life support, and the silent-running posture.

1064

shrike/roost.py

The endgame campaign: Lure fragments, Fletching pity, and the Roost duel.

653

shrike/runtime.py

The shared runtime spine of SHRIKE.

531

shrike/save.py

Persistence: the permanent meta profile and the one-slot suspend save.

528

shrike/sector.py

What a warp-in drops you into: one sector’s harvestable content.

1735

shrike/ship.py

The player ship: drift flight, the shield arc, hull breaches and the warp spool.

1104

shrike/signature.py

The signature meter: the bill this sector charges for everything you do.

203

shrike/trading.py

The scrap ledger, the drift depots, and the Broker barge.

1121

shrike/vfx.py

SHRIKE’s effects library: pooled bursts, the two live emitters and the dust.

1509

shrike/waves.py

The wave composer: what arrives every 35 to 55 seconds, and from where.

1104

shrike/weapons.py

Hardpoints, the ten-weapon roster, and everything they put in the air.

1313

Source¶

  1# /// simvx
  2# tags = ["3d", "game", "roguelike", "survival", "procedural"]
  3# screenshot_frame = 40
  4# web = { width = 1280, height = 720, responsive = true }
  5# ///
  6"""SHRIKE: top-down 3D space survival roguelike.
  7
  8Run:            simvx examples run demos_shrike
  9Frame cap:      simvx examples run demos_shrike --fps 60
 10Headless check: simvx examples run demos_shrike --test
 11
 12Controls (keyboard and mouse): WASD thrust, mouse aims the nose, LMB fires,
 13RMB mines, Q/E steer the shield arc, Shift afterburner, C silent running,
 14X solar wings, Z generator, F interact, G jettison, R warp spool, Tab scoop,
 15M star chart, Esc pause. Full controller mapping in shrike/runtime.py.
 16"""
 17
 18import importlib.util
 19import math
 20import os
 21import sys
 22import tempfile
 23
 24from shrike import balance
 25from shrike.runtime import (
 26    CAMERA_DISTANCE,
 27    CAMERA_PITCH_RADIANS,
 28    INPUT_ACTIONS,
 29    PLANE_Y,
 30    WINDOW_HEIGHT,
 31    WINDOW_WIDTH,
 32    CameraRig,
 33    Groups,
 34    move_input,
 35)
 36
 37from simvx.core import (
 38    DirectionalLight3D,
 39    Input,
 40    Material,
 41    Mesh,
 42    MeshInstance3D,
 43    Node,
 44    Node3D,
 45    Quat,
 46    Vec2,
 47    Vec3,
 48    WorldEnvironment,
 49)
 50
 51#: Re-exported so that the screenshot and web-export pipelines can reach the
 52#: front door by name: both resolve ``web.root`` as an attribute of this module,
 53#: and the run lifecycle otherwise only ever arrives through
 54#: :func:`create_entry_scene`. Guarded for the same reason that function is: a
 55#: tree without ``shrike.flow`` still has to import, and there ``FoundationScene``
 56#: is the entry and no menu exists to point a capture at.
 57try:
 58    from shrike.flow import MainMenuScene
 59except ImportError:  # pragma: no cover - only reachable without shrike.flow
 60    MainMenuScene = None
 61
 62FOUNDATION_SHIP_SPEED = 26.0
 63
 64
 65class FoundationShip(Node3D):
 66    """Minimal flight probe: dampened drift movement on the flight plane.
 67
 68    Stands in for the real player ship until ``shrike.ship`` provides one, and
 69    keeps the boot path and the headless self-check exercising real input,
 70    movement and camera-follow behaviour. The full flight model (nose-to-aim,
 71    afterburner, breaches) belongs to ``shrike.ship.PlayerShip``.
 72    """
 73
 74    def __init__(self, **kwargs):
 75        super().__init__(**kwargs)
 76        self.velocity = Vec2(0.0, 0.0)
 77
 78    def on_enter_tree(self):
 79        super().on_enter_tree()
 80        self.add_to_group(Groups.SHIP)
 81
 82    def on_ready(self):
 83        self.add_child(
 84            MeshInstance3D(
 85                name="Hull",
 86                mesh=Mesh.cone(radius=0.6, height=1.8, segments=12),
 87                material=Material(colour=(0.75, 0.78, 0.82, 1.0), metallic=0.8, roughness=0.35),
 88                rotation=Quat.from_euler(math.radians(90), 0.0, 0.0),
 89            )
 90        )
 91
 92    def on_fixed_update(self, dt: float):
 93        thrust = move_input()
 94        ax = float(thrust.x) * FOUNDATION_SHIP_SPEED
 95        az = float(thrust.y) * FOUNDATION_SHIP_SPEED
 96        damp = balance.INERTIAL_DAMPENING ** (dt * 60.0)
 97        self.velocity = Vec2(
 98            (float(self.velocity.x) + ax * dt) * damp,
 99            (float(self.velocity.y) + az * dt) * damp,
100        )
101        self.position = Vec3(
102            float(self.position.x) + float(self.velocity.x) * dt,
103            PLANE_Y,
104            float(self.position.z) + float(self.velocity.y) * dt,
105        )
106
107
108class FoundationScene(Node):
109    """The minimal run scene the game boots until ``shrike.flow`` takes over.
110
111    Environment, sun, a flight probe on the plane, and the camera rig. Quits
112    on the ``star_chart`` action; the flow module replaces that with the real
113    pause and chart screens.
114    """
115
116    input_context = INPUT_ACTIONS
117
118    def on_ready(self):
119        env = self.add_child(WorldEnvironment(name="Environment"))
120        env.ambient_light_colour = (0.10, 0.12, 0.18, 1.0)
121        env.ambient_light_energy = 1.0
122        env.bloom_enabled = True
123
124        sun = self.add_child(DirectionalLight3D(name="Sun"))
125        sun.direction = Vec3(-0.5, -1.0, -0.4)
126        sun.colour = (0.9, 0.92, 1.0)
127        sun.intensity = 1.2
128
129        self.ship = self.add_child(FoundationShip(name="Ship", position=Vec3(0.0, PLANE_Y, 0.0)))
130        self.rig = self.add_child(CameraRig(name="CameraRig"))
131        self.rig.set_target(self.ship)
132
133    def on_update(self, dt: float):
134        self.rig.set_aim(self._mouse_aim_point())
135        if Input.just_pressed("star_chart"):
136            self.app.quit()
137
138    def _mouse_aim_point(self) -> Vec3:
139        """Approximate flight-plane point under the mouse, relative to the ship.
140
141        A screen-centre-relative mapping, good enough for the camera lead;
142        the real ship module replaces this with a proper camera ray cast.
143        """
144        w, h = self.tree.screen_size
145        m = Input.mouse_position
146        dx = (float(m.x) - w / 2.0) / max(w, 1.0)
147        dz = (float(m.y) - h / 2.0) / max(h, 1.0)
148        reach = 30.0
149        p = self.ship.position
150        return Vec3(float(p.x) + dx * reach, PLANE_Y, float(p.z) + dz * reach)
151
152
153def create_entry_scene() -> Node:
154    """The scene the app boots into.
155
156    ``shrike.flow`` owns the run lifecycle (main menu, run setup, the run
157    itself) and is what boots in a complete tree. The foundation scene is the
158    fallback for a tree without that module, where it keeps the game bootable
159    and the ``--test`` smoke gate meaningful.
160    """
161    if importlib.util.find_spec("shrike.flow") is not None:
162        from shrike.flow import create_entry_scene as flow_entry
163
164        return flow_entry()
165    return FoundationScene(name="FoundationScene")
166
167
168#: Fixed seed for the scripted run slice, so the smoke gate is repeatable.
169SELFTEST_SEED = 20260823
170#: Run number past the onboarding schedule, so the chart answers the player.
171SELFTEST_RUN_NUMBER = 4
172
173
174def _fail(reason: str) -> bool:
175    print(f"SELFTEST: FAIL ({reason})")
176    return False
177
178
179def _boot_check() -> bool:
180    """Boot the entry scene headlessly and verify the front door.
181
182    Runs without a GPU or a window: the scene is stepped by ``SceneRunner``.
183    The game opens on the main menu, so the check is that the menu mounts,
184    draws its centred panel inside the screen, and offers entries. Flight is
185    checked by the run slice, which drives a live ship.
186    """
187    from shrike.flow import MainMenuScene
188
189    from simvx.core.testing import SceneRunner
190
191    runner = SceneRunner(screen_size=(WINDOW_WIDTH, WINDOW_HEIGHT))
192    runner.load(create_entry_scene())
193    runner.advance_frames(60)
194    root = runner.root
195    if root is None or not root.children:
196        return _fail("entry scene mounted empty")
197    if not isinstance(root, MainMenuScene):
198        return _fail(f"entry scene is {type(root).__name__}, not the main menu")
199    if root.menu is None or not root.menu.entries:
200        return _fail("the main menu has no entries")
201    panel = root.menu_panel()
202    if panel is None:
203        return _fail("the main menu drew no panel")
204    x, y, width, height = panel.get_global_rect()
205    if x < 0.0 or y < 0.0:
206        return _fail("the menu panel hangs off the top-left of the screen")
207    if x + width > WINDOW_WIDTH or y + height > WINDOW_HEIGHT:
208        return _fail("the menu panel hangs off the bottom-right of the screen")
209    entries = [text for text, _ in root.menu.entries]
210    print(f"SELFTEST: boot ok ({runner.frame_count} frames, menu entries: {', '.join(entries)})")
211    return True
212
213
214def _run_slice() -> bool:
215    """Fly one scripted slice of a real run, end to end, headlessly.
216
217    Boot into a fixed-seed run, mine a deposit and grab the motes, provoke
218    the signature lock, ride out a wave and the Shrike's arrival on the move,
219    flee through the star chart once the lockout lifts, then extract at the
220    Deep Gate and check the ledger that comes back.
221    """
222    from shrike.flow import LedgerScene, RunConfig, RunScene
223
224    from simvx.core import Controller, Key
225    from simvx.core.testing import InputSimulator, SceneRunner
226
227    runner = SceneRunner(screen_size=(WINDOW_WIDTH, WINDOW_HEIGHT))
228    sim = InputSimulator(runner.tree)
229    sim.reset()
230    scene = RunScene(RunConfig(seed=SELFTEST_SEED, run_number=SELFTEST_RUN_NUMBER).as_dict())
231    runner.load(scene)
232    runner.advance_frames(10)
233    if not scene.running or scene.ship is None or scene.sector is None:
234        return _fail("run scene did not assemble")
235    if not scene.sector.deposits:
236        return _fail("start sector generated no deposits")
237
238    # The spine first: thrust moves the ship on the flight plane and the
239    # camera rig follows it. It is checked here rather than at boot because the
240    # boot scene is the menu, which mounts no ship.
241    ship = scene.ship
242    start = Vec3(*ship.position)
243    sim.press_key(Key.W)
244    runner.advance_frames(90)
245    sim.release_key(Key.W)
246    runner.advance_frames(30)
247    moved = float(abs(ship.position.z - start.z)) + float(abs(ship.position.x - start.x))
248    if moved < 1.0:
249        return _fail(f"thrust did not move the ship; moved {moved:.3f}")
250    if abs(float(ship.position.y) - PLANE_Y) > 1e-4:
251        return _fail(f"ship left the flight plane: y={float(ship.position.y):.4f}")
252    rig = runner.find(CameraRig)
253    if rig is None or rig.camera is None:
254        return _fail("the run mounted no camera rig")
255    cam_focus_z = float(rig.camera.position.z) - CAMERA_DISTANCE * math.cos(CAMERA_PITCH_RADIANS)
256    if abs(cam_focus_z - float(ship.position.z)) > 20.0:
257        return _fail("camera rig did not follow the ship")
258
259    seen: set[str] = set()
260    scene.waves.wave_spawned.connect(lambda count: seen.add("wave"))
261
262    # Harvest: park west of a deposit, aim the pad stick at it, hold the beam,
263    # then tap-grab the motes it sheds.
264    deposit = scene.sector.deposits[0]
265    ship.position = Vec3(float(deposit.position.x) - 6.0, PLANE_Y, float(deposit.position.z))
266    sim.set_controller_axis(Controller.RIGHT_X, 1.0)
267    sim.set_controller_axis(Controller.LEFT_TRIGGER, 1.0)
268    runner.advance_time(6.0)
269    sim.set_controller_axis(Controller.LEFT_TRIGGER, 0.0)
270    for _ in range(10):
271        sim.tap_key(Key.F)
272        runner.advance_frames(6)
273    if float(scene.economy.scrap) <= 0.0:
274        return _fail("mining and grabbing banked no scrap")
275
276    # Provoke the Shrike, then flee the warning: burner lit, shield astern,
277    # weaving so the wave that lands mid-telegraph cannot line up a shot.
278    scene.signature.add(balance.SIGNATURE_MAX, "provocation")
279    runner.advance_frames(3)
280    hunter = scene.hunter_director.hunter
281    if hunter is None:
282        return _fail("signature lock raised no hunter")
283    hunter.hunter_telegraph.connect(lambda stage: seen.add(f"telegraph_{stage}"))
284    hunter.hunter_arrived.connect(lambda index: seen.add("arrived"))
285    sim.set_controller_axis(Controller.RIGHT_X, 0.0)
286    sim.set_controller_axis(Controller.RIGHT_Y, 1.0)
287    sim.press_key(Key.W)
288    sim.press_key(Key.LEFT_SHIFT)
289    weave = (Key.A, Key.D)
290    for step in range(60):
291        side = weave[step % 2]
292        sim.press_key(side)
293        runner.advance_time(1.5)
294        sim.release_key(side)
295        if "arrived" in seen or scene.ended:
296            break
297    if "arrived" not in seen:
298        return _fail("the Shrike never arrived")
299    if "wave" not in seen:
300        return _fail("no wave spawned before the arrival")
301    for step in range(30):
302        side = weave[step % 2]
303        sim.press_key(side)
304        runner.advance_time(1.0)
305        sim.release_key(side)
306        if not ship.warp_scrambled or scene.ended:
307            break
308    sim.release_key(Key.LEFT_SHIFT)
309    if scene.ended or float(ship.hull) <= 0.0:
310        return _fail("the ship did not survive the arrival")
311    if ship.warp_scrambled:
312        return _fail("the arrival lockout never lifted")
313
314    # Flee through the star chart.
315    sim.tap_key(Key.M)
316    runner.advance_frames(2)
317    chart = scene.chart_screen
318    if not chart.is_open:
319        return _fail("the star chart did not open")
320    offered = [d for d in chart.destinations() if d.available]
321    if not offered:
322        return _fail("no affordable escape route on the chart")
323    target = min(offered, key=lambda d: d.quote.total).node_id
324    if not (chart.select(target) and chart.confirm()):
325        return _fail("the chart refused the escape jump")
326    if not runner.simulate_until(lambda _: scene.state.current_node_id == target, max_ticks=30 * 60):
327        return _fail("the warp never completed")
328    if not runner.simulate_until(lambda _: scene.hunter_director.hunter is None, max_ticks=30 * 60):
329        return _fail("the Shrike did not depart after the jump")
330    runner.advance_frames(30)
331    sim.release_key(Key.W)
332    if scene.sector is None or scene.state.ledger.arrivals_survived < 1:
333        return _fail("the fled-to sector did not come up")
334
335    # Extract at the Deep Gate and read the ledger back.
336    gate = next((nid for nid, node in scene.state.chart.nodes.items() if node.is_deep_gate), "")
337    if not gate:
338        return _fail("the chart has no Deep Gate")
339    scene.enter_sector(gate)
340    runner.advance_frames(5)
341    root = runner.root
342    if not isinstance(root, LedgerScene):
343        return _fail("extraction did not raise the ledger screen")
344    ledger = root.ledger
345    if ledger.outcome != "extraction" or not ledger.conversion or ledger.cores_total <= 0.0:
346        return _fail("the extraction ledger did not settle")
347    if not ledger.recap or not root.lines():
348        return _fail("the ledger screen has nothing to say")
349
350    stages = ", ".join(sorted(seen))
351    print(f"SELFTEST: slice ok ({runner.frame_count} frames; {stages}; {ledger.cores_total:.1f} Cores)")
352    return True
353
354
355def _selftest() -> bool:
356    """The full headless gate: the boot check, then the scripted run slice.
357
358    The caller points ``SHRIKE_DATA_DIR`` at a throwaway directory, so the gate
359    never touches a real profile.
360    """
361    if not _boot_check():
362        return False
363    if not _run_slice():
364        return False
365    print("SELFTEST: PASS")
366    return True
367
368
369#: The settings key holding the player's frame cap, in frames per second. Zero
370#: (and an absent key) means "follow the display", which is what the game does
371#: with no cap set: vsync-gated and otherwise uncapped.
372FRAME_CAP_SETTING = "frame_cap"
373#: The settings key holding vertical sync. Unlike the cap this one is live
374#: (``App.set_vsync``), and the settings screen flips it there and then; it is
375#: still read here so the window opens the way the player left it.
376VSYNC_SETTING = "vsync"
377
378
379def resolve_frame_cap(argv: list[str] | None = None) -> int:
380    """The frame cap to run at: ``--fps`` if given, else the saved setting.
381
382    Uncapped is not free. Without a cap the loop runs at whatever the panel
383    refreshes at, so a 144 Hz laptop does two and a half times the work of a
384    sixty-frame cap for a twin-stick game that reads no better for it, and
385    spends the difference in heat and battery.
386
387    Read from disk here so the window opens the way the player left it. That is
388    all this does: ``App.target_fps`` is writable mid-run, so the settings
389    screen applies a new cap on the next frame without a relaunch. This is the
390    boot half of the same setting.
391    """
392    argv = list(sys.argv[1:] if argv is None else argv)
393    if "--fps" in argv:
394        index = argv.index("--fps")
395        value = argv[index + 1] if index + 1 < len(argv) else ""
396        if not value.isdigit():
397            raise SystemExit("--fps expects a frame rate, for example --fps 60 (0 follows the display)")
398        return int(value)
399    from shrike import save
400
401    try:
402        return max(0, int(save.stored_settings().get(FRAME_CAP_SETTING, 0)))
403    except TypeError, ValueError:
404        return 0
405
406
407def resolve_vsync() -> bool:
408    """Whether to open the window with vertical sync, off the saved setting.
409
410    On by default and on for any profile that has never been asked, which is
411    the setting a laptop panel wants; the settings screen can turn it off
412    mid-session because this one the engine does expose a setter for.
413    """
414    from shrike import save
415
416    return bool(save.stored_settings().get(VSYNC_SETTING, True))
417
418
419def main(argv: list[str] | None = None) -> None:
420    from simvx.graphics import App
421
422    cap = resolve_frame_cap(argv)
423    app = App(
424        title="SHRIKE",
425        width=WINDOW_WIDTH,
426        height=WINDOW_HEIGHT,
427        target_fps=cap or None,
428        vsync=resolve_vsync(),
429    )
430    app.run(create_entry_scene())
431
432
433if __name__ == "__main__":
434    if "--test" in sys.argv:
435        from simvx.core.testing import run_selftest
436
437        with tempfile.TemporaryDirectory(prefix="shrike-selftest-") as data_dir:
438            os.environ.setdefault("SHRIKE_DATA_DIR", data_dir)
439            status = run_selftest(_selftest)
440        sys.exit(status)
441    main()