Ultima 5 Underworld in R

Weihnachten 1989! Unter dem Gabentisch liegt Ultima V. Yippie... Mama sei Dank!


Quelle: wikipedia.org

Meine vom Papa "ausgeborgte" Hardware war eher bescheiden, ein Aries Computer XT mit 8088er Prozessor, wahlweise 4,77Mhz oder mit Turbo Taste 10Mhz. Die Grafikkarte ein Hercules Graphics Adapter. Auflösung 720x348 Pixel in 2 Farben. Also genau genommen 1 Farbe, bei mir nämlich Grün, oder eben kein Grün, heisst Schwarz.

Damals gab es kein Internet, zumindest nicht für mich, und man musste alles selbst herausfinden, oder man hat es von Freunden erfahren oder aus Spielemagazinen.

Ein Zentraler Punkt in dem Spiel ist es die in der Spielreihe neu erschaffene Unterwelt zu erkunden. Ähnlich wie der "CRPG Addict" hatte ich damals schon das Bedürfnis die Spielwelt zu kartographieren.

Zu Beginn habe ich in der Unterwelt über "view a gem" die In Game Map aufgerufen und den kleinen dort angezeigten Bildausschnitt über eine Tastenkombination direkt an den Matrixdrucker geschickt.
Die In Game Map habe ich dann ausgeschnitten und alle Teile zusammen geklebt.

Allerdings gibt es in der Underworld Bereiche wo man nicht so gut hinkommt, so dass sie in der In Game Map nicht aufscheinen. Da meine Gedruckte Karte also Lücken hatte, bin ich auf die Idee gekommen direkt in den Spieldateien nach der Karte zu suchen. Wie wird die Karte der Unterwelt wohl heißen?
under.dat klingt nahe liegend.



Oh! Genau 65536 bytes. Sehr verdächtig! Das ist eine Potenz von 2, und "zufällig" genau 256*256.
Damals habe ich ein Turbo Pascal Programm geschrieben, die Datei eingelesen und als Bild dargestellt. Nach kurzer Zeit bin ich drauf gekommen, dass in der Datei die Karte nicht einfach alles zeilenweise abspeichert ist, sondern immer Blöcke von 16x16 hintereinander dargestellt werden.

Soviel 1989, im April 2020 bin ich eines Tages aufgewacht und mit der dringenden Frage: "Kann ich das über 30 Jahre später immer noch?". Gesagt, getan, diesmal allerdings nicht mit Turbo Pascal oder Delphi, sondern am einfachsten in R. (R version 3.5.1, R Studio Version 1.2.5033)

# Zuerst tidyverse laden
# wir brauchen das für ggplot und dplyr
library(tidyverse)

rm(list = ls())

# die Datei UNDER.DAT (hier ist die Karte abgespeichert, einlesen)

finfo = file.info("UNDER.DAT")
toread= file("UNDER.DAT", "rb")
alldata = readBin(toread, integer(), size=1, n = finfo$size, endian="little")

# jetzt werden die Daten aus der Datei, die in 16x16 Blöcken abgespeichert sind
# in eine Matrix mit den Spalten "x", "y" und dem Wert umcodiert.
# zugegeben, das geht sicher eleganter.
# Da R das als Integer einliest mache ich durch Additon von 128 wieder ein Byte draus

df = matrix(NA, 65536, ncol = 3)
for (xb in 1:16){
   for (yb in 1:16){
     for (l in 1:16){
        for (k in 1:16){
          z = (l +(k-1)*16)+(yb-1)*256 + (xb-1) *4096
          y= k + (xb-1)*16
          x= l + (yb-1)*16
          df[z,1] =x
          df[z,2] =257-y
          df[z,3] = alldata[z] +128
        }
     }
   }
}

# aus der Matrix wird ein Data Frame, die Bytes (von R als Integer eingelesen) repräsentieren jeweils ein Geländefeld. Das ist dann sinnvollerweise in R ein factor.

