Monday, December 26, 2016

Project Downloads

Source code and binaries of some of my projects are now available at:
http://kamoly.space/projects/

Current projects available include:
http://kamoly.space/projects/comcopy/
http://kamoly.space/projects/gps/
http://kamoly.space/projects/pebble/all_info_as_text_watch_face/
http://kamoly.space/projects/pebble/rpg/

If you happen to download some of these and find them useful, please leave a comment and let me know!

Thanks!

Sunday, December 11, 2016

Retro Computing: Serial File Transfer v1.0

UPDATE: COMCopy binaries and source are now available at:
http://kamoly.space/projects/comcopy/

Getting large / large numbers of files from a modern computer to my old 486 was easy enough (CD-ROM), but since the 486 doesn't have a CD writer the reverse was very difficult. I wanted a way to simply and directly copy from my old computer to my modern one.

Options were to use DOSBox with InterLnk/InterSrv or one of the other many software products out there, but all the options I was seeing were a pain to use. I just wanted a simple mechanism to copy files; not dig through 5 levels of menus or run 25 year old software in an emulator. Embracing any excuse to fire up my old copy of QBasic 4.5, I got to work on making my own.

It's currently just one way (from the old computer to modern), but I'm very happy with how it turned out. There are literally -no- config checkboxes or menus to get it to work. You can change the default folder that files are copied to if you really want, but even that's totally optional.

You run the server program on the modern computer.


It automatically detects the first available COM port and connects to it. You don't even have to click Connect. (The Port drop down and Connect/Disconnect buttons are available on the minuscule chance you actually have 2 COM ports on your modern computer.)

The copy program for the old computer has no configuration at all. For convenience, you'll want to set the PATH environment variable to the location of the executable (or copy the executable to an existing PATH directory).

The PATH environment variable allows you to run DOS executables from any directory you're currently in. When you type in a program name, DOS looks for that file in your current directory and the directories listed in your PATH variable. It's when it fails to see it in either location that it report "Bad command or file name".

Honestly setting the path isn't required, but it makes it super convenient. I just modified my AUTOEXEC.BAT to include a path to my QB45 directory so anything I compile will be readily accessible.


Typing COMCOPY on its own shows help.


Instructions are pretty simple, type:
COMCOPY <source file> <destination path> where <destination path> includes either COM1 or COM2 and an optional target directory/file.

Let's copy over the source code!


It will connect to the server, copy the file, and report the status as it goes along.

When done it will report a simple checksum so you can confirm the file matches on both sides.




You can even specify folders or a full specific path to copy to. I'll make a backup of my drivers...



The biggest technical shortcoming is the checksum calculation. The file copy does a single checksum calculation at the end, but it is possible for it to report an equal value if the failures aligned just right (extremely unlikely though). Better methods would be to continuously check during the copy so it can automatically retransmit incorrect blocks of data. I've never had a copy error (6 foot serial cable at 19200 baud seems pretty reliable), so I'm not overly concerned about it.

This has been a good version 1.0. Future versions need to support wildcards (*.* for example), directory recursion, and handling of a source absolute path. And obviously copying from the modern computer to the old one. For now it satisfies my needs so I'll likely move on to other projects. I hope it is of use to others!


Full source code of the QBasic program (for the DOS based computer):


DEFINT A-Z

commandArgs$ = COMMAND$ '"test.txt COM1"

IF commandArgs$ = "" THEN
   PRINT
   PRINT "COM Copy"
   PRINT "Copy a local file to a destination via the COM port."
   PRINT
   PRINT "Syntax: COMCOPY <FILENAME.EXT> <PORT>"
   PRINT "        COMCOPY <FILENAME.EXT> <PORT>:FILENAME.EXT"
   PRINT "        COMCOPY <FILENAME.EXT> <PORT>:FOLDER\"
   PRINT "        COMCOPY <FILENAME.EXT> <PORT>:FOLDER\FILENAME.EXT"
   PRINT "<PORT> options: COM1 COM2"
   PRINT
   PRINT "Examples:"
   PRINT "COMCOPY MYDATA.XML COM1"
   PRINT "COMCOPY MYDATA.XML COM2:22NOV16.XML"
   PRINT "COMCOPY MYDATA.XML COM1:DATA\"
   PRINT "COMCOPY MYDATA.XML COM2:DATA\22NOV16.XML"
   END
END IF

' Parse the source path.
commandIndex = 1
sourcePath$ = ""
FOR commandIndex = commandIndex TO LEN(commandArgs$)
   indexChar$ = MID$(commandArgs$, commandIndex, 1)
   IF indexChar$ = " " THEN
      commandIndex = commandIndex + 1
      EXIT FOR
   END IF
   sourcePath$ = sourcePath$ + indexChar$
NEXT commandIndex

' Ensure a file name was given.
IF sourcePath$ = "" THEN
   PRINT "Need source file name!"
   END
END IF

' Parse the destination port.
destinationPort$ = ""
FOR commandIndex = commandIndex TO LEN(commandArgs$)
   indexChar$ = MID$(commandArgs$, commandIndex, 1)
   IF indexChar$ = ":" THEN
      commandIndex = commandIndex + 1
      EXIT FOR
   END IF
   destinationPort$ = destinationPort$ + indexChar$
NEXT commandIndex

IF destinationPort$ <> "COM1" AND destinationPort$ <> "COM2" THEN
   PRINT "Invalid Port:" + destinationPort$
   END
END IF

' Parse the destination path.
destinationPath$ = ""
IF commandIndex <= LEN(commandArgs$) THEN
   destinationPath$ = MID$(commandArgs$, commandIndex, LEN(commandArgs$) - commandIndex + 1)
END IF

IF destinationPath$ = "" THEN
   ' No destination path given, use source path.
   destinationPath$ = sourcePath$
ELSEIF RIGHT$(destinationPath$, 1) = "\" THEN
   ' Destination path ends with \, append source file to the end.
   destinationPath$ = destinationPath$ + sourcePath$
END IF

PRINT
PRINT "COM Copy, ESC to cancel"

' Open file for binary transfer.
OPEN sourcePath$ FOR BINARY AS #2
sourceLength& = LOF(2)

' Ensure the file was found / non-zero.
IF sourceLength& = 0 THEN
   PRINT "Nothing to copy!"
   END
END IF

' Open communications (19200 baud, no parity, 8-bit data,
' 1 stop bit, 256-byte input buffer):
' DS0 - Set DSR timeout in milliseconds to 0, effectively off.
' CS0 - Set CTS timeout in milliseconds to 0, effectively off.
' RS - Disable detection of RTS (request to send).
' QBasic 4.5 max baud rate is 19200
OPEN destinationPort$ + ":19200,N,8,1,DS0,CS0,RS" FOR RANDOM AS #1 LEN = 256

