Let's learn Dialog: flipping the map

The recent thread on flipping the map in Inform 7 got me thinking that this is a perfect use case for the way Dialog lets you assign conditions to any rule. So here’s my take on doing the same in Dialog; I’d be curious to know if anyone else has an alternate way of doing it.

(current player #player)
(#player is #in #lab)

#lab
(room *)
(name *) Lab
(look *) An untidy laboratory. The workshop is to
    the south and a small closet is to the (west flippable).
(from * go west flippable to #closet)
(from * go #south to #workshop)

#closet
(room *)
(name *) Closet
(look *) A cramped storage closet. The exit is to
    the (east flippable).
(from * go east flippable to #lab)

#workshop
(room *)
(name *) Workshop
(look *) A dusty workshop. The lab lies north and a
    small doorway opens to the (east flippable).
(from * go #north to #lab)
(from * go east flippable to #kitchenette)

#kitchenette
(room *)
(name *) Kitchenette
(look *) A small kitchenette. The only exit is
    (west flippable).
(from * go west flippable to #workshop)

~(world is flipped)

(west flippable)
    (if) (world is flipped) (then) east
    (else) west (endif)

(east flippable)
    (if) (world is flipped) (then) west
    (else) east (endif)

(from $A go #west to $B)
    (from $A go west flippable to $B)
    ~(world is flipped)

(from $A go #west to $B)
    (from $A go east flippable to $B)
    (world is flipped)

(from $A go #east to $B)
    (from $A go east flippable to $B)
    ~(world is flipped)

(from $A go #east to $B)
    (from $A go west flippable to $B)
    (world is flipped)

(perform [jump])
    (if) ~(world is flipped) (then)
        (now) (world is flipped)
    (else)
        (now) ~(world is flipped)
    (endif)
    Everything looks different!
    (try [look])
10 Likes

This is very cool, thanks for sharing!

Woah,I didn’t know you could do this in Dialog. It’s a super interesting lamguage - I always thought of it as a branch of Inform 7, but this feels very different!

1 Like

Very nice! It definitely shows off one of Dialog’s strengths: it lets many things be computed that Inform requires to be assigned.

This is also one of its weaknesses: unlike Inform, it’s very hard to rewrite arbitrary exits on the fly in Dialog (e.g. if you can tunnel through walls). But overall I think it’s a good tradeoff.

It’s good! Nevertheless I will suggest some minor tweaks…

(current player #player)
(#player is #in #lab)

#lab
(room *)
(name *) Lab
(look *) An untidy laboratory. The workshop is to
    the south and a small closet is to the (dir from * to #closet).
(from * go #west flippable to #closet)
(from * go #south to #workshop)

#closet
(room *)
(name *) Closet
(look *) A cramped storage closet. The exit is to
    the (dir from * to #lab).
(from * go #east flippable to #lab)

#workshop
(room *)
(name *) Workshop
(look *) A dusty workshop. The lab lies north and a
    small doorway opens to the (dir from * to #lab).
(from * go #north to #lab)
(from * go #east flippable to #kitchenette)

#kitchenette
(room *)
(name *) Kitchenette
(look *) A small kitchenette. The only exit is
    (dir from * to #workshop).
(from * go #west flippable to #workshop)

~(world is flipped)

(dir from $A to $B)
    (from $A go $Dir to $B)
    (name $Dir)

(from $A go $Dir to $B)
    ~(world is flipped)
    (from $A go $Dir flippable to $B)

(from $A go #east to $B)
    (world is flipped)
    (from $A go #west flippable to $B)

(from $A go #west to $B)
    (world is flipped)
    (from $A go #east flippable to $B)

(perform [jump])
    (if) ~(world is flipped) (then)
        (now) (world is flipped)
    (else)
        (now) ~(world is flipped)
    (endif)
    Everything looks different!
    (try [look])
2 Likes

(Most of what I did there is replace some hardcoded predicates, like (east flippable), with slightly more dynamic ones – in that case, querying the direction between two rooms and printing it. I don’t think it makes a ton of difference in your example, but would help if eg. you had passages north-east that ought to become north-west when the world is flipped. As always up to personal taste of course!)

1 Like

Yeah, I wondered if I should do something more generic than having fixed predicates for east and west. I hadn’t thought about dynamically querying the map connections to figure out what direction to display, which is a neat trick - but you might still want to be able to use other directions in the text as well?

The sun is rising in the (east flippable).
1 Like

When I coded something like this, it was not the map but the objects on it, including the protagonist, that could be flipped, so I used a per-object flag ($ is flipped). From the player’s perspective it makes little difference whether a map flip hinges on a per-object flag; after all, from the perspective of a flipped protagonist, the world would appear to be flipped. But if the author decides part of the way into a project, “Actually, it would be more interesting if it was the objects that could be flipped,” they will be glad to have used a per-object flag from the start.

1 Like

Totally!

Yet another option would be to have a predicate (#east flips to #west), etc., which encodes the raw information, and have everything delegate to that. That’s probably the most Prolog-ish version, since logic programming loves to make little databases of facts and infer things from those facts. Probably overkill for this little example though!

2 Likes

This got me thinking about dynamic maps in general, so I gave “dig” a try. It did indeed turn out to be a pain. I ran into the two variable limit on dynamic predicates, so I made the three-variable from rule depend on two variable dynamic predicates. I also ran into the n-squared problem with two variables that couldn’t be compiled to zblorb, so I made a separate rule for each direction in the two-way tunnel relationship.

Full code here: https://github.com/tzbits/learn-dialog/blob/main/prison.dg

The interesting snippets:

Summary
(excavated $Room)
	(from $Room go $ to $)

(fully excavated $Room)
	(starting dig node $Room)
	(from $Room go $ to $B)
	(from $B go $ to $Room)

(fully excavated $Room)
	(collect $Dir)
		*(from $Room go $Dir to $)
	(into $List)
	($List = [$ | $Rest])
	~($Rest = [])

(from $A go #east to $B)
	(tunnel $A east to $B)

(from $A go #west to $B)
	(tunnel $A west to $B)

(from $A go #north to $B)
	(tunnel $A north to $B)

(from $A go #south to $B)
	(tunnel $A south to $B)

(from $A go #down to $B)
	(tunnel $A down to $B)

(from $A go #up to $B)
	(tunnel $A up to $B)

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

(generate 6 (dig node $))

(room *(dig node $))
(name $Room)
	(dig node $Room)
	tunnel

(unused dig nodes $List)
	(collect $Node)
		*(dig node $Node) ~(starting dig node $Node) ~(excavated $Node)
	(into $List)

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

(perform [dig $Dir])
	(current room $Room)
	(unused dig nodes $Unused)
	($Unused = [$NextRoom | $])
	(if) ($Dir = #north) (then)
		(now) (tunnel $Room north to $NextRoom)
		(now) (tunnel $NextRoom south to $Room)
	(elseif) ($Dir = #south) (then)
		(now) (tunnel $Room south to $NextRoom)
		(now) (tunnel $NextRoom north to $Room)
	(elseif) ($Dir = #east) (then)
		(now) (tunnel $Room east to $NextRoom)
		(now) (tunnel $NextRoom west to $Room)
	(elseif) ($Dir = #west) (then)
		(now) (tunnel $Room west to $NextRoom)
		(now) (tunnel $NextRoom east to $Room)
	(elseif) ($Dir = #up) (then)
		(now) (tunnel $Room up to $NextRoom)
		(now) (tunnel $NextRoom down to $Room)
	(elseif) ($Dir = #down) (then)
		(now) (tunnel $Room down to $NextRoom)
		(now) (tunnel $NextRoom up to $Room)
	(else)
		(fail)
	(endif)
	Determined to escape, you begin digging with the spoon, and slowly extend
	the tunnel (name $Dir).

1 Like

Yeah, I think the way I’d do it would be to have a list stored in a global variable, [source dir dest source dir dest...], and iterate through that every time an exit needs checking. That takes O(n) time but also only O(n) space, and space is precious on Z-machine.

3 Likes

Good idea, thanks for the suggestion! It also makes the program shorter.

prison.dg
(intro)
	(try [look])

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

(global variable (depth 0))

(before [go #down])
	(depth $N) ($N plus 1 into $Np1)
	(now) (depth $Np1)

(before [go #up])
	(depth $N) ($N minus 1 into $Nm1)
	(now) (depth $Nm1)

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

#spoon
(item *)
(name *)        spoon
(* is #in #prison-cell)
(* is handled)

(prevent [dig $])
	~(#spoon is #heldby #player)
	If only you could find something to dig with.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

#prison-cell
(room *)
(dig-node *)
(starting-dig-node *)
(name *)        prison cell
(heads *)       cell
(look *)        The prison cell has four walls and seemingly no way out.

(prevent [leave * $Dir])
	~($Dir = #down)
	It's no use. You are locked in here.

(prevent [dig $Dir])
	(current room *)
	(#spoon is #heldby #player)
	~($Dir = #down)
	You can't dig (name $Dir) from here.

(prevent [dig $Dir])
	(current room *)
	(#spoon is #heldby #player)
	~($Dir = #down)
	You can't dig that way from here.

(narrate failing to leave * #down)
	~(#spoon is #heldby #player)
	If only you could find something to dig with.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

(flip [#north #south])
(flip [#south #north])
(flip [#east #west])
(flip [#west #east])
(flip [#up #down])
(flip [#down #up])

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

(global variable (tunnel-list []))

(tunnel $Room $Dir $Dest)
	(tunnel-list $List)
	(tunnel-search $Room $Dir $Dest $List)

(tunnel-search $Room $Dir $Dest $List)
	([$Room $Dir $Dest | $Tail] = $List)
	(or) { ([$ $ $ | $Tail] = $List) (tunnel-search $Room $Dir $Dest $Tail) }

(excavated $Room)
	(tunnel $Room $ $) (or) (tunnel $ $ $Room)

(fully-excavated $Room)
	(starting-dig-node $Room)
	(tunnel $Room $ $)

(fully-excavated $Room)
	(tunnel $Room $ $)
	(tunnel $ $ $Room)

(from $A go $Dir to $B)
	(dig-node $A)
	(tunnel $A $Dir $B)
	(or) { (flip [$Dir $Back]) (tunnel $B $Back $A) }

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

(generate 6 (dig-node $))

(room *(dig-node $))
(name $Room)
	(dig-node $Room)
	tunnel

(look $Room)
	(dig-node $Room)
	You are in a small spoon-dug tunnel.
	(if) (tunnel $Room $Dir1 $) (then)
		The tunnel continues (name $Dir1).
	(endif)
	(if) (tunnel $ $Dir2 $Room) (then)
		(flip [$Dir2 $Back])
		(Name $Back) leads back.
	(endif)

(interface (unused-dig-nodes $>List))

(unused-dig-nodes $List)
	(collect $Node)
		*(dig-node $Node) ~(starting-dig-node $Node) ~(excavated $Node)
	(into $List)

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

(grammar [dig [direction]] for [dig $])
(grammar [dig] for itself)

(perform [dig])
	In which direction do you want to dig?
	(asking for direction in [dig []])

(prevent [dig $])
	(current room $Room)
	(fully-excavated $Room)
	You already dug here.

(perform [dig $Dir])
	(unused-dig-nodes [])
	You try digging (name $Dir), but hit a wall of rock that prevents further
	progress.

(perform [dig $Dir])
	(current room $Room)
	(unused-dig-nodes $Unused)
	([$Dest | $] = $Unused)
	(tunnel-list $TunnelList)
	(now) (tunnel-list [$Room $Dir $Dest | $TunnelList])
	Determined to escape, you begin digging with the spoon, and slowly extend
	the tunnel (name $Dir).

(instead of [go #up])
	(current room $Room)
	(from $Room go #up to $Dest)
	~(starting-dig-node $Dest)
	(depth $depth)
	($depth = 0)
	Your spoon breaks through to daylight. You poke your head out of the
	tunnel.
	(par) There is a prison guard here.
	(par) He takes you back to prison.
	(game over { Game Over. })

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

#player
(current player *)
(#player is #in #prison-cell)
2 Likes