df_n = as.data.frame(df) %>%
  select(x=1,y=2, wert= 3) %>%
  mutate(wert= as.factor(wert))

# damit ist die Karte von 16x16 Blöcken in eine tidy Darstellung x,y umformatiert
# Unterschiedliche Geländetypen kommen unterschiedlich oft vor.
# Schaun wir mal was am öftesten vorkommt:

df_n %>%
  group_by(wert) %>%
  summarize(count = n()) %>%
  arrange(desc(count))

# hier die ersten 10
# wer das Spiel kennt weiß sofort, am häufigsten sind in der Unterwelt Mountains, Peaks und Grass.
# also die Werte 140, 141 und 133

# Eine genauere (nicht vollständige) Analyse offenbart welcher Wert welchem Geländetyp
# entspricht. Ich ordne jedem Typ eine hoffentlich sinnvolle Farbe zu:

farben = data.frame(
  stringsAsFactors = FALSE,
  wert = c(140L, #Mountains
           141L, #Peaks
           133L, #Grass
           143L, #Foothills-left
           142L, #Foothills-right
           132L, #Swamp
           130L, #Water
           139L, #Foothills-full
           129L, #Sea
           131L, #Shoals
           224L, #River N-S
           183L, #grass - rechtsoben grass/linksunten shoals
           181L, #grass - rechtsoben shoal/linksunten grass
           225L, #River W-E
           226L, #River N-E
           228L, #River W-S
           84L, #Waterfall
           176L, #Grass Beach N
           127L,#?
           180L,  #Grass - linkssoben shoal/rechtsunten grass
           15L, #Lava?
           227L, #River S-E
           182L, #Grass - linkssoben grass/rechtsunten shoal
           178L, #Grass Beach S
           229L, #River N-W
           177L, #Grass Beach E
           179L, #Grass Beach W
           236L, #River Mouth N (narrow)-S
           238L, #238 ?
           232L, #232 River N-S-W
           239L, #239 River Mouth W (narrow) -E
           230L, #River N-S-W
           237L, #River Mouth E (narrow) -W
           9L,   #Sign
           231L, #River W-E-S
           150L, 152L, 151L, 233L, 36L,94L,146L,158L,159L),
  mycolor = c("brown", "black", "green3", "darkgray", "darkgray",
              "darkgreen","blue", "darkgray", "darkblue",
              "lightblue", "blue", "seagreen3","seagreen3",
              "blue","blue","blue","lightblue", "seagreen4",
              "white","seagreen3","orange","blue","seagreen3",
              "seagreen3", "blue", "seagreen3", "seagreen3",
              "blue", "white", "blue", "blue", "blue", "blue",
              "gray","blue","white","white","white","white",
              "white","white", "white", "white", "white"))

# Die Werte sind hier auch wieder Faktoren, sie werden an unser data.frame mit den Koordinaten angehängt

farben = farben %>% mutate(wert= as.factor(wert))
df_n = df_n %>%
  left_join(farben, by="wert")

# Jetzt eine Grafik draus machen

ggplot(data= df_n %>% mutate(wert= as.factor(wert))) +
  geom_tile(aes(x=x , y=y, color=mycolor),
            size =2)+
  scale_color_identity()+
  theme_minimal()+
  theme(legend.title = element_blank(),
        legend.position = "none",
        legend.text=element_blank())+
  theme(axis.text.x=element_blank(),
        axis.title.x=element_blank(),
        axis.text.y = element_blank(),
        axis.title.y= element_blank())+
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank())


Voila!:

Ich versuche mir vorzustellen wie 1987 oder 1988 jemand bei Origin gesessen ist und die Karte gezeichnet hat. Ob er/sie sich vorstellen konnte, dass 2020 sich jemand nochmals damit beschäftigt? Wohl eher nicht.
Das wars. Ich werde dieses Thema von Zeit zu Zeit wieder aufgreifen.





Comments

Popular posts from this blog

Breaking Excel Encryption (Password Recovery) Part 1

Schach mit ggplot