' Clear the incoming buffer.
IF NOT EOF(1) THEN
   ' LOC(1) gives the number of characters waiting:
   ModemInput$ = INPUT$(LOC(1), #1)
END IF

' Send initial byte to establish connection.
PRINT #1, "*";

' Need short delay before sending file to establish connection.
SLEEP 1

' Receive the connection response / clear the incoming buffer.
IF NOT EOF(1) THEN
   ModemInput$ = INPUT$(LOC(1), #1)
   IF ModemInput$ = "" THEN
      PRINT "No response from server!"
      END
   END IF
ELSE
   PRINT "No response from server!"
   END
END IF

' Send the destination path.
PRINT #1, destinationPath$;

' Send delimiter.
PRINT #1, "*";

' Send the length we'll be sending.
PRINT #1, STR$(sourceLength&);

' Send a delimiter.
PRINT #1, "*";

PRINT "Sending file "; sourcePath$; ", "; STR$(sourceLength&); " bytes"
PRINT "==10%==20%==30%==40%==50%==60%==70%==80%==90%=100%"

' Setup reporting increment. One * every 2% sent.
byteStatusIncrement! = sourceLength& / 50
statusMarks = 0
checkSum& = 0

' Transfer the file.
FOR fileIndex& = 1 TO sourceLength& STEP 2

   ' Get the data from the file. (This actually gets 2 bytes.)
   GET 2, , dataByte
  
   ' Sends those 2 bytes to the serial port.
   PUT 1, , dataByte

   ' Calculate a simple checksum based on the data sent.
   checkSum& = checkSum& + dataByte
  
   ' Keep the checksum bounded so we don't exceed a long int.
   IF checkSum& > 32767 THEN
      checkSum& = checkSum& - 32767
   ELSEIF checkSum& < -32768 THEN
      checkSum& = checkSum& + 32768
   END IF

   IF INKEY$ = CHR$(27) THEN
      ' User pressed ESC key.
      PRINT "Transfer aborted."
      'EXIT FOR
      END
   END IF

   ' Show indication for each 2% done.
   statusMarksToShow = fileIndex& / byteStatusIncrement!
   WHILE statusMarks < statusMarksToShow
      PRINT "*";
      statusMarks = statusMarks + 1
   WEND

NEXT fileIndex&

' Show final 100% mark.
WHILE statusMarks < 50
   PRINT "*";
   statusMarks = statusMarks + 1
WEND

' Delay to ensure data finishes being sent.
SLEEP 1

PRINT
PRINT "Transfer Complete, Checksum: " + STR$(checkSum&)

' Close the file.
CLOSE 2

' Close the serial port.
CLOSE 1

END


Full source code of the VB.NET program (for the modern Windows based computer):

Imports System.IO.Ports
Imports System.Text

Public Class FormMain

    ''' <summary>
    ''' The serial port object that will communicate.
    ''' </summary>
    ''' <remarks></remarks>
    Private WithEvents connectedSerialPort As New SerialPort()

    Private Class TransferModel
        Public Enum TransferStateEnum As Integer
            Waiting = 0
            ReceivingFilename = 1
            ReceivingSize = 2
            ReceivingData = 3
        End Enum

        Private state As TransferStateEnum = TransferStateEnum.Waiting
        Private destinationPath As String = ""
        Private fileSizeString As String = ""
        Private fileSize_bytes As Integer = 0
        Private lastUpdate_seconds As Double = 0
        Private data As Byte()
        Private dataIndex As Integer = 0
        'Private checkSum As Integer = 0

        Public statusMessage As String = ""

        ''' <summary>
        ''' Local folder on the server that transfers will be saved to.
        ''' </summary>
        ''' <remarks></remarks>
        Public localFolder As String = "C:\COMCOPY\"

        ''' <summary>
        ''' Return a high resolution system time stamp.
        ''' </summary>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Private Function currentSystemTime() As Double
            Dim elapsedTicks As Long = Date.Now.Ticks
            Return elapsedTicks / 10000000.0
        End Function

        ''' <summary>
        ''' Clear temporary variables used in tranfers as reset the transfer state.
        ''' </summary>
        ''' <remarks></remarks>
        Public Sub clear()
            Try
                Me.state = TransferStateEnum.Waiting
                Me.destinationPath = ""
                Me.fileSizeString = ""
                Me.fileSize_bytes = 0
            Catch ex As Exception
            End Try
        End Sub

        ''' <summary>
        ''' Check timeout for transfers that have gone stale.
        ''' </summary>
        ''' <remarks></remarks>
        Public Sub checkTimeout()
            Try
                If Me.state <> TransferStateEnum.Waiting Then
                    If Me.currentSystemTime() > (Me.lastUpdate_seconds + 3) Then
                        ' Timed out!
                        Me.clear()
                    End If
                End If
            Catch ex As Exception
            End Try
        End Sub

        ''' <summary>
        ''' Process a byte off the network as appropriate for our current transfer state.
        ''' Return if bytes need to be sent back across the network.
        ''' </summary>
        ''' <param name="newByte"></param>
        ''' <param name="responseBytes"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Function processIncomingByte(ByVal newByte As Byte, ByRef responseBytes As Byte()) As Boolean
            Dim responseBytesPending As Boolean = False
            Try
                Me.lastUpdate_seconds = Me.currentSystemTime()

                Select Case Me.state
                    Case TransferStateEnum.Waiting
                        If newByte = 42 Then
                            ' Initial byte to establish connection. Return a confirmation byte.
                            responseBytes = {newByte}
                            responseBytesPending = True
                            Me.state = TransferStateEnum.ReceivingFilename
                        End If
                    Case TransferStateEnum.ReceivingFilename
                        If newByte = 42 Then
                            ' Done with filename.
                            Me.destinationPath = Trim(destinationPath)
                            Me.state = TransferStateEnum.ReceivingSize
                        ElseIf newByte <> 0 Then
                            Me.addDestinationPathCharacter(Convert.ToChar(newByte))
                        End If
                    Case TransferStateEnum.ReceivingSize
                        If newByte = 42 Then
                            ' Done with size.
                            Me.prepareForTransfer()
                            Me.state = TransferStateEnum.ReceivingData
                        Else
                            Me.addFileSizeCharacter(Convert.ToChar(newByte))
                        End If
                    Case TransferStateEnum.ReceivingData
                        Me.addData(newByte)
                        If Me.dataIndex >= Me.fileSize_bytes Then
                            ' Done with transfer.
                            Me.completeTransfer()
                            responseBytesPending = False
                        End If
                End Select
            Catch ex As Exception
            End Try
            Return responseBytesPending
        End Function

        ''' <summary>
        ''' Add the latest incoming byte as a destination path character.
        ''' </summary>
        ''' <param name="newCharacter"></param>
        ''' <remarks></remarks>
        Private Sub addDestinationPathCharacter(ByVal newCharacter As Char)
            Try
                Me.destinationPath += newCharacter
            Catch ex As Exception
            End Try
        End Sub

        ''' <summary>
        ''' Add the latest incoming byte as a file size character.
        ''' </summary>
        ''' <param name="newCharacter"></param>
        ''' <remarks></remarks>
        Private Sub addFileSizeCharacter(ByVal newCharacter As Char)
            Try
                Me.fileSizeString += newCharacter
            Catch ex As Exception
            End Try
        End Sub

        ''' <summary>
        ''' Call to prepare for transfer. Initialize the file data array and reset the checksum.
        ''' </summary>
        ''' <remarks></remarks>
        Private Sub prepareForTransfer()
            Try
                Me.fileSizeString = Trim(Me.fileSizeString)
                Me.fileSize_bytes = Val(Me.fileSizeString)
                Me.data = New Byte(Me.fileSize_bytes - 1) {}
                Me.dataIndex = 0
                'Me.checkSum = 0
            Catch ex As Exception
            End Try
        End Sub

        ''' <summary>
        ''' Add incoming data to the new file data array.
        ''' </summary>
        ''' <param name="newByte"></param>
        ''' <remarks></remarks>
        Private Sub addData(ByVal newByte As Byte)
            Try
                Me.data(Me.dataIndex) = newByte
                Me.dataIndex += 1
            Catch ex As Exception
            End Try
        End Sub

        ''' <summary>
        ''' Call when the transfer has completed. This will save the file to the given path and
        ''' reply a checksum value that can be used to ensure successful transfer.
        ''' </summary>
        ''' <remarks></remarks>
        Private Sub completeTransfer()
            Try
                Dim fileFullPath As String = localFolder + destinationPath
                System.IO.File.WriteAllBytes(fileFullPath, Me.data)

                Dim checkSumValue As Integer = 0
                For checkSumIndex As Integer = 0 To (Me.data.Length - 1) Step 2
                    If checkSumIndex = Me.data.Length - 1 Then
                        ' Last byte.
                        checkSumValue += Me.data(checkSumIndex)
                    Else
                        Dim data1 As Int16 = Me.data(checkSumIndex + 1)
                        Dim data0 As Int16 = Me.data(checkSumIndex)
                        Dim newData As Int16 = ((data1 << 8) Or data0)
                        checkSumValue += newData
                    End If
                    If checkSumValue > 32767 Then
                        checkSumValue -= 32767
                    ElseIf checkSumValue < -32768 Then
                        checkSumValue += 32768
                    End If
                Next

                statusMessage = fileFullPath + ", Checksum: " + checkSumValue.ToString + vbCrLf + statusMessage
                Me.clear()
            Catch ex As Exception
            End Try
        End Sub
    End Class

    Private transferObject As TransferModel

    Public Sub New()

        ' This call is required by the designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.
        transferObject = New TransferModel
        txtLocalPath.Text = transferObject.localFolder

    End Sub

    Private Sub FormMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Try
            refreshPortsList()

            If ddlCOMPort.SelectedIndex >= 0 Then
                ' Port is found, attempt to connect to it.
                connectSerialPort()
            Else
                ' Update which controls are enabled (so disconnect button is disabled at startup).
                updateConnectedEnableFields()
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub btnConnect_Click(sender As Object, e As EventArgs) Handles btnConnect.Click
        Try
            connectSerialPort()
        Catch ex As Exception
        End Try
    End Sub

    Private Sub btnDisconnect_Click(sender As Object, e As EventArgs) Handles btnDisconnect.Click
        Try
            disconnectSerialPort()
        Catch ex As Exception
        End Try
    End Sub

    Private Sub connectedSerialPort_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles connectedSerialPort.DataReceived
        Try
            ' Received data from the serial port.
            Dim receivedSize As Integer = connectedSerialPort.BytesToRead
            Dim receiveBuffer As Byte() = New Byte(receivedSize - 1) {}
            connectedSerialPort.Read(receiveBuffer, 0, receivedSize)

            ' Check if our last transfer has timed out.
            transferObject.checkTimeout()

            If receiveBuffer.Length > 0 Then
                ' Data received, process each incoming byte in the buffer.
                Dim responseBytes As Byte() = {}
                For bufferLoop As Integer = 0 To (receiveBuffer.Length - 1)
                    If transferObject.processIncomingByte(receiveBuffer(bufferLoop), responseBytes) Then
                        ' Response bytes need to be sent, send now.
                        connectedSerialPort.Write(responseBytes, 0, responseBytes.Length)
                    End If
                Next
            End If

        Catch ex As Exception
        End Try
    End Sub

    Private Sub connectedSerialPort_ErrorReceived(sender As Object, e As SerialErrorReceivedEventArgs) Handles connectedSerialPort.ErrorReceived
        Try
        Catch ex As Exception
        End Try
    End Sub

    Private Sub btnRefreshPorts_Click(sender As Object, e As EventArgs) Handles btnRefreshPorts.Click
        Try
            refreshPortsList()
        Catch ex As Exception
        End Try
    End Sub

    ''' <summary>
    ''' Returns if the serial port is currently connected.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private ReadOnly Property isSerialPortConnected
        Get
            Return connectedSerialPort.IsOpen()
        End Get
    End Property

    ''' <summary>
    ''' Update whether or not each UI control should be enabled based on
    ''' the connection state.
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub updateConnectedEnableFields()
        Try
            Dim isConnected As Boolean = isSerialPortConnected
            btnConnect.Enabled = Not isConnected
            btnDisconnect.Enabled = isConnected
            ddlCOMPort.Enabled = Not isConnected
            btnRefreshPorts.Enabled = Not isConnected
        Catch ex As Exception
        End Try
    End Sub

    ''' <summary>
    ''' Connect to the selected serial port.
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub connectSerialPort()
        Try
            If ddlCOMPort.SelectedIndex >= 0 Then
                connectedSerialPort.PortName = ddlCOMPort.SelectedItem
                connectedSerialPort.BaudRate = 19200

                connectedSerialPort.WriteTimeout = -1
                connectedSerialPort.ReadTimeout = -1

                connectedSerialPort.ReadBufferSize = 4096
                connectedSerialPort.WriteBufferSize = 2048

                connectedSerialPort.Parity = IO.Ports.Parity.None
                connectedSerialPort.StopBits = IO.Ports.StopBits.One
                connectedSerialPort.DataBits = 8
                connectedSerialPort.Open()
            End If
            updateConnectedEnableFields()
        Catch ex As Exception
        End Try
    End Sub

    ''' <summary>
    ''' Disconnect the serial port.
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub disconnectSerialPort()
        Try
            connectedSerialPort.Close()
            updateConnectedEnableFields()
        Catch ex As Exception
        End Try
    End Sub

    ''' <summary>
    ''' Detect available serial ports and update the COM port list.
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub refreshPortsList()
        Try
            ' Keep track of the selected port so we can reselect it after fresh.
            Dim currentlySelectedPort As String = ddlCOMPort.SelectedItem

            ' Get the available ports and update the dropdown list.
            Dim availablePorts As Array = IO.Ports.SerialPort.GetPortNames()
            ddlCOMPort.Items.Clear()
            ddlCOMPort.Items.AddRange(availablePorts)

            ' Reselect the previously selected port as a default.
            If currentlySelectedPort IsNot Nothing Then
                If ddlCOMPort.Items.Contains(currentlySelectedPort) Then
                    ddlCOMPort.SelectedItem = currentlySelectedPort
                End If
            End If

            If ddlCOMPort.SelectedIndex = -1 AndAlso ddlCOMPort.Items.Count > 0 Then
                ' No port selected but one is available, default to it.
                ddlCOMPort.SelectedIndex = 0
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub btnLocalPath_Click(sender As Object, e As EventArgs) Handles btnLocalPath.Click
        Try
            Dim openFolderDialog As New FolderBrowserDialog
            openFolderDialog.SelectedPath = txtLocalPath.Text
            openFolderDialog.Description = "Select folder to save incoming transfers to."

            If openFolderDialog.ShowDialog() = Windows.Forms.DialogResult.OK Then
                txtLocalPath.Text = openFolderDialog.SelectedPath + "\"
                transferObject.localFolder = txtLocalPath.Text
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub timerStatus_Tick(sender As Object, e As EventArgs) Handles timerStatus.Tick
        Try
            txtReceivedFiles.Text = transferObject.statusMessage
        Catch ex As Exception
        End Try
    End Sub
End Class

Screenshot of the VB.NET designer:





Copyright (c) 2016 Clinton Kam
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Sunday, November 20, 2016

Office / Shop: Floor

The existing base boards around the perimeter wall of the shop were totally rotted.



I ripped those 2x4s out...



There are lots of 2x4 cross braces along the perimeter wall. I went with treated 2x6s for the bottom plates so I could build a 2x4 wall along the perimeter without interfering with the existing cross boards. And the new wall will be flush across the 6x6 columns as opposed to having a bump from the middle and corners columns.



The existing concrete floor was also in rough shape. A few major cracks...


I had never used self-leveling concrete, but this seemed like the perfect situation for it. I first applied Home Depot's SikaLevel Primer.


Then the next day I poured SikaLevel self-leveling concrete.


I'm experienced with concrete, but this was my first go with the self-leveling variety. I had no idea how much I would need for my shop space. No one seems to want to give a good rule of thumb - likely because it's a huge "it depends" situation. I could have (and probably should have) gone with quite a few more bags. If anyone else is looking to use the self-leveling concrete, you'd probably want to budget -minimum- one 50 pound bag per 20 sq ft - regardless of how level and smooth it currently is. Unfortunately the stuff is not cheap!


When pouring concrete, there is a small range between "minimum sufficient" water and "too much" water. If you see dry powder (after mixing), you've got too little water. If you see a pool of water floating at the top (after mixing), you've got too much water. The directions said to use 1 gallon of water per 50 pound bag; I did that with the first batch and it was very thick and hard to spread. For the additional bags I ended up exceeding the 1 gallon of water recommendation (but still under "too much") so future bags spread out much better.

From reading up on self-leveling concrete, it sounded like the wet concrete would seep through every tiny gap in the wall and leak out. That was a non-issue; there was no problem keeping it contained even when I mixed extra water.

I don't know how SikaLevel compares to other brands. My first "thicker" pour is a bit uneven (from me trying to spread it), but overall I'm happy with the results. Cracks are gone!


Next up is the epoxy paint for a shiny uniform finish. Unfortunately my weather window to get that done has closed. Per Rustoleum's instructions:

Apply when air (ambient) temperature is 60-85°F and relative humidity is below 80%. Concrete floors take a long time to warm up; make sure the previous day has also been at least 60°F prior to painting. Minimum floor temperature for painting is 55°F. Painting in mid-afternoon ensures the best cure. Do not apply coating if night time temperature is expected to drop below a low of 65°F (18.3°C). For best results, temperatures should not drop below 65°F for 24-48 hours after applying the coating.

None of that is happening until summer or until I can get the room insulated and heated.






Sunday, November 13, 2016

Retro Computing: Gateway 2000 486 DX4/100

Nostalgia is getting the better of me. I recently bought a Nintendo 64 along with a bunch of the must-have N64 games. And now I'm returning to the good old days of computing when keyboard was king, memory was measured in kilobytes, and plug and pray was wishful thinking.


So... I bought one of the computers I had growing up. A Gateway 2000 486 DX4/100 off E-bay. I've been playing Doom 2 and the original Grand Theft Auto on it, and they're fantastic. :-) The computer came with Windows 95, so I also installed Spaceward Ho.

If you've never played Spaceward Ho, go download it... now... I'll hear back from you in a few days how awesome it is.


Rather than continuing down memory lane, I figured I'd offer some tips for others that are going a bit retro. Computer RAM nowadays is measured in GBs, but in the 90s it was measured in MBs. Worse yet though, on DOS based machines in the 80s/early 90s (before Windows 95), you had to care about the first 1 MB of your memory.

That first MB of memory was divided into conventional and upper memory. (I found a Wiki article that covers it pretty well: https://en.wikipedia.org/wiki/DOS_memory_management ) Any ordinary application you were going to run needed conventional memory to get going. Even if you had 16 MB of total memory, those apps could easily require 580+ KB of conventional memory (out of 640 KB max). The issue is DOS and all its drivers (mouse, sound card, CD-ROM, etc) needed to be in that first MB of memory as well.

For those young ones out there, that's what "Boot Disks" were for. If you were going to play a certain game, you made a floppy disk the computer booted from for just that game so it only loaded the bare minimum necessary to keep as much conventional memory free to enable the DOS game to run.

I hated that, so I got obsessed with maximizing my conventional memory so I never had to make a boot disk. Things got easier in later DOS versions (memmaker was awesome). But just a couple key things would help:
1. Load as many of your drivers into upper memory as possible (area between 640 KB and 1 MB).
2. Find more efficient drivers that required less memory.

With the DOS command:
mem /c /p
You could see how your memory was being used.

Here's my memory on my new (old) computer after loading as much as I could to upper memory.



My available memory... 587K (601,584 bytes)
With only 1K (1,472 bytes) remaining unused in the upper memory block.

Could be better but not too shabby! I'm sure there are more efficient drivers I could load, but I've reached the conventional memory threshold such that no program should complain it's not enough.

When a DOS based computer boots up, it goes through the CONFIG.SYS then AUTOEXEC.BAT files to load up drivers and any other software that starts automatically. (So much nicer than the hundred ways a nefarious Windows program can sneak its way into starting now.)


My C:\CONFIG.SYS ...

DEVICE=C:\WINDOWS\HIMEM.SYS
DEVICE=C:\WINDOWS\EMM386.EXE RAM
DOS=HIGH,UMB
FILES=40
DEVICEHIGH=C:\WINDOWS\SETVER.EXE
DEVICEHIGH=C:\SB16\DRV\CTSB16.SYS /UNIT=0 /BLASTER=A:220 I:5 D:1 H:5
DEVICEHIGH=C:\SB16\DRV\CTMMSYS.SYS
DEVICEHIGH=C:\SB16\DRV\SBCD.SYS /D:MSCD001 /P:220

HIMEM.SYS is necessary for the computer to access high memory and memory beyond the first 1 MB.

EMM386.EXE provides expanded memory support and access to upper memory for programs. Beyond the first MB of memory, the RAM can be treated as Expanded (EMS) or Extended (XMS). Most programs of the day required EMS rather than XMS, so adding the "RAM" keyword after EMM386.EXE was necessary. Unfortunately it also eats up a lot of upper memory. The alternate keyword is "NOEMS", but then programs that require Expanded memory can't load.

HIMEM.SYS and EMM386 should be at the top; after that you can start taking advantage of your upper memory with DEVICEHIGH.

DOS=HIGH,UMB instructs DOS to load itself into the high memory just above "upper" memory. This always helps free up memory, and I've never seen a downside to it.

FILES=40 allows DOS to have 40 files open simultaneously. I've seen this value as low as 30 and as high as 40.

SETVER.EXE allows you to spoof other DOS versions so you can run older programs. This is tiny and easily fits in upper memory with DEVICEHIGH.

CTSB16.SYS, CTMMSYS.SYS, and SBCD.SYS are all for the sound card and CD-ROM. 


My C:\AUTOEXEC.BAT ...

SET SOUND=C:\SB16
SET BLASTER=A220 I5 D1 H5 P330 T6
SET MIDI=SYNTH:1 MAP:E
C:\SB16\DIAGNOSE /S
C:\SB16\MIXERSET /P /Q
LH C:\WINDOWS\COMMAND\MSCDEX.EXE /D:MSCD001 /V /M:15 /E
C:\MOUSE\CTMOUSE.EXE
@ECHO OFF
PROMPT $p$g
PATH C:\WINDOWS;C:\WINDOWS\COMMAND;C:\DOS
SET TEMP=C:\DOS

The SET command defines DOS environment variables so programs later can know how the computer is configured. As you can see in my setup, mine are mostly for the sound card.

MSCDEX.EXE gets the CD-ROM going.

CTMOUSE.EXE are mouse drivers from the FreeDOS project.

@ECHO OFF is just used to hide the following commands so startup looks cleaner.

PROMPT $p$g gives you the classic C:\ look.

PATH is very handy to set what folders DOS should look in whenever you type something into the prompt. I could create a new folder called C:\UTILITY and put a bunch of executable files in there to help me manage my computer. Rather than explicitly typing C:\UTILITY\PROGRAM.EXE each time I wanted to execute one, I just add C:\UTILITY to my PATH. Then I can type "PROGRAM.EXE" from any folder and it would execute it.

Computers with Windows 95 could actually boot straight to DOS. From there you could start Windows by typing "win". Since the vast majority of the programs I'm running on this computer are DOS based, I modified the C:\MSDOS.SYS file to always boot directly to DOS.


Just needed to change BootGUI=1 to BootGUI=0

This is what the computer starts up to!


So far I've just been transferring files to and from the computer via a 3.5" floppy and CD-ROM. However, I want something more sustainable long term. Although these old computers don't have USB, this time period was the high point of serial communication ports so support there is ready to go.

I purchased a USB->COM port adapter for my modern computer.


To physically connect the computers, I ended up with the old computer connected to a 9-pin serial cable connected to a "Null Modem" adapter (which reverses the transmit and receive wires) connected to a gender change connected to the USB->COM adapter connected to the modern computer.


(You can buy a single USB->Null Modem Serial cable that literally encompasses everything above, but I already had all the other components and I like the flexibility.)

My USB->COM adapter reported in Windows as COM3.


So I started PuTTY on the modern computer and configured it for COM3


Next I opened QBasic 4.5 on the old computer (yes, I still have my original 3.5" install disks and manual) and loaded the example TERMINAL.BAS program.


My serial cable was connected to COM1 on the old computer so that was fine in the example, but I did increase the baud rate from 1200 to 9600.

Finally I ran the TERMINAL.BAS program on the old computer and opened the connection in PuTTY on the new!



We have 2-way communication!

Now that I have this connection made, there are many solutions out there for transferring files over the serial cable. I may make my own approach; we'll see how it goes. :-)

On a lighter note, when I opened the CD-ROM drive on the computer, the seller had put an AOL CD in there. Freaking awesome! For anyone that owned a computer in the 90s, you know what's up with that.


Side thing you don't care about but I want recorded on the Internet so it will be easy for me to lookup later: You press F1 to bring up the BIOS on this Gateway computer. F1!? Right!? Not delete. Not F2. F1! Does it tell you F1 enters the BIOS at bootup? Of course not.

Saturday, October 1, 2016

Pebble Game: 2 Steps RPG Source Code

UPDATE: Complete source is now available at:
http://kamoly.space/projects/pebble/rpg/

The complete source code for my "2 Steps RPG" game is below. It's fairly commented, but it could always use more if I had the time. The code was written specifically for the Pebble watch. If it had been done on a different platform then I'd definitely have taken other approaches, but the following worked well with the performance and memory limitations of the little watch.

Someone could easily treat my code as a simple RPG game engine. Just replace my NPCs with your own, replace my locations with your own, and a maybe few other tweaks here and there and you could have a fairly distinct game ready to play.

If you make a game using my source code, then it'd be nice if you could give me some credit and a link back to my blog on wherever you publish it. Also please let me know about it! I'd love to hear that others found the code useful.

One exception: If you make a game that will likely put you on a government watch list, then please don't give me any kind of credit.  ;-)


Source Code


#include <pebble.h>

#define STORAGE_KEY_SAVE_VERSION 100
#define STORAGE_KEY_PLAYER 101
#define STORAGE_KEY_ENEMY 102

const int currentSaveVersion = 1;

// Storage Keys to record the game state on the Pebble watch when you leave the game.
const uint32_t storageKeySaveVersion = 1; // Save version information for future backwards compatibility.
const uint32_t storageKeyGameSettings = 2; // Current game state (excluding player and enemy).
const uint32_t storageKeyPlayer = 3; // Player's current state.
const uint32_t storageKeyNpc = 4; // NPC's current state.

// Enumerations for the NPC types.
// Locations can either select a specific NPC type or randomly pick from a range.
// In general they should be ordered from lowest threat to highest threat, but it's
// fine having special purposes NPC types in between (as long as you don't want a
// location to pick a random NPC from before AND after the special purpose NPC).
enum NpcTypes
{
  npcTypeNothing,
  npcTypeKing,
  npcTypeInnkeeper,
  npcTypeAttackTrainer,
  npcTypeDefenseTrainer,
  npcTypeCastleGuards,
  npcTypeRabbit,
  npcTypeGopher,
  npcTypeSnake,
  npcTypeGiantAnt,
  npcTypeSquirrel,
  npcTypeSlime,
  npcTypeDeer,
  npcTypeWolf,
  npcTypeOpossum,
  npcTypeWitch,
  npcTypeRaccoon,
  npcTypeRodentOfUnusualSize,
  npcTypeGoblin,
  npcTypeBlackBear,
  npcTypeMoose,
  npcTypeBeaver,
  npcTypeFish,
  npcTypeKobold,
  npcTypeMountainLion,
  npcTypeMountainGoat,
  npcTypeWyvern,
  npcTypeGrizzlyBear,
  npcTypeBighornSheep,
  npcTypeCoyote,
  npcTypeBobcat,
  npcTypeRattlesnake,
  npcTypeBandit,
  npcTypeRabidDog,
  npcTypeSkeleton,
  npcTypeZombie,
  npcTypeGhoul,
  npcTypeOrc,
  npcTypeTroll,
  npcTypeMage,
  npcTypeDragon,
  npcTypeHellBeast,
  npcTypeDemon,
  npcTypeFallenAngel,
  npcTypeGatekeeper,
  npcTypeDeadGatekeeper
};

// Enumerations for travel locations.
// These should be in order from initial location to last location.
// As currently coded, index 0 (locationGreatHall) will be the starting location of the
// game, but the user won't be able to return to it. It will prevent him or her from
// traveling before index 1 (locationInn).
enum TravelLocations
{
  locationGreatHall,
  locationInn,
  locationCombatArena,
  locationDefenseArena,
  locationCastleEntrance,
  locationFieldsOutsideCastle,
  locationRollingFields,
  locationFieldsOutsideForest,
  locationEdgeOfForest,
  locationLightlyWoodedForest,
  locationForest,
  locationHeavilyWoodedForest,
  locationClearingInForest,
  locationNearingAStream,
  locationStreamInForest,
  locationStreamJoiningRiver,
  locationRiverInForest,
  locationAlongRiver,
  locationGentleHill,
  locationSteepRockyHill,
  locationBottomOfMountain,
  locationAlongMountainCliff,
  locationSteepCliffFace,
  locationHighOnRockyCliff,
  locationNearTopOfMountain,
  locationRidgelineOfMountain,
  locationAlongMountainRidge,
  locationRockyMountainside,
  locationLowerMountainFace,
  locationPatchesOfDryGrass,
  locationEdgeOfDesertValley,
  locationDesertValley,
  locationRoadThroughDesert,
  locationAbandonedCarriages,
  locationDesertOutsideTown,
  locationEntranceToSmallTown,
  locationQuietDesertedTown,
  locationDilapidatedBuildings,
  locationDestroyedHomes,
  locationMassGrave,
  locationEdgeOfDesertTown,
  locationOpenBarenDesert,
  locationNearingVolcano,
  locationBaseOfVolanco,
  locationLowVolcanoSlope,
  locationSteepVolcanoSlope,
  locationCaveEntrance,
  locationNarrowingCave,
  locationDeepWithinCave,
  locationLavaTubeHole,
  locationWithinLavaTube,
  locationOpeningInLavaTube,
  locationOpenChamber,
  locationNearingGlowingGate,
  locationGatewayFromHell,
  lastLocationIndex = locationGatewayFromHell
};

// Enumerations for player actions, which will be passed between functions
// depending on button presses.
enum PlayerActions
{
  actionNothing,
  actionForward,
  actionBack,
  actionIgnore,
  actionFight,
  actionFlee,
  actionCastHeal,
  actionRest, // At Inn, get Hit Points and Spell Points
  actionTrainAttack, // At Trainer
  actionTrainDefense, // At Trainer
  actionResetGame // At Gate
};

// Constant to control how quickly player will raise in level.
// Increasing this number will mean it takes longer to reach the next level.
const int nextLevelExperienceIncrement = 15;

// Constant for the cost of resting at the inn.
const int costRest = 1;

// Constant for the cost of training. Cost is the multiplier * the player's current level.
const int costTrainMultiplier = 4;

// Constant for the cost in spell points to cast heal.
const int spellPointsHeal = 1;

// Structure saved in the Pebble when the app is closed and read when the app is open
// that contains various values so the game can resume right where the player left off.
struct gameSettingsStructure
{
  int lowestMovesToWin;
  bool gatekeeperKilled;
  int chanceToFleePercent;
  int chanceOfEncounterPercent;
  char locationDescription[64];
};

// Structure of all properties of the character - both player and NPC. This structure
// will be saved in the Pebble when the app is closed and read when the app is open
// so the game can resume right where the player left off.
struct characterStructure
{
  char description[32];
  int type;
  int location;
  int hitPoints;
  int hitPointsMaximum;
  int spellPoints;
  int spellPointsMaximum;
  int attack;
  int defense;
  int gold;
  int level;
  int experience;
  int experienceNextLevel;
  int experienceLastLevel;
  bool targetable;
  bool ignorable;
  int moves;
};

// Window Objects - these are the various screens that may be shown to the player.
static Window *windowGame; // Main Game Screen
static Window *windowConversation; // Conversation Screen

// Layers for the Main Game Screen
static Layer *layerGameCanvas;
static TextLayer *layerGameTextLocation;
static TextLayer *layerGameTextLocationDescription;
static TextLayer *layerGameTextNpcDescription;
static TextLayer *layerGameTextNpcHitPoints;
static TextLayer *layerGameTextPlayerAttack;
static TextLayer *layerGameTextPlayerDefense;
static TextLayer *layerGameTextPlayerExperience;
static TextLayer *layerGameTextPlayerHitPoints;
static TextLayer *layerGameTextPlayerSpellPoints;
static TextLayer *layerGameTextPlayerGold;
static TextLayer *layerGameTextMessage;
static TextLayer *layerGameTextActionUp;
static TextLayer *layerGameTextActionSelect;
static TextLayer *layerGameTextActionDown;

// Layers for the Conversation Screen
static TextLayer *layerConversationTextSpeaker;
static TextLayer *layerConversationTextMessage;
static TextLayer *layerConversationTextActionBack;

// Last Message is shown at the bottom for the player to get feedback that his or
// her action was processed and/or the amount of damage that was dealt.
static char lastMessage[64];

// Variables containing the current game state.
static struct gameSettingsStructure gameSettings;
static struct characterStructure player;
static struct characterStructure npc;

// Function to return the about of experience necessary for a given level.
static int getNextLevelExperience(int level)
{
  // For nextLevelExperienceIncrement = 15:
  // Level 1: 15
  // Level 2: 45
  // Level 3: 90
  // Level 4: 135
  int requiredExperience = 0;
  for (int loopLevel = 1; loopLevel <= level; loopLevel++)
  {
    requiredExperience += nextLevelExperienceIncrement * loopLevel;
  }
  return requiredExperience;
}

// Function to save the entire game state to the Pebble. Called on exit.
static void saveGame()
{
  persist_write_int(storageKeySaveVersion, currentSaveVersion);
  persist_write_data(storageKeyGameSettings, &gameSettings, sizeof(struct gameSettingsStructure));
  persist_write_data(storageKeyPlayer, &player, sizeof(struct characterStructure));
  persist_write_data(storageKeyNpc, &npc, sizeof(struct characterStructure));
  return;
}

// Function to load the entire game state from the Pebble. Called on load.
static bool loadGame()
{
  if (persist_exists(storageKeySaveVersion) && persist_exists(storageKeyGameSettings) && persist_exists(storageKeyPlayer) && persist_exists(storageKeyNpc))
  {
    int storedVersion = persist_read_int(storageKeySaveVersion);
    if (storedVersion == 0)
    {
      // Always force a new game (handy for debugging).
      return false;
    }
    else if (storedVersion == currentSaveVersion)
    {
      persist_read_data(storageKeyGameSettings, &gameSettings, sizeof(struct gameSettingsStructure));
      persist_read_data(storageKeyPlayer, &player, sizeof(struct characterStructure));
      persist_read_data(storageKeyNpc, &npc, sizeof(struct characterStructure));
      return true;
    }
    // In the future if the save variables change, then else conditions can be added for backwards compatibility.
  }
  return false;
}

// Once a new npc.type has been assigned, calling updateNpcProperties populates the
// npc structure will all the initial values.
static void updateNpcProperties()
{
  // chanceToFleePercent starts at 25%, but it increases each time the player attempts to flee so
  // a player isn't absolutely stuck at a difficult guy until he or she is killed.
  gameSettings.chanceToFleePercent = 25;
  switch(npc.type)
  {
    case npcTypeNothing:
      strcpy(npc.description, "");
      npc.targetable = false;
      npc.ignorable = true;
      npc.hitPoints = 0;
      npc.attack = 0;
      npc.defense = 0;
      npc.level = 0;
      npc.experience = 0;
      npc.gold = 0;
      break;
    case npcTypeKing:
      strcpy(npc.description, "King");
      npc.targetable = false;
      npc.ignorable = true;
      npc.hitPoints = 0;
      npc.attack = 0;
      npc.defense = 0;
      npc.level = 0;
      npc.experience = 0;
      npc.gold = 0;
      break;
    case npcTypeInnkeeper:
      strcpy(npc.description, "Innkeeper");
      npc.targetable = false;
      npc.ignorable = true;
      npc.hitPoints = 0;
      npc.attack = 0;
      npc.defense = 0;
      npc.level = 0;
      npc.experience = 0;
      npc.gold = 0;
      break;
    case npcTypeAttackTrainer:
      strcpy(npc.description, "Attack Trainer");
      npc.targetable = false;
      npc.ignorable = true;
      npc.hitPoints = 0;
      npc.attack = 0;
      npc.defense = 0;
      npc.level = 0;
      npc.experience = 0;
      npc.gold = 0;
      break;
    case npcTypeDefenseTrainer:
      strcpy(npc.description, "Defense Trainer");
      npc.targetable = false;
      npc.ignorable = true;
      npc.hitPoints = 0;
      npc.attack = 0;
      npc.defense = 0;
      npc.level = 0;
      npc.experience = 0;
      npc.gold = 0;
      break;
    case npcTypeCastleGuards:
      strcpy(npc.description, "Castle Guards");
      npc.targetable = false;
      npc.ignorable = true;
      npc.hitPoints = 0;
      npc.attack = 0;
      npc.defense = 0;
      npc.level = 0;
      npc.experience = 0;
      npc.gold = 0;
      break;
    case npcTypeRabbit:
      strcpy(npc.description, "Rabbit");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 3;
      npc.hitPointsMaximum = 3;
      npc.attack = 1;
      npc.defense = 2;
      npc.level = 0;
      npc.experience = 1;
      npc.gold = 1;
      break;
    case npcTypeGopher:
      strcpy(npc.description, "Gopher");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 3;
      npc.hitPointsMaximum = 3;
      npc.attack = 2;
      npc.defense = 1;
      npc.level = 0;
      npc.experience = 1;
      npc.gold = 1;
      break;
    case npcTypeSnake:
      strcpy(npc.description, "Snake");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 2;
      npc.hitPointsMaximum = 2;
      npc.attack = 3;
      npc.defense = 3;
      npc.level = 1;
      npc.experience = 2;
      npc.gold = 1;
      break;
    case npcTypeGiantAnt:
      strcpy(npc.description, "Giant Ant");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 6;
      npc.hitPointsMaximum = 6;
      npc.attack = 4;
      npc.defense = 3;
      npc.level = 2;
      npc.experience = 3;
      npc.gold = 0;
      break;
    case npcTypeSquirrel:
      strcpy(npc.description, "Squirrel");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 2;
      npc.hitPointsMaximum = 2;
      npc.attack = 2;
      npc.defense = 3;
      npc.level = 1;
      npc.experience = 2;
      npc.gold = 1;
      break;
    case npcTypeSlime:
      strcpy(npc.description, "Slime");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 8;
      npc.hitPointsMaximum = 8;
      npc.attack = 6;
      npc.defense = 1;
      npc.level = 2;
      npc.experience = 7;
      npc.gold = 0;
      break;
    case npcTypeDeer:
      strcpy(npc.description, "Deer");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 10;
      npc.hitPointsMaximum = 10;
      npc.attack = 3;
      npc.defense = 6;
      npc.level = 2;
      npc.experience = 8;
      npc.gold = 5;
      break;
    case npcTypeWolf:
      strcpy(npc.description, "Wolf");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 10;
      npc.hitPointsMaximum = 10;
      npc.attack = 6;
      npc.defense = 4;
      npc.level = 3;
      npc.experience = 10;
      npc.gold = 4;
      break;
    case npcTypeOpossum:
      strcpy(npc.description, "Opossum");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 8;
      npc.hitPointsMaximum = 8;
      npc.attack = 6;
      npc.defense = 3;
      npc.level = 3;
      npc.experience = 10;
      npc.gold = 2;
      break;
    case npcTypeWitch:
      strcpy(npc.description, "Witch");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 10;
      npc.hitPointsMaximum = 10;
      npc.attack = 12;
      npc.defense = 2;
      npc.level = 4;
      npc.experience = 12;
      npc.gold = 20;
      break;
    case npcTypeRaccoon:
      strcpy(npc.description, "Raccoon");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 8;
      npc.hitPointsMaximum = 8;
      npc.attack = 3;
      npc.defense = 4;
      npc.level = 3;
      npc.experience = 8;
      npc.gold = 3;
      break;
    case npcTypeRodentOfUnusualSize:
      strcpy(npc.description, "Unusual Size Rodent");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 10;
      npc.hitPointsMaximum = 10;
      npc.attack = 8;
      npc.defense = 3;
      npc.level = 5;
      npc.experience = 11;
      npc.gold = 5;
      break;
    case npcTypeGoblin:
      strcpy(npc.description, "Goblin");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 15;
      npc.hitPointsMaximum = 15;
      npc.attack = 9;
      npc.defense = 5;
      npc.level = 6;
      npc.experience = 11;
      npc.gold = 7;
      break;
    case npcTypeBlackBear:
      strcpy(npc.description, "Black Bear");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 20;
      npc.hitPointsMaximum = 20;
      npc.attack = 6;
      npc.defense = 4;
      npc.level = 5;
      npc.experience = 15;
      npc.gold = 5;
      break;
    case npcTypeKobold:
      strcpy(npc.description, "Kobold");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 25;
      npc.hitPointsMaximum = 25;
      npc.attack = 10;
      npc.defense = 10;
      npc.level = 7;
      npc.experience = 20;
      npc.gold = 14;
      break;
    case npcTypeMoose:
      strcpy(npc.description, "Moose");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 20;
      npc.hitPointsMaximum = 20;
      npc.attack = 8;
      npc.defense = 4;
      npc.level = 6;
      npc.experience = 20;
      npc.gold = 8;
      break;
    case npcTypeBeaver:
      strcpy(npc.description, "Beaver");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 8;
      npc.hitPointsMaximum = 8;
      npc.attack = 4;
      npc.defense = 2;
      npc.level = 2;
      npc.experience = 7;
      npc.gold = 3;
      break;
    case npcTypeFish:
      strcpy(npc.description, "Fish");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 1;
      npc.hitPointsMaximum = 1;
      npc.attack = 0;
      npc.defense = 2;
      npc.level = 0;
      npc.experience = 2;
      npc.gold = 1;
      break;
    case npcTypeMountainLion:
      strcpy(npc.description, "Mountain Lion");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 16;
      npc.hitPointsMaximum = 16;
      npc.attack = 8;
      npc.defense = 6;
      npc.level = 7;
      npc.experience = 20;
      npc.gold = 10;
      break;
    case npcTypeMountainGoat:
      strcpy(npc.description, "Mountain Goat");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 13;
      npc.hitPointsMaximum = 13;
      npc.attack = 5;
      npc.defense = 5;
      npc.level = 6;
      npc.experience = 17;
      npc.gold = 7;
      break;
    case npcTypeWyvern:
      strcpy(npc.description, "Wyvern");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 25;
      npc.hitPointsMaximum = 25;
      npc.attack = 15;
      npc.defense = 9;
      npc.level = 9;
      npc.experience = 22;
      npc.gold = 7;
      break;
    case npcTypeGrizzlyBear:
      strcpy(npc.description, "Grizzly Bear");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 28;
      npc.hitPointsMaximum = 28;
      npc.attack = 9;
      npc.defense = 6;
      npc.level = 8;
      npc.experience = 20;
      npc.gold = 10;
      break;
    case npcTypeBighornSheep:
      strcpy(npc.description, "Bighorn Sheep");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 13;
      npc.hitPointsMaximum = 13;
      npc.attack = 5;
      npc.defense = 5;
      npc.level = 5;
      npc.experience = 17;
      npc.gold = 7;
      break;
    case npcTypeCoyote:
      strcpy(npc.description, "Coyote");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 10;
      npc.hitPointsMaximum = 10;
      npc.attack = 6;
      npc.defense = 4;
      npc.level = 6;
      npc.experience = 10;
      npc.gold = 4;
      break;
    case npcTypeBobcat:
      strcpy(npc.description, "Bobcat");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 10;
      npc.hitPointsMaximum = 10;
      npc.attack = 4;
      npc.defense = 6;
      npc.level = 7;
      npc.experience = 10;
      npc.gold = 4;
      break;
    case npcTypeRattlesnake:
      strcpy(npc.description, "Rattlesnake");
      npc.targetable = true;
      npc.ignorable = true;
      npc.hitPoints = 4;
      npc.hitPointsMaximum = 4;
      npc.attack = 6;
      npc.defense = 3;
      npc.level = 4;
      npc.experience = 7;
      npc.gold = 2;
      break;
    case npcTypeBandit:
      strcpy(npc.description, "Bandit");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 30;
      npc.hitPointsMaximum = 30;
      npc.attack = 12;
      npc.defense = 12;
      npc.level = 10;
      npc.experience = 30;
      npc.gold = 14;
      break;
    case npcTypeRabidDog:
      strcpy(npc.description, "Rabid Dog");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 8;
      npc.hitPointsMaximum = 8;
      npc.attack = 10;
      npc.defense = 4;
      npc.level = 9;
      npc.experience = 10;
      npc.gold = 0;
      break;
    case npcTypeSkeleton:
      strcpy(npc.description, "Skeleton");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 15;
      npc.hitPointsMaximum = 15;
      npc.attack = 8;
      npc.defense = 8;
      npc.level = 12;
      npc.experience = 12;
      npc.gold = 0;
      break;
    case npcTypeZombie:
      strcpy(npc.description, "Zombie");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 25;
      npc.hitPointsMaximum = 25;
      npc.attack = 14;
      npc.defense = 6;
      npc.level = 12;
      npc.experience = 15;
      npc.gold = 10;
      break;
    case npcTypeGhoul:
      strcpy(npc.description, "Ghoul");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 30;
      npc.hitPointsMaximum = 30;
      npc.attack = 15;
      npc.defense = 8;
      npc.level = 11;
      npc.experience = 15;
      npc.gold = 3;
      break;
    case npcTypeOrc:
      strcpy(npc.description, "Orc");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 35;
      npc.hitPointsMaximum = 35;
      npc.attack = 16;
      npc.defense = 12;
      npc.level = 14;
      npc.experience = 30;
      npc.gold = 12;
      break;
    case npcTypeTroll:
      strcpy(npc.description, "Troll");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 45;
      npc.hitPointsMaximum = 45;
      npc.attack = 18;
      npc.defense = 10;
      npc.level = 15;
      npc.experience = 35;
      npc.gold = 15;
      break;
    case npcTypeMage:
      strcpy(npc.description, "Mage");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 30;
      npc.hitPointsMaximum = 30;
      npc.attack = 20;
      npc.defense = 10;
      npc.level = 16;
      npc.experience = 40;
      npc.gold = 20;
      break;
    case npcTypeDragon:
      strcpy(npc.description, "Dragon");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 50;
      npc.hitPointsMaximum = 50;
      npc.attack = 22;
      npc.defense = 22;
      npc.level = 19;
      npc.experience = 60;
      npc.gold = 40;
      break;
    case npcTypeHellBeast:
      strcpy(npc.description, "Hell Beast");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 40;
      npc.hitPointsMaximum = 40;
      npc.attack = 18;
      npc.defense = 16;
      npc.level = 17;
      npc.experience = 60;
      npc.gold = 30;
      break;
    case npcTypeDemon:
      strcpy(npc.description, "Demon");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 50;
      npc.hitPointsMaximum = 50;
      npc.attack = 20;
      npc.defense = 18;
      npc.level = 18;
      npc.experience = 60;
      npc.gold = 30;
      break;
    case npcTypeFallenAngel:
      strcpy(npc.description, "Fallen Angel");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 70;
      npc.hitPointsMaximum = 70;
      npc.attack = 25;
      npc.defense = 25;
      npc.level = 20;
      npc.experience = 60;
      npc.gold = 35;
      break;
    case npcTypeGatekeeper:
      strcpy(npc.description, "Gatekeeper");
      npc.targetable = true;
      npc.ignorable = false;
      npc.hitPoints = 100;
      npc.hitPointsMaximum = 100;
      npc.attack = 30;
      npc.defense = 30;
      npc.level = 23;
      npc.experience = 1000;
      npc.gold = 0;
      break;
    case npcTypeDeadGatekeeper:
      strcpy(npc.description, "Dead Gatekeeper");
      npc.targetable = false;
      npc.ignorable = false;
      npc.hitPoints = 1;
      npc.hitPointsMaximum = 1;
      npc.attack = 1;
      npc.defense = 1;
      npc.level = 1;
      npc.experience = 1;
      npc.gold = 0;
      break;
  }
}

// Function that will return a NPC type given a chance of encounter, the lower NPC enumeration,
// and the upper NPC enumeration. This allows locations to have a specific range of NPC types
// possible with a chance of encountering them.
static int getNpcType(int chanceOfNpc, int lowerRange, int upperRange)
{
  gameSettings.chanceOfEncounterPercent += chanceOfNpc;
  if (gameSettings.chanceOfEncounterPercent > rand() % 100)
  {
    gameSettings.chanceOfEncounterPercent = 0;
    return rand() % (upperRange - lowerRange + 1) + lowerRange;
  }
  else
  {
    return 0;
  }
}

// Once a new player.location has been assigned, calling updateLocationProperties populates the
// location structure will all the initial values. This function also assigns a NPC appropriate
// for the new location. Some locations always have the same NPCs, some locations have a random
// range. Some locations may change their properties depending on other game settings.
static void updateLocationProperties()
{
  switch(player.location)
  {
    case locationGreatHall:
      strcpy(gameSettings.locationDescription, "Castle Great Hall");
      npc.type = npcTypeKing;
      break;
    case locationInn:
      strcpy(gameSettings.locationDescription, "Inn, $1");
      npc.type = npcTypeInnkeeper;
      break;
    case locationCombatArena:
      snprintf(gameSettings.locationDescription, sizeof(gameSettings.locationDescription), "Combat Arena  $%d*Att", costTrainMultiplier);
      npc.type = npcTypeAttackTrainer;
      break;
    case locationDefenseArena:
      snprintf(gameSettings.locationDescription, sizeof(gameSettings.locationDescription), "Defense Arena  $%d*Def", costTrainMultiplier);
      npc.type = npcTypeDefenseTrainer;
      break;
    case locationCastleEntrance:
      strcpy(gameSettings.locationDescription, "Castle Entrance");
      npc.type = npcTypeCastleGuards;
      break;
    case locationFieldsOutsideCastle:
      strcpy(gameSettings.locationDescription, "Fields Outside Castle");
      npc.type = getNpcType(25, npcTypeRabbit, npcTypeGopher);
      break;
    case locationRollingFields:
      strcpy(gameSettings.locationDescription, "Rolling Fields");
      npc.type = getNpcType(25, npcTypeRabbit, npcTypeGopher);
      break;
    case locationFieldsOutsideForest:
      strcpy(gameSettings.locationDescription, "Fields Outside Forest");
      npc.type = getNpcType(25, npcTypeRabbit, npcTypeGopher);
      break;
    case locationEdgeOfForest:
      strcpy(gameSettings.locationDescription, "Edge of Forest");
      npc.type = getNpcType(25, npcTypeRabbit, npcTypeSnake);
      break;
    case locationLightlyWoodedForest:
      strcpy(gameSettings.locationDescription, "Lightly Wooded Forest");
      npc.type = getNpcType(25, npcTypeSnake, npcTypeSquirrel);
      break;
    case locationForest:
      strcpy(gameSettings.locationDescription, "Forest");
      npc.type = getNpcType(25, npcTypeGiantAnt, npcTypeDeer);
      break;
    case locationHeavilyWoodedForest:
      strcpy(gameSettings.locationDescription, "Heavily Wooded Forest");
      npc.type = getNpcType(25, npcTypeGiantAnt, npcTypeWolf);
      break;
    case locationClearingInForest:
      strcpy(gameSettings.locationDescription, "Clearing in Forest");
      npc.type = getNpcType(25, npcTypeDeer, npcTypeMoose);
      break;
    case locationNearingAStream:
      strcpy(gameSettings.locationDescription, "Nearing a Stream");
      npc.type = getNpcType(25, npcTypeDeer, npcTypeMoose);
      break;
    case locationStreamInForest:
      strcpy(gameSettings.locationDescription, "Stream in Forest");
      npc.type = getNpcType(25, npcTypeRaccoon, npcTypeKobold);
      break;
    case locationStreamJoiningRiver:
      strcpy(gameSettings.locationDescription, "Stream Joining River");
      npc.type = getNpcType(25, npcTypeRaccoon, npcTypeKobold);
      break;
    case locationRiverInForest:
      strcpy(gameSettings.locationDescription, "River in Forest");
      npc.type = getNpcType(25, npcTypeRaccoon, npcTypeKobold);
      break;
    case locationAlongRiver:
      strcpy(gameSettings.locationDescription, "Along River, Few Trees");
      npc.type = getNpcType(25, npcTypeRaccoon, npcTypeKobold);
      break;
    case locationGentleHill:
      strcpy(gameSettings.locationDescription, "Gentle Hill");
      npc.type = getNpcType(25, npcTypeKobold, npcTypeGrizzlyBear);
      break;
    case locationSteepRockyHill:
      strcpy(gameSettings.locationDescription, "Steep Rocky Hill");
      npc.type = getNpcType(25, npcTypeMountainLion, npcTypeBighornSheep);
      break;
    case locationBottomOfMountain:
      strcpy(gameSettings.locationDescription, "Bottom of Mountain");
      npc.type = getNpcType(25, npcTypeMountainLion, npcTypeBighornSheep);
      break;
    case locationAlongMountainCliff:
      strcpy(gameSettings.locationDescription, "Along Mountain Cliff");
      npc.type = getNpcType(25, npcTypeMountainLion, npcTypeBighornSheep);
      break;
    case locationSteepCliffFace:
      strcpy(gameSettings.locationDescription, "Steep Cliff Face");
      npc.type = getNpcType(25, npcTypeMountainLion, npcTypeBighornSheep);
      break;
    case locationHighOnRockyCliff:
      strcpy(gameSettings.locationDescription, "High on Rocky Cliff");
      npc.type = getNpcType(25, npcTypeGrizzlyBear, npcTypeBobcat);
      break;
    case locationNearTopOfMountain:
      strcpy(gameSettings.locationDescription, "Near Top of Mountain");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeBobcat);
      break;
    case locationRidgelineOfMountain:
      strcpy(gameSettings.locationDescription, "Ridgeline of Mountain");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeBobcat);
      break;
    case locationAlongMountainRidge:
      strcpy(gameSettings.locationDescription, "Along Mountain Ridge");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeBobcat);
      break;
    case locationRockyMountainside:
      strcpy(gameSettings.locationDescription, "Rocky Mountainside");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeRattlesnake);
      break;
    case locationLowerMountainFace:
      strcpy(gameSettings.locationDescription, "Lower Mountain Face");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeRattlesnake);
      break;
    case locationPatchesOfDryGrass:
      strcpy(gameSettings.locationDescription, "Patches of Dry Grass");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeBandit);
      break;
    case locationEdgeOfDesertValley:
      strcpy(gameSettings.locationDescription, "Edge of Desert Valley");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeBandit);
      break;
    case locationDesertValley:
      strcpy(gameSettings.locationDescription, "Desert Valley");
      npc.type = getNpcType(25, npcTypeCoyote, npcTypeRabidDog);
      break;
    case locationRoadThroughDesert:
      strcpy(gameSettings.locationDescription, "Road through Desert");
      npc.type = getNpcType(25, npcTypeRattlesnake, npcTypeSkeleton);
      break;
    case locationAbandonedCarriages:
      strcpy(gameSettings.locationDescription, "Abandoned Carriages");
      npc.type = getNpcType(25, npcTypeRattlesnake, npcTypeSkeleton);
      break;
    case locationDesertOutsideTown:
      strcpy(gameSettings.locationDescription, "Desert Outside a Town");
      npc.type = getNpcType(25, npcTypeRabidDog, npcTypeOrc);
      break;
    case locationEntranceToSmallTown:
      strcpy(gameSettings.locationDescription, "Entrance to Small Town");
      npc.type = getNpcType(25, npcTypeRabidDog, npcTypeOrc);
      break;
    case locationQuietDesertedTown:
      strcpy(gameSettings.locationDescription, "Quiet Deserted Town");
      npc.type = getNpcType(25, npcTypeRabidDog, npcTypeOrc);
      break;
    case locationDilapidatedBuildings:
      strcpy(gameSettings.locationDescription, "Dilapidated Buildings");
      npc.type = getNpcType(25, npcTypeRabidDog, npcTypeOrc);
      break;
    case locationDestroyedHomes:
      strcpy(gameSettings.locationDescription, "Destroyed Homes");
      npc.type = getNpcType(25, npcTypeSkeleton, npcTypeTroll);
      break;
    case locationMassGrave:
      strcpy(gameSettings.locationDescription, "Mass Grave");
      npc.type = getNpcType(25, npcTypeSkeleton, npcTypeTroll);
      break;
    case locationEdgeOfDesertTown:
      strcpy(gameSettings.locationDescription, "Edge of Desert Town");
      npc.type = getNpcType(25, npcTypeSkeleton, npcTypeTroll);
      break;
    case locationOpenBarenDesert:
      strcpy(gameSettings.locationDescription, "Open Baren Desert");
      npc.type = getNpcType(25, npcTypeSkeleton, npcTypeMage);
      break;
    case locationNearingVolcano:
      strcpy(gameSettings.locationDescription, "Nearing Volcano");
      npc.type = getNpcType(25, npcTypeZombie, npcTypeMage);
      break;
    case locationBaseOfVolanco:
      strcpy(gameSettings.locationDescription, "Base of Volcano");
      npc.type = getNpcType(25, npcTypeOrc, npcTypeDragon);
      break;
    case locationLowVolcanoSlope:
      strcpy(gameSettings.locationDescription, "Low Volcano Slope");
      npc.type = getNpcType(25, npcTypeOrc, npcTypeDragon);
      break;
    case locationSteepVolcanoSlope:
      strcpy(gameSettings.locationDescription, "Steep Volcano Slope");
      npc.type = getNpcType(25, npcTypeOrc, npcTypeDragon);
      break;
    case locationCaveEntrance:
      strcpy(gameSettings.locationDescription, "Cave Entrance");
      npc.type = getNpcType(25, npcTypeOrc, npcTypeDragon);
      break;
    case locationNarrowingCave:
      strcpy(gameSettings.locationDescription, "Narrowing Cave");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationDeepWithinCave:
      strcpy(gameSettings.locationDescription, "Deep Within Cave");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationLavaTubeHole:
      strcpy(gameSettings.locationDescription, "Lava Tube Hole");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationWithinLavaTube:
      strcpy(gameSettings.locationDescription, "Within Lava Tube");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationOpeningInLavaTube:
      strcpy(gameSettings.locationDescription, "Opening in Lava Tube");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationOpenChamber:
      strcpy(gameSettings.locationDescription, "Open Chamber");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationNearingGlowingGate:
      strcpy(gameSettings.locationDescription, "Nearing Glowing Gate");
      npc.type = getNpcType(25, npcTypeHellBeast, npcTypeFallenAngel);
      break;
    case locationGatewayFromHell:
      if (!gameSettings.gatekeeperKilled)
      {
        strcpy(gameSettings.locationDescription, "Glowing Gate");
        npc.type = npcTypeGatekeeper;      
      }
      else
      {
        strcpy(gameSettings.locationDescription, "Destroyed Gate");
        npc.type = npcTypeDeadGatekeeper;
      }
      break;
    default:
      strcpy(gameSettings.locationDescription, "The Void");
      npc.type = npcTypeNothing;
      break;
  }
  updateNpcProperties();

  return;
}

