I decided to try out using Ooui which is a framework for creating WebAssemblies using C# and Xamarin Forms. You can learn about it here https://github.com/praeclarum/Ooui and at http://praeclarum.org/post/171899388348/oouiwasm-net-in-the-browser. The general idea is write C# and compile it to WebAssembly and run that in the browser instead of JavaScript. I could have gone the Blazor route, but my internet has been flaky and I didn't want to download the VS2017 preview release.
After going thru the Ooui Getting Started, I needed to make something. WHAT? I decided a text based game like Zork would be nice.
Zork is a text based piece of interactive fiction made by Infocom. Zork is inspired by Collosal Cave a game written in Fortran for DEC computers. Selling games for DEC computers didn't make a lot of sense, so Infocom decided to create a virtual machine called ZMachine. This ZMachine would let them use the same code (image) across multiple platforms. Back in the day these platforms were personal computers like the C64, IBM, Apple II and lots of others.
Zork appears to be abandonware and is freely distributable at a lot of sites. There are a lot of unofficial versions, including this one.
Naturally I didn't want to spend a lot of time creating my own ZMachine thus I googled and I found this Adeimantius Z-Machine. Downloaded it and ported it over to .Net core which was super simple. Ran it as a console app and it worked! I knew at some point I would need HTML DOM elements talking to this thing so I was going to need to change it.
namespace zmachine.Tests
{
public class BasicTests
{
private readonly ITestOutputHelper _output;
public BasicTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void PlayZork()
{
var io = new SimpleInputOutput();
var machine = new Machine(Zork1.GetData(), io);
WaitForOutput(machine, io);
Assert.True(ProcessCommand(machine, io, "look").Contains("West of House"));
Assert.True(ProcessCommand(machine, io, "open mailbox").Contains("leaflet"));
Assert.True(ProcessCommand(machine, io, "read leaflet").Contains("(Taken)"));
Assert.True(ProcessCommand(machine, io, "inventory").Contains("A leaflet"));
}
private string ProcessCommand(Machine machine, SimpleInputOutput io, string text)
{
io.Output = "";
io.WaitingForInput = false;
_output.WriteLine(">{0}", text);
machine.processText(text);
return WaitForOutput(machine, io);
}
private string WaitForOutput(Machine machine, SimpleInputOutput io)
{
while (!io.WaitingForInput && !machine.isFinished())
{
machine.processInstruction();
}
_output.WriteLine(io.Output.Substring(0, io.Output.Length-1));
return io.Output;
}
}
}
Now I figured if I could test the thing, I could use the Ooui to make it work in a web browser.
OOUI is in its infancy and changes fast. The problems I am pointing out here are probably already fixed. I was using 0.8.186
Time to wire it up to the Ooui. I added a new tool called the OouiConsole under tools. And this is the content of that Program.cs.
using System;
using Ooui;
using WebAssembly;
using zmachine;
using ZorkConsole;
namespace OouiConsole
{
class Program
{
/* TO RUN THIS LOOK IN THE FOLLOWING FOLDER...
* tools\OouiConsole\bin\Debug\netcoreapp2.0\dist
* Load the index.html in a web browser. It compiles down to a WASM.
*/
private static Span _area;
static void Main()
{
//Start Game
var io = new SimpleInputOutput();
var machine = new Machine(Zork1.GetData(), io);
_area = new Span();
var frm = new Form();
var textInput = new TextInput();
_area.Text = "";
frm.Submit += (sender, eventArgs) =>
{
DisplayOutput(ProcessCommand(machine, io, textInput.Value));
textInput.Value = ""; //DOESN'T WORK RIGHT
//value doesn't change so...
var js = string.Format("document.getElementById(\"{0}\").value = \"\";window.scrollTo(0,document.body.scrollHeight);", textInput.Id);
Runtime.InvokeJS(js);
};
var container = new Paragraph();
var p1 = new Paragraph();
var p2 = new Paragraph();
p1.AppendChild(_area);
frm.AppendChild(textInput);
p2.AppendChild(frm);
container.AppendChild(p1);
container.AppendChild(p2);
// Publish a root element to be displayed
UI.Publish("/", container);
DisplayOutput(WaitForOutput(machine, io));
}
private static string ProcessCommand(Machine machine, SimpleInputOutput io, string text)
{
io.Output = "";
io.WaitingForInput = false;
var p = new Paragraph {Text = string.Format(">{0}", text.Replace("\r\n", "
"))};
_area.AppendChild(p);
machine.processText(text);
return WaitForOutput(machine, io);
}
private static string WaitForOutput(Machine machine, SimpleInputOutput io)
{
if (machine.isFinished())
{
return "Game crashed. Refresh to start over, and don't do that again.";
}
while (!io.WaitingForInput && !machine.isFinished())
{
machine.processInstruction();
}
return io.Output;
}
private static void DisplayOutput(string s)
{
var stringSeparators = new[] { Environment.NewLine };
var lines = s.Substring(0, s.Length - 1).Split(stringSeparators, StringSplitOptions.None);
foreach (var line in lines)
{
var p = new Paragraph(line);
_area.AppendChild(p);
}
}
}
}
Basically I have a form containing a TextArea called _area and a TextField (Input with the type of text) called textInput.
So what problems did I have now?
var js = string.Format("document.getElementById(\"{0}\").value =
\"\";window.scrollTo(0,document.body.scrollHeight);", textInput.Id);
Runtime.InvokeJS(js);
That Javascript also clears out the TextField value.
I took the contents of the dist directory and added some CSS to it.
I also added some extra text so it looks like.
body { background-color: black; color: springgreen; font-family: "Lucida Console"; }
input { background-color: black; color: springgreen; font-family: "Lucida Console"; }
Everything for the WebAssembly is just static pages, I should be able to push this up to my website hosted via Azure and it will be available for the world! I did just that, went there with my browser but it didn't work.
I got an error in my console.
I noticed it said "has unsupported MIME type", I figured the problem here was that the server didn't know the mime type for the .wasm files. I added a web.config to the folder and pushed again.
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".wasm"/>
<mimeMap fileExtension=".wasm" mimeType="application/wasm" />
</staticContent>
</system.webServer>
</configuration>
And problem solved!
It's available here.
This is literally C# compiled down to WebAssembly and running in your browser. There is a very small amount of HTML and Javascript. Almost none. That is NUTS!
Maybe once my internet stops flaking out I will try to port this over to Blazor.
I wish I had a chance to try out the Xamarin Forms. The Ooui website has demos of them. They look very interesting.
There appears to be support for the Canvas element. I definitely need to look into that. I wonder how different the implementation is versus System.Drawing?
I did have some problems when building the OouiConsole application. If you get an error like.
Could not find a part of the path 'C:\Users\something\AppData\Local\Temp\mono-wasm-ddf4e7be31b\bcl\Facades'. OouiConsole C:\Users\something\.nuget\packages\ooui.wasm\0.8.186\build\netstandard2.0\Ooui.Wasm.targets 17
Delete that directory, aka C:\Users\something\AppData\Local\Temp\mono-wasm-ddf4e7be31b. When it builds it will recreate it. That's the build resources it uses to create the WebAssembly stuff.