// Function to reset the entire game to a default state. (Called on new game.)
static void resetGame()
{
  strcpy(player.description, "");
  player.type = 0;
  // Move the player to the very first room (which will be inaccessible later).
  player.location = 0;
  // Assign initial values for the player.
  player.hitPoints = 10;
  player.hitPointsMaximum = 10;
  player.spellPoints = 4;
  player.spellPointsMaximum = 4;
  player.gold = 5;
  player.attack = 1;
  player.defense = 1;
  player.level = 1;
  player.experience = 0;
  player.experienceLastLevel = 0;
  player.experienceNextLevel = getNextLevelExperience(player.level);
  player.targetable = true;
  player.ignorable = false;
  player.moves = 0;

  gameSettings.gatekeeperKilled = false;
  gameSettings.chanceToFleePercent = 25;
  gameSettings.chanceOfEncounterPercent = 0;

  updateLocationProperties();
  return;
}

// Get the action that should be processed when the "Up" button is pressed on the Pebble.
static int getActionUp()
{
  // Up always attempts to bring us further away from the first location.
  // If we're in the middle of combat, then it attempts an attack.
  if (!npc.targetable || npc.type == npcTypeNothing)
  {
    if (player.location == lastLocationIndex)
    {
      return actionNothing;
    }
    else
    {
      return actionForward;
    }
  }
  else if (npc.ignorable)
  {
    return actionFight;
  }
  else
  {
    return actionFight;
  }
}

// Get the action that should be processed when the "Select" button is pressed on the Pebble.
static int getActionSelect()
{
  // The select action is primarily for when we're interacting with a NPC, so the action will be based
  // on the NPC type. For enemy NPCs, we'll default to the heal action (if we're injured).
  switch(npc.type)
  {
    case npcTypeInnkeeper:
      if ((player.gold >= costRest) &&
          ((player.hitPoints < player.hitPointsMaximum) || (player.spellPoints < player.spellPointsMaximum)))
      {
        return actionRest;
      }
      break;
    case npcTypeAttackTrainer:
      if (player.gold >= costTrainMultiplier * player.attack)
      {
        return actionTrainAttack;
      }
      break;
    case npcTypeDefenseTrainer:
      if (player.gold >= costTrainMultiplier * player.defense)
      {
        return actionTrainDefense;
      }
      break;
    case npcTypeDeadGatekeeper:
      return actionResetGame;
      break;
    default:
      if ((player.spellPoints >= spellPointsHeal) && (player.hitPoints < player.hitPointsMaximum))
      {
        return actionCastHeal;
      }
      break;
  }
  return actionNothing;
}

// Get the action that should be processed when the "Down" button is pressed on the Pebble.
static int getActionDown()
{
  // Down always attempts to bring us towards the first location.
  // If we're in the middle of combat with someone we can't ignore, then it attempts a flee.
  if (!npc.targetable || npc.type == npcTypeNothing)
  {
    if (player.location > 1)
    {
      return actionBack;
    }
    else
    {
      return actionNothing;
    }
  }
  else if (npc.ignorable)
  {
    return actionIgnore;
  }
  else
  {
    return actionFlee;
  }
}

// Function to get a text description (for display) of a given action.
static void getActionDescription(int action, char *string)
{
  switch(action)
  {
    case actionNothing:
      strcpy(string, "");
      break;
    case actionForward:
      strcpy(string, "Fwd");
      break;
    case actionBack:
      strcpy(string, "Back");
      break;
    case actionIgnore:
      strcpy(string, "Ignore");
      break;
    case actionFight:
      strcpy(string, "Fight");
      break;
    case actionFlee:
      strcpy(string, "Flee");
      break;
    case actionCastHeal:
      strcpy(string, "Cast Heal");
      break;
    case actionRest:
      strcpy(string, "Rest");
      break;
    case actionTrainAttack:
      strcpy(string, "Train");
      break;
    case actionTrainDefense:
      strcpy(string, "Train");
      break;
    case actionResetGame:
      strcpy(string, "New Game");
      break;
  }
  return;
}

// Call to update the Main Game Screen for the player.
static void updateGameWindow()
{
  // Label the Up/Select/Down buttons.
  static char bufferActionUp[64];
  static char bufferActionSelect[64];
  static char bufferActionDown[64];
  getActionDescription(getActionUp(), bufferActionUp);
  getActionDescription(getActionSelect(), bufferActionSelect);
  getActionDescription(getActionDown(), bufferActionDown);
  text_layer_set_text(layerGameTextActionUp, bufferActionUp);
  text_layer_set_text(layerGameTextActionSelect, bufferActionSelect);
  text_layer_set_text(layerGameTextActionDown, bufferActionDown);

  // Show player location number and description.
  static char bufferPlayerLocation[64];
  snprintf(bufferPlayerLocation, sizeof(bufferPlayerLocation), "Location:%d", player.location);
  text_layer_set_text(layerGameTextLocation, bufferPlayerLocation);

  static char bufferPlayerLocationDescription[64];
  strcpy(bufferPlayerLocationDescription, gameSettings.locationDescription);
  text_layer_set_text(layerGameTextLocationDescription, bufferPlayerLocationDescription);

  // If a NPC is present, show its information.
  static char bufferNpcDescription[32];
  if (npc.type > npcTypeNothing)
  {
    strcpy(bufferNpcDescription, npc.description);
  }
  else
  {
    strcpy(bufferNpcDescription, "");
  }
  text_layer_set_text(layerGameTextNpcDescription, bufferNpcDescription);

  static char bufferNpcHitPoints[32];
  if (npc.targetable && npc.type > npcTypeNothing && npc.hitPoints > 0)
  {
    snprintf(bufferNpcHitPoints, sizeof(bufferNpcHitPoints), "HP:%d", npc.hitPoints); //, npc.hitPointsMaximum);
  }
  else
  {
    strcpy(bufferNpcHitPoints, "");  
  }
  text_layer_set_text(layerGameTextNpcHitPoints, bufferNpcHitPoints);

  // Show the player's current properties.

  static char bufferPlayerAttack[64];
  snprintf(bufferPlayerAttack, sizeof(bufferPlayerAttack), "Att:%d", player.attack);
  text_layer_set_text(layerGameTextPlayerAttack, bufferPlayerAttack);

  static char bufferPlayerDefense[64];
  snprintf(bufferPlayerDefense, sizeof(bufferPlayerDefense), "Def:%d", player.defense);
  text_layer_set_text(layerGameTextPlayerDefense, bufferPlayerDefense);

  static char bufferPlayerExperience[64];
  snprintf(bufferPlayerExperience, sizeof(bufferPlayerExperience), "Level:%d", player.level);
  text_layer_set_text(layerGameTextPlayerExperience, bufferPlayerExperience);

  static char bufferPlayerHitPoints[64];
  snprintf(bufferPlayerHitPoints, sizeof(bufferPlayerHitPoints), "HP:%d/%d", player.hitPoints, player.hitPointsMaximum);
  text_layer_set_text(layerGameTextPlayerHitPoints, bufferPlayerHitPoints);

  static char bufferPlayerSpellPoints[64];
  snprintf(bufferPlayerSpellPoints, sizeof(bufferPlayerSpellPoints), "SP:%d/%d", player.spellPoints, player.spellPointsMaximum);
  text_layer_set_text(layerGameTextPlayerSpellPoints, bufferPlayerSpellPoints);

  static char bufferPlayerGold[64];
  snprintf(bufferPlayerGold, sizeof(bufferPlayerGold), "$%d", player.gold);
  text_layer_set_text(layerGameTextPlayerGold, bufferPlayerGold);

  // Show the last message to the player (for feedback from the last action taken).

  static char bufferMessage[64];
  strcpy(bufferMessage, lastMessage);
  text_layer_set_text(layerGameTextMessage, bufferMessage);

  return;
}

// Call to update the Conversation Screen for the player.
static void updateConversationWindow()
{
  static char bufferConversationSpeaker[64];
  static char bufferConversationMessage[256];
  static char bufferConversationTextActionBack[64];

  // Conversations are currently very simple. It's only dependent upon the NPC you are
  // encountering and they each have only one thing to say.
  switch (npc.type)
  {
    case npcTypeKing:
      // New character, tell them what's going on!
      snprintf(bufferConversationSpeaker, sizeof(bufferConversationSpeaker), "King");
      snprintf(bufferConversationMessage, sizeof(bufferConversationMessage),
               "There is a growing threat to the lands. A dark evil from the east has been murdering citizens and destroying towns. Find what is causing this and put an end to it.");
      break;
    case npcTypeInnkeeper:
      // We died! Give a pep-talk.
      snprintf(bufferConversationSpeaker, sizeof(bufferConversationSpeaker), "Innkeeper");
      snprintf(bufferConversationMessage, sizeof(bufferConversationMessage),
               "You were found unconscious and dying by a fellow adventurer and brought back to the inn. You lost some of your skills, but rest up and continue your quest.");
      break;
    case npcTypeDeadGatekeeper:
      // We won!
      snprintf(bufferConversationSpeaker, sizeof(bufferConversationSpeaker), "The Gate");
      snprintf(bufferConversationMessage, sizeof(bufferConversationMessage),
               "You have closed the gateway from hell! The kingdom is safe again. It took you %i moves, your best is %i moves. You may reset your character to try again.", player.moves, gameSettings.lowestMovesToWin);
      break;
    default:
      snprintf(bufferConversationSpeaker, sizeof(bufferConversationSpeaker), " ");
      snprintf(bufferConversationMessage, sizeof(bufferConversationMessage), " ");
      break;
  }

  // Label for the Pebble back button.
  snprintf(bufferConversationTextActionBack, sizeof(bufferConversationTextActionBack), "Press Back");

  text_layer_set_text(layerConversationTextSpeaker, bufferConversationSpeaker);
  text_layer_set_text(layerConversationTextMessage, bufferConversationMessage);
  text_layer_set_text(layerConversationTextActionBack, bufferConversationTextActionBack);
  return;
}

// Function process the forward action.
static void processActionForward()
{
  if (player.location < lastLocationIndex)
  {
    player.location += 1;
    player.moves += 1;
    updateLocationProperties();
    snprintf(lastMessage, sizeof(lastMessage), "Moved forward.");
    updateGameWindow();
  }
  return;
}

// Function to process the back action.
static void processActionBack()
{
  if (player.location > 1)
  {
    player.location -= 1;
    player.moves += 1;
    snprintf(lastMessage, sizeof(lastMessage), "Moved back.");
    updateLocationProperties();
    updateGameWindow();
  }
  return;
}

// Function to process the ignore action.
static void processActionIgnore()
{
  npc.type = 0;
  snprintf(lastMessage, sizeof(lastMessage), "Ignored enemy.");
  updateGameWindow();
  return;
}

// Function to process an attack given an attacker and defender. The attacker could be the
// player or NPC (and the defender would be the NPC or player respectively).
static int processAttack(struct characterStructure *attacker, struct characterStructure *defender)
{
  // Math to determine whether or not we should have hit.
  int chanceToHitPercent = (15 * (1 + attacker->level - defender->level)) + (2 * (attacker->attack - defender->defense));
  // No matter how underpowered the attacker is, always give a chance to hit.
  if (chanceToHitPercent < 25)
  {
    chanceToHitPercent = 25;
  }

  int damage = 0;
  if (chanceToHitPercent >= (rand() % 100))
  {
    // Basing the damage range as the difference between the attacker and defender resulted in too many 1 HP hits.
    //int damageRange = (attacker->level + attacker->attack) - (defender->level + defender->defense);
    int damageRange = attacker->attack;
    if (damageRange < 1)
    {
      // Even if the defender is significantly more powerful than attacker, attacker should be able do something.
      damageRange = 1;
    }
    damage = rand() % damageRange;
    if (damage < 1)
    {
      damage = 1;
    }
  }

  return damage;
}

// Function called when the player has been killed.
static void processDeath()
{
  snprintf(lastMessage, sizeof(lastMessage), " ");

  // Set the player back to the start and bump the hit and spell points back up.
  player.location = 1;
  player.hitPoints = player.hitPointsMaximum;
  player.spellPoints = player.spellPointsMaximum;

  // We need some punishment for dying, take away his or her cash and reduce skills.
  player.gold = 0;
  player.attack -= 1;
  if (player.attack < 1)
  {
    player.attack = 1;
  }
  player.defense -= 1;
  if (player.defense < 1)
  {
    player.defense = 1;
  }

  // Return back to the inn.
  updateLocationProperties();

  // Show the conversation with the innkeeper.
  updateConversationWindow();
  window_stack_push(windowConversation, true);

  return;
}

// Function called when the player leveled up.
static void processLevelUp()
{
  player.level += 1;
  player.experienceLastLevel = player.experienceNextLevel;
  player.experienceNextLevel = getNextLevelExperience(player.level);
  player.hitPointsMaximum += 3;
  player.spellPointsMaximum += 2;
  // Makes the game too easy if we restore hit/spell points on level up.
  //player.hitPoints = player.hitPointsMaximum;
  //player.spellPoints = player.spellPointsMaximum;
  return;
}

// Function to process the fight action.
static void processActionFight()
{
  if (npc.targetable && npc.type > npcTypeNothing)
  {
    // Attack the enemy.
 
    // If the player and npc have similar ability, then it could take many attacks before a blow is landed.
    // In those cases, users were constantly pushing the fight button and the message window would just say
    // hit 0 lost 0. That's silly though, we might as well reroll until someone hits.
    int deliveredDamage = 0;
    int receivedDamage = 0;
    // Although at some point a while loop should eventually return some damage, let's not risk a race
    // condition and just loop a maximum of 10 times.
    for (int attackLoop = 0; attackLoop < 10; attackLoop++)
    {
      deliveredDamage = processAttack(&player, &npc);
      receivedDamage = processAttack(&npc, &player);
      if ((deliveredDamage > 0) || (receivedDamage > 0))
      {
        // Someone got a hit in, exit the loop.
        break;
      }
    }
 
    // Always deal the damage (if any) to the npc. We'll only take damage ourselves if the npc is still alive.
    npc.hitPoints -= deliveredDamage;
 
    if(npc.hitPoints > 0)
    {
      // Enemy still alive, take the damage is dealt us.
      player.hitPoints -= receivedDamage;
      snprintf(lastMessage, sizeof(lastMessage), "Hit %d, lost %d HP!", deliveredDamage, receivedDamage);
      if (player.hitPoints <= 0)
      {
        // We've been killed!
        processDeath();
      }
    }
    else
    {
      // Killed the enemy.
      int npcExperience = (npc.hitPointsMaximum + npc.attack + npc.defense + npc.level) / 4;
      if (npcExperience < 1)
      {
        npcExperience = 1;
      }
      player.experience += npcExperience;
      int npcGold = npc.gold;
      player.gold += npcGold;

      if (npc.type == npcTypeGatekeeper)
      {
        // We won, show winning screen!
        gameSettings.gatekeeperKilled = true;
        if ((player.moves < gameSettings.lowestMovesToWin) || (gameSettings.lowestMovesToWin == 0))
        {
          gameSettings.lowestMovesToWin = player.moves;
        }
        // Reload the location to get a new room description and dead gatekeeper npc.
        updateLocationProperties();
        updateGameWindow();
        window_stack_push(windowConversation, true);
      }
      else
      {
        // Clear out the room.
        npc.type = npcTypeNothing;
        updateNpcProperties();
      }
   
      if (player.experience > player.experienceNextLevel)
      {
        processLevelUp();
        snprintf(lastMessage, sizeof(lastMessage), "Killed! Leveled!");
      }
      else
      {
        snprintf(lastMessage, sizeof(lastMessage), "Killed! Got $%d", npcGold);
      }
    }
  } // npc.targetable && npc.type > npcTypeNothing
  updateGameWindow();
  return;
}

// Function to process the flee action.
static void processActionFlee()
{
  if (gameSettings.chanceToFleePercent > rand() % 100)
  {
    // Successfully fled!
    processActionBack();
    snprintf(lastMessage, sizeof(lastMessage), "Fled back!");
  }
  else
  {
    // Failed to flee, but make the next attempt more likely.
    gameSettings.chanceToFleePercent += 25;
    // Take damage while we're fleeing.
    int receivedDamage = processAttack(&npc, &player);
    if (receivedDamage > 0)
    {
      player.hitPoints -= receivedDamage;
      snprintf(lastMessage, sizeof(lastMessage), "Blocked, lost %d!", receivedDamage);
      if (player.hitPoints <= 0)
      {
        // We've been killed!
        processDeath();
      }
    }
    else
    {
      snprintf(lastMessage, sizeof(lastMessage), "Blocked!");
    }
  }
  updateGameWindow();
  return;
}

// Function to process the heal action.
static void processActionCastHeal()
{
  if (player.spellPoints >= spellPointsHeal)
  {
    player.spellPoints -= spellPointsHeal;
    player.hitPoints = player.hitPointsMaximum;
    if (npc.targetable && !npc.ignorable && npc.type > npcTypeNothing)
    {
      int receivedDamage = processAttack(&npc, &player);
      if (receivedDamage > 0)
      {
        player.hitPoints -= receivedDamage;
        snprintf(lastMessage, sizeof(lastMessage), "Healed, lost %d!", receivedDamage);
      }
      else
      {
        snprintf(lastMessage, sizeof(lastMessage), "Healed!");
      }
    }
    else
    {
        snprintf(lastMessage, sizeof(lastMessage), "Healed.");
    }
  }
  updateGameWindow();
  return;
}

// Function to process the rest action (done at the Inn).
static void processActionRest()
{
  if (player.gold >= costRest)
  {
    player.gold -=costRest;
    player.hitPoints = player.hitPointsMaximum;
    player.spellPoints = player.spellPointsMaximum;
    snprintf(lastMessage, sizeof(lastMessage), "Rested.");
    updateGameWindow();
  }
  return;
}

// Function to process the train attack action (done at the Attack Trainer).
static void processActionTrainAttack()
{
  int costToTrain = costTrainMultiplier * player.attack;
  if (player.gold >= costToTrain)
  {
    player.gold -= costToTrain;
    player.attack += 1;
    snprintf(lastMessage, sizeof(lastMessage), "Trained attack.");
    updateGameWindow();
  }
  return;
}

// Function to process the train defense action (done at the Defense Trainer).
static void processActionTrainDefense()
{
  int costToTrain = costTrainMultiplier * player.defense;
  if (player.gold >= costToTrain)
  {
    player.gold -= costToTrain;
    player.defense += 1;
    snprintf(lastMessage, sizeof(lastMessage), "Trained defense.");
    updateGameWindow();
  }
  return;
}

// Function to reset the game - typically when the player has beaten it.
static void processActionResetGame()
{
  // Go back to a new player state.
  resetGame();
  updateGameWindow();
  // Show the conversation with the king!
  window_stack_push(windowConversation, true);
  return;
}

// Given an action enumeration, this function calls the appropriate function for that action.
static void processAction(int action)
{
  switch(action)
  {
    case actionNothing:
      break;
    case actionForward:
      processActionForward();
      break;
    case actionBack:
      processActionBack();
      break;
    case actionIgnore:
      processActionIgnore();
      break;
    case actionFight:
      processActionFight();
      break;
    case actionFlee:
      processActionFlee();
      break;
    case actionCastHeal:
      processActionCastHeal();
      break;
    case actionRest:
      processActionRest();
      break;
    case actionTrainAttack:
      processActionTrainAttack();
      break;
    case actionTrainDefense:
      processActionTrainDefense();
      break;
    case actionResetGame:
      processActionResetGame();
      break;
  }
  return;
}

// Event handler for when the player presses the select button at the main game screen.
static void buttonGameSelectClicked(ClickRecognizerRef recognizer, void *context)
{
  // Call getActionSelect() to see what action should be performed.
  // Call processAction to actually do it.
  processAction(getActionSelect());
  return;
}

// Event handler for when the player presses the up button at the main game screen.
static void buttonGameUpClicked(ClickRecognizerRef recognizer, void *context)
{
  // Call getActionUp() to see what action should be performed.
  // Call processAction to actually do it.
  processAction(getActionUp());
  return;
}

// Event handler for when the player presses the down button at the main game screen.
static void buttonGameDownClicked(ClickRecognizerRef recognizer, void *context)
{
  // Call getActionDown() to see what action should be performed.
  // Call processAction to actually do it.
  processAction(getActionDown());
  return;
}

// Function to register the button events for the main game screen.
static void subscribeGameWindowButtons(void *context)
{
  window_single_click_subscribe(BUTTON_ID_SELECT, buttonGameSelectClicked);
  window_single_click_subscribe(BUTTON_ID_UP, buttonGameUpClicked);
  window_single_click_subscribe(BUTTON_ID_DOWN, buttonGameDownClicked);
  return;
}

// Event handler for when the player presses the select button at the conversation screen.
static void buttonConversationSelectClicked(ClickRecognizerRef recognizer, void *context)
{
  // This purposely does nothing so players doen't accidently miss important conversation
  // items if they're quickly pushing buttons in the game.
  //window_stack_pop(true);
  return;
}

// Event handler for when the player presses the up button at the conversation screen.
static void buttonConversationUpClicked(ClickRecognizerRef recognizer, void *context)
{
  // This purposely does nothing so players doen't accidently miss important conversation
  // items if they're quickly pushing buttons in the game.
  //window_stack_pop(true);
  return;
}

// Event handler for when the player presses the down button at the conversation screen.
static void buttonConversationDownClicked(ClickRecognizerRef recognizer, void *context)
{
  // This purposely does nothing so players doen't accidently miss important conversation
  // items if they're quickly pushing buttons in the game.
  //window_stack_pop(true);
  return;
}

// Function to register the button events for the conversation screen.
static void subscribeConversationWindowButtons(void *context)
{
  window_single_click_subscribe(BUTTON_ID_SELECT, buttonConversationSelectClicked);
  window_single_click_subscribe(BUTTON_ID_UP, buttonConversationUpClicked);
  window_single_click_subscribe(BUTTON_ID_DOWN, buttonConversationDownClicked);
}

// Event handler when the main game screen is loaded.
static void gameWindowLoad(Window *window)
{
  updateGameWindow();
  return;
}

// Event handler when the main game screen is unloaded.
static void gameWindowUnload(Window *window)
{
  return;                        
}

// Event handler when the conversation screen is loaded.
static void conversationWindowLoad(Window *window)
{
  updateConversationWindow();
  return;
}

// Event handler when the conversation screen is unloaded.
static void conversationWindowUnload(Window *window)
{
  return;                        
}

// Event handler for the main game screen canvas.
static void gameCanvasUpdate(Layer *layer, GContext *ctx)
{
  //GRect(0,80,80,20)
  //wqGPoint start = GPoint(10, 10);
  //GPoint end = GPoint(40, 60);

  // Draw a line
  //graphics_draw_line(ctx, start, end);

  // Pebble watch does not have built in floating point support, so rely on
  // integers for calculation. Using ()s around the 100 multiplication and
  // division to ensure we're not rounding out all precision.

  // Calculate how far we are to the next level.
  int experienceToNextLevel = player.experience - player.experienceLastLevel;
  int experienceForNextLevel = player.experienceNextLevel - player.experienceLastLevel;
  int percentageToNextLevel = (100 * experienceToNextLevel) / experienceForNextLevel;

  // Draw border for experience to next level.
  graphics_draw_rect(ctx, GRect(73,86,65,12));

  // Fill in how much experience we've achieve to the next level.
  int experienceBarPixels = (percentageToNextLevel * 63) / 100;
  graphics_fill_rect(ctx, GRect(74,86,experienceBarPixels,12), 0, GCornerNone);

  return;
}

// Main init call for the Pebble app!
static void init(void)
{
// Create our 2 windows: the main game window and the conversation window.
windowGame = window_create();
  windowConversation = window_create();

  window_set_click_config_provider(windowGame, subscribeGameWindowButtons);
  window_set_click_config_provider(windowConversation, subscribeConversationWindowButtons);

  strcpy(lastMessage, "");

  window_set_window_handlers(windowGame, (WindowHandlers)
                            {
                              .load = gameWindowLoad,
                              .unload = gameWindowUnload,
                            });

  window_set_window_handlers(windowConversation, (WindowHandlers)
                            {
                              .load = conversationWindowLoad,
                              .unload = conversationWindowUnload,
                            });

  //Layer *windowLayer = window_get_root_layer(windowGame);
  //GRect windowBounds = layer_get_bounds(windowLayer);


  // Create Game Layers
  GRect windowBounds = layer_get_bounds(window_get_root_layer(windowGame));
  layerGameCanvas = layer_create(windowBounds);

  // Setup the event for when the game canvas needs to be redrawn.
  layer_set_update_proc(layerGameCanvas, gameCanvasUpdate);

  // Set the locations and sizes for the many text layers on the main game screen.

  layerGameTextActionUp = text_layer_create(GRect(96,0,44,20));
  layerGameTextActionSelect = text_layer_create(GRect(66,60,74,20));
  layerGameTextActionDown = text_layer_create(GRect(96,140,44,20));

layerGameTextLocation = text_layer_create(GRect(0,0,95,20));
layerGameTextLocationDescription = text_layer_create(GRect(0,20,144,20));

  layerGameTextNpcDescription = text_layer_create(GRect(0,40,144,20));
  layerGameTextNpcHitPoints = text_layer_create(GRect(0,60,65,20));

  layerGameTextPlayerExperience = text_layer_create(GRect(0,80,70,20));

  layerGameTextPlayerAttack = text_layer_create(GRect(0,100,50,20));
  layerGameTextPlayerDefense = text_layer_create(GRect(51,100,50,20));
  layerGameTextPlayerGold = text_layer_create(GRect(102,100,42,20));

  layerGameTextPlayerHitPoints = text_layer_create(GRect(0,120,70,20));
  layerGameTextPlayerSpellPoints = text_layer_create(GRect(72,120,70,20));

  layerGameTextMessage = text_layer_create(GRect(0,140,95,20));

  // Set the locations and sizes for the many text layers on the conversation screen.

  layerConversationTextSpeaker = text_layer_create(GRect(0,0,144,20));
  layerConversationTextMessage = text_layer_create(GRect(0,20,144,128));
  layerConversationTextActionBack = text_layer_create(GRect(0,148,144,20));

  // Set the fonts and text alignments.

  text_layer_set_font(layerGameTextActionUp, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerGameTextActionUp, GTextAlignmentRight);

text_layer_set_font(layerGameTextActionSelect, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerGameTextActionSelect, GTextAlignmentRight);

text_layer_set_font(layerGameTextActionDown, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerGameTextActionDown, GTextAlignmentRight);

  text_layer_set_font(layerGameTextLocation, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerGameTextLocation, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextLocationDescription, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextLocationDescription, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextNpcDescription, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerGameTextNpcDescription, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextNpcHitPoints, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextNpcHitPoints, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextPlayerAttack, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextPlayerAttack, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextPlayerDefense, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextPlayerDefense, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextPlayerExperience, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerGameTextPlayerExperience, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextPlayerHitPoints, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextPlayerHitPoints, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextPlayerSpellPoints, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextPlayerSpellPoints, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextPlayerGold, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextPlayerGold, GTextAlignmentLeft);

  text_layer_set_font(layerGameTextMessage, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerGameTextMessage, GTextAlignmentLeft);

  text_layer_set_font(layerConversationTextSpeaker, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerConversationTextSpeaker, GTextAlignmentCenter);

  text_layer_set_font(layerConversationTextMessage, fonts_get_system_font(FONT_KEY_GOTHIC_18));
text_layer_set_text_alignment(layerConversationTextMessage, GTextAlignmentLeft);

  text_layer_set_font(layerConversationTextActionBack, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
text_layer_set_text_alignment(layerConversationTextActionBack, GTextAlignmentLeft);

// Add the layers to the windows.

  layer_add_child(window_get_root_layer(windowGame), layerGameCanvas);

  layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextActionUp));
  layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextActionSelect));
  layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextActionDown));

  layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextLocation));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextLocationDescription));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextNpcDescription));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextNpcHitPoints));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextPlayerAttack));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextPlayerDefense));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextPlayerExperience));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextPlayerHitPoints));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextPlayerSpellPoints));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextPlayerGold));
layer_add_child(window_get_root_layer(windowGame), text_layer_get_layer(layerGameTextMessage));

  layer_add_child(window_get_root_layer(windowConversation), text_layer_get_layer(layerConversationTextSpeaker));
  layer_add_child(window_get_root_layer(windowConversation), text_layer_get_layer(layerConversationTextMessage));
  layer_add_child(window_get_root_layer(windowConversation), text_layer_get_layer(layerConversationTextActionBack));

  // Enable text flow and paging on the text layer, with a slight inset of 10, for round screens
  //text_layer_enable_screen_text_flow_and_paging(layerGameTextLocation, 10);

  bool showIntro = false;

  // Attempt to load an existing game.
  if (!loadGame())
  {
    // Load failed, reset the player.
    resetGame();
 
    showIntro = true;
  }

// Push the main game window with the window animation.
window_stack_push(windowGame, true);

  // If this is the first time we're starting the game on this Pebble, we'll show the
  // intro (which is just the king telling us what he wants).
  if (showIntro)
  {
    window_stack_push(windowConversation, true);
  }

// App Logging!
//APP_LOG(APP_LOG_LEVEL_DEBUG, "Just pushed a window!");

  return;
}

// Main closing function. Saves the game and cleans up variables from memory.
static void deinit(void)
{
  // Save the progress the player has done.
  saveGame();

// Destroy the text layers.
  text_layer_destroy(layerGameTextActionUp);
  text_layer_destroy(layerGameTextActionSelect);
  text_layer_destroy(layerGameTextActionDown);
text_layer_destroy(layerGameTextLocation);
text_layer_destroy(layerGameTextLocationDescription);
text_layer_destroy(layerGameTextNpcDescription);
text_layer_destroy(layerGameTextNpcHitPoints);
text_layer_destroy(layerGameTextPlayerAttack);
text_layer_destroy(layerGameTextPlayerDefense);
text_layer_destroy(layerGameTextPlayerExperience);
text_layer_destroy(layerGameTextPlayerHitPoints);
text_layer_destroy(layerGameTextPlayerSpellPoints);
text_layer_destroy(layerGameTextPlayerGold);
text_layer_destroy(layerGameTextMessage);

  text_layer_destroy(layerConversationTextSpeaker);
  text_layer_destroy(layerConversationTextMessage);
  text_layer_destroy(layerConversationTextActionBack);

// Destroy the windows.
window_destroy(windowGame);
  window_destroy(windowConversation);

  return;
}

// Last but not least, the main function!
int main(void)
{
init();
app_event_loop();
deinit();
}


Copyright (c) 2016 Clinton Kam
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.