Saturday, March 28, 2020

Tumultuous With A T


My store has had a tumultuous year so far. Our sales are up 23%, with net income up 230%, which is easy to do when we were at a negative net income a year ago at this time. The San Francisco Bay Area is on fire, thankfully only figuratively. The Bay Area would be the world's 19th largest economy, if it were tracked that way. I just want to crow about how well we're doing, how well everyone here is doing, so this post doesn't sound like a pity party.

We have transitioned nearly our entire staff this year, a staff that averages a turnover every three years. It has been a huge hit to our institutional knowledge, which means training has been a huge expense. Training means overlapping, unproductive shifts, and it's is our single largest expense this year, when you also include the tremendous wage inflation we've got here in California (at the bottom tier of employment). Starting wages for part timers are going up a dollar a year, but it's not fast enough for many, who criticize us for not having every job starting at a living wage (likely in the $20+ range). We'll get there Felicia, just give it a minute. Enthusiastic new staff are a strong reason for that 23% growth, most of it really, so you get what you pay for.

I will refer to 2019 as my Year of Entropy, assuming my store makes it out alive. Besides expensive staff transitions, our drink cooler died ($2,000). One of our two, multi ton air conditioning units gave up the ghost a couple weeks ago, requiring a new compressor ($3,000). By the end of the year, we'll need two new computers, including a replacement of our six year old POS system which will need the POS software and hardware reinstalled ($5,000). Overall, add these expenses to the usual entropy of plumbing problems and CAM increase and it's about $20,000 out of pocket.

We're still a profitable business. About half that profit goes towards construction loans, so I feel we're investing in the business each month when those checks get processed, even if nothing new arrives. I'm thankful to have windfall profits in a year with crazy high expenses. Imagine having flat sales and all these expenses start beating you down. It's why the threat of failure never goes away for small businesses, never reduces the chance of closing no matter how many years you've been in business. 

Are new expenses hitting us while we're on an upward trajectory or downward? It becomes a simple calculation. Should we cut bait or cast out again? Some of our competitors disappeared this year after doing that calculation. This has added a lot of unexpected energy to our store as the displaced seek new homes. Thankfully there's light at the end of the tunnel. We haven't really been walking in darkness, since it's a profitable business. Having debt while encountering the usual entropy is like walking through a dim tunnel while bats fly overhead and muss your hair. You'll make it, it's just disconcerting.

Meanwhile we'll enjoy a little money thrown at re-branding and selling our updated image. We've had enthusiasm for our new logo, sold some stickers, and talked with people who were unaware of our previous brand identity, which is currently limited to our website and business cards.


Download Shotcut Video Editor Free

shotcut is only free video editor








downlod shotcut free

downlod shotcut video editor software without any water mark

system reqirments
minimum=2gb ram
processor=intel pentunium
64bit computer

shotcut is a free video editor sofware 
  • Support for the latest audio and video formats thanks to FFmpeg
  • Supports popular image formats such as BMP, GIF, JPEG, PNG, SVG, TGA, TIFF, WebP as well as image sequences
  • No import required - native timeline editing
  • Frame-accurate seeking for many formats
  • Multi-format timeline: mix and match resolutions and frame rates within a project
  • Webcam capture
  • Audio capture
  • Support for 4K resolutions
  • Network stream playback (HTTP, HLS, RTMP, RTSP, MMS, UDP)
  • Frei0r video generator plugins (e.g. color bars and plasma)
  • Color, text, noise, and counter generators
  • EDL (CMX3600 Edit Decision List) export
  • Export single frame as image or video as image sequence
  • Videos files with alpha channel - both reading and writing
  • Video Effects

    • Video compositing across video tracks
    • HTML5 (sans audio and video) as video source and filters
    • 3-way (shadows, mids, highlights) color wheels for color correction and grading
    • Eye dropper tool to pick neutral color for white balancing
    • Deinterlacing
    • Auto-rotate
    • Fade in/out audio and fade video from and to black with easy-to-use fader controls on timeline
    • Video wipe transitions:
      bar, barn door, box, clock (radial), diagonal, iris, matrix, and custom gradient image
    • Track compositing/blending modes:
      None, Over, Add, Saturate, Multiply, Screen, Overlay, Darken, Dodge, Burn, Hard Light, Soft Light, Difference, Exclusion, HSL Hue, HSL Saturation, HSL Color, HSL Luminosity.
    • Video Filters:
      Alpha Channel: Adjust, Alpha Channel: View, Audio Spectrum Visualization, Audio Waveform Visualization, Blur, Brightness, Chroma Key: Advanced, Chroma Key: Simple, Contrast, Color Grading, Crop, Diffusion, Glow, Hue/Lightness/Saturation, Invert Colors, Key Spill: Advanced, Key Spill: Simple, Lens Correction, 3D LUT, Mask, Mirror, Mosaic, Old Film: Dust, Old Film: Grain, Old Film: Projector, Old Film: Scratches, Old Film: Technocolor, Opacity, Rotate and Scale, Rutt-Etra-Izer, Saturation, Sepia Tone, Sharpen, Size and Position, Sketch, Spot Remover, Stabilize, Text, Timer, Vignette, Unpremultiply Alpha, Wave, White Balance
    • Speed effect for audio/video clips
    • Reverse a clip
    • Waveform video scope
    • Editing Features

      • Trimming on source clip player or timeline with ripple option
      • Easy-to-use cut, copy, and paste operations
      • Append, insert, overwrite, lift, and ripple delete editing on the timeline
      • 3-point editing
      • Hide, mute, and lock track controls
      • Multitrack timeline with thumbnails and waveforms
      • Unlimited undo and redo for playlist edits including a history view
      • Create, play, edit, save, load, encode, and stream MLT XML projects (with auto-save)
      • Save and load trimmed clip as MLT XML file
      • Load and play complex MLT XML file as a clip
      • Drag-n-drop files from file manager
      • Scrubbing and transport control
      • Keyframes for filter parameters
      • Detach audio from video clip






                                                                           download here



              https://www.sendspace.com/file/q85lmu

Tuesday, March 24, 2020

The Inspirations Of Oceanhorn 2: Knights Of The Lost Realm - Part 3

For this last installment of our Inspirations series, we had the chance to sit down with Heikki Repo, Creative Director of Cornfox & Brothers. Heikki, one of the founders of the company, is responsible for the overall vision and story of the Oceanhorn saga.




"When talking about influences, we need to differentiate between the inspiration for the whole series, and those specific to Oceanhorn 2: Knights of the Lost Realm," he says.


Upon the release of the first iPhone, the whole studio was excited to know everyone will have a powerful gaming machine in their pockets. At the time, the only RPGs on the device where some fairly obscure Korean-style action games – no RPGs in the vein of Zelda or Secret of Mana were available.


"Some of the games I hold most dear from my childhood were portable," says Heikki, "two of my favorites are Link's Awakening and Mystic Quest – Final Fantasy Adventure (Seiken Densetsu). I love them because they could combine the portable experience with extremely high-quality content. Mystic Quest, for example, uses a real myth (think Excalibur) and builds its story upon it. It also has a lot more drama than Zelda – a quite peculiar trait for those years."





Oceanhorn, since the beginning, was planned as Cornfox's own RPG franchise: an homage to the classics with its own personality. Versatility and gameplay experimentation were the keywords the company used as a guiding principle during the development of the first chapter.


"The first Oceanhorn is undeniably a Zelda-like, but we have XPs, and the story becomes increasingly dramatic towards the end – that's not something you'd expect, for instance, from a Zelda game. These ambitions carry on to the second game as well. When it comes to the actual plot, I think I've been deeply influenced by Final Fantasy VI, VII, and IX: they never take shortcuts, and everything that happens there is the outcome of very thoughtfully laid out worlds and events. What actually goes down in the games is the natural consequence of what already had happened before."






The story told in Oceanhorn 2: Knights of the Lost Realm is the background story of the first Oceanhorn: an opportunity to lay strong foundations for the saga, add more details, and create a universe that will keep making sense for potential new projects as well. "The production phase of Oceanhorn 2 brought everything into focus. Certain story elements were a bit vague, and I think we managed to handle them quite well in Oceanhorn 2."


Visually, Oceanhorn 2 will be an inviting, colorful game. Here the references are, again, Zelda and the Mana series: while its approach is console-style, the game will feature some dark undertones.
"Oceanhorn was developed by three people", says Heikki, "me, Antti, and Jukka. It was a 15-20 hours game, so it was a huge undertaking for so few people, but we managed to squeeze in cinematics and most of what you'd expect from an RPG. At the time I was playing The Last Story, Hironobu Sakaguchi's game for Wii." Sakaguchi had previously delivered Lost Odyssey, Xbox 360's own 'Final Fantasy'. The Last Story, developed in collaboration with Nintendo, wasn't destined, for obvious reasons, to set a new graphical standard, but the gameplay was something truly inspiring. "I saw that game as Sakaguchi's idea of where to take the genre's next: he focused on the feeling of presence, with party members talking to each other during gameplay, and an unprecedented possibility to use the environment to your advantage. The story wasn't limited to cinematics, but brought directly to the levels."


Energized by The Last Story, Heikki decided Oceanhorn 2: Knights of the Lost Realm would be a third person experience, with multiple party members.  




"I don't mind when people make comparisons with Skyward's Sword or Breath of the Wild, it means we're giving out the right vibes. If you compare screenshots from Call of Duty and Battlefield it might not always be obvious which one is which, but when you get to play, these games feel quite different. The same is true if you compare Oceanhorn to Zelda or Xenoblade Chronicles – they provide similar experiences but each in its own unique way."


One more saga that had an impact on Oceanhorn 2: Mass Effect. "After I played the Mass Effect Trilogy, I realized how the characters companionship and the way they explore the planets made those games great. I think that that, combined with the Zelda-like heritage of the first Oceanhorn, is what makes Knights of the Lost Realm special", Heikki concludes.

---

Want to read these updates before anyone else? Subscribe to our newsletter.

Saturday, March 21, 2020

Simplifying Encumbrance

Ah, encumbrance. How I've loved to hate thee, but how I've found that thou art needed...

This is my simplified encumbrance approach. Many of you will recognize this as the approach from BX, but it's also the approach taken in Chainmail - encumbrance is based primarily on the armor worn, and then by what else the player is carrying.

To make it more simple, I've simplified that further into just number/type of weapons, amount of treasure or number of items in the pack.

This chart shows the progression of move rates, starting at what kind of armor they are wearing. Shield is included. Rates shift right as the PCs grow more encumbered and shift left as they grow less encumbered. (Note that I use the OD&D/AD&D rates of inches - roughly translated into tens of feet for folks using BX or retroclones.)

Unarmored  Leather/Chain  Platemail  Encumbered  Overencumbered
12"9"6"3"0"

With these move rates, I'm also assuming a small amount of equipment. A light weapon or two and a backpack with a few supplies and equipment (less than a dozen).

For each heavy weapon or every three light weapons, shift one rate to the right.

Heavy weapons include the pole arm, halberd, pike, two-handed sword, morning star, flail, and battle axe or similar. Light weapons include the spear, sword, mace, hand axe, and bow or similar.

For every 300 coins in treasure, shift to the right/heavier. (Alternatively, if they're carrying around sacks of gold - a small sack holds 50 gold, a large sack holds 150 gold).

I usually rule that more than a dozen items in the backpack/on the person requires a shift to the right. It's a rough guideline, if the items are large or bulky, it will be fewer items. If the items are small, I might allow double.

Strength counts! For AD&D, the damage bonus for Strength also counts how many shifts to the left (lighter) they get. For OD&D, Str 14 -16 shifts one left, 17 gives two shifts left, 18, three shifts left.

An example or two:

Kujo the Fighter is wearing chainmail and has a shield. He has a sword, and a backpack with a bedroll, 6 torches (I count the bunch as one item), a crowbar and ten days of iron rations (I'll count that as two items).  His move starts at 9". 

In town, Kujo buys a bow and a quiver of a dozen arrows. He's still at 9"

In the dungeon, Kujo recovers a halberd from a dead orc. He now moves at 6", with the heavy/bulky weapon. Later in the dungeon, he comes upon a treasure chest full of gold and silver coins. There's about 300 coins in that chest, so he's now moving at (a very slow) 3"! I hope Kujo makes it!

If Kujo has a Strength of 16, then for both OD&D and AD&D, he would start at 12" and after his dungeon adventures, be moving at a 6" rate. Go Kujo go! If Kujo had been at an 18 strength, he would have remained at 12" the entire time.

So the question comes up "What's a little vs a lot?" "Why do you rule that someone is carrying a lot."

In many times, it's the duck test - if it quacks like a duck, then I'm pretty sure they're carrying too much. Often, I'll ask the player... are you carrying a lot?  Many times, they'll own up to their own encumbrance. If it "feels" like a lot to me, I'll have the player convince me that they're not carrying a lot. To me, "a lot" is a good amount of equipment, bulky books, weeks of food, etc. If I could go either way, I'll go in favor of the player, but honestly, we all know when we're probably carrying too much.

What do you think?

Friday, March 20, 2020

Insurance (Tradecraft)

A nuts and bolts post? Sure, why not.

A business requires insurance. At the minimum, you need liability insurance. There are other types of insurance though. I've got liability insurance, workers comp insurance, key man life insurance, and for years we had vehicle insurance on our van. Insurance is one of those things new store owners don't always think about.

My second year in business I went to a seminar where a store owner had someone drive through their front window into their store. Twice. He was the likely guy to give a presentation on business insurance and for many, needing such a thing was an eye opener. When I started I had a home and assets and made sure the store was incorporated and insured and far away from my personal life. For most young people without a pot to relieve themselves, that might not be a consideration. 

So how do you get insurance? Find a human. I'm not sure if you can get business liability insurance online, but if so, it's a bad idea. You want to talk to a human about your particular business needs and exactly what you do and where you do it. For example, my store went from a regular retail environment to an improved, two story monstrosity in need of rebuilding if it burned down. It probably wouldn't be rebuilt there, but they'll pay to rebuild it somewhere.  If I had just gotten regular liability insurance, I would be left with no protection and a ton of outstanding loans for a burnt out shell. 

Other considerations include insuring Magic singles. My policy has a "fine art" clause that includes those. What you don't want are surprises. Insurance companies are all about taking as much money from you as they can and paying out as little as possible. It's better to be up front, find angles to cover everything necessary, and avoid surprises. Look at your lease and see what you're responsible for.  Plate glass insurance is often inexpensive, but if someone breaks all your windows, you could be out thousands of dollars. Over time, revisit your policy and up your limits when you add inventory or fixtures.

Who is that human? I have a customer who jumps from job to job, and one of his latest was insurance agent. He was an agent for about four months before he moved on. Do not use a green agent. Find someone experienced. I had a new agent misclassify my company for my workers comp policy, thinking I didn't need to be covered as an owner, when in fact it was necessary. A company audit revealed the mistake, and rather than admitting their agent error, they charged me $3,500 in back premiums. Insurance companies exist to do two things: take as much money as possible while denying your claims. 

Personal insurance agents tend not to handle commercial insurance, so you probably won't be able to use your Aunt Kathy's agency to handle your commercial needs (my Aunt K does my personal insurance). Attempt to find a well regarded commercial agent who has been doing this for a while and lay all your cards on the table.

Classification is tricky as there is no "game store" category for liability insurance. You might be classified as a toy store, a book store, a hobby store, whatever is close, and the cost between agencies and within each agency could be enormous. As long as you don't get cute, something reasonable should be fine.

Avoid buying a policy based entirely on price. Ask what's not covered in your case. Have a good heart to heart with the agent. These policies are really cheap compared to what could happen. I once transitioned between agencies and in a 30 day overlap period, someone broke into my store, doing a lot of damage and stealing a lot of cash (foolish me).  I talked to my new agent who walked me through my old coverage, which sucked, and my new coverage, which was great. I filed a claim with the better coverage, and yeah, they canceled me exactly a year later, but I saved $900. 

Make sure you have extensive business interruption insurance, which is usually standard. Knowing that my store could burn down and I have months to find a new place while paying employees allows me to sleep at night. When you get bigger, losing all your employees can destroy your business as easily as fire. 

What's a Good Company? I don't know! They're all terrible! I can't even remember who I have now. Let me check: Travelers for liability, State Farm for key man life insurance, and AP Intego for workers compensation insurance (pay as you go). Farmers burned me on that workers comp policy I mentioned, so maybe avoid them. 


People Of Frictional: Max Lidbeck

WHO AM I


I'm Max, and I do gameplay programming and design. I joined Frictional about a year and a half ago, and I've been working on one of our super secret projects since.

Yours truly.

For the first nine months or so I, like everyone else, worked from home. Last summer we got an office set up in the heart of Malmö. Since then the amount of days I spend working from home has reduced greatly, though I still do it from time to time.


Setup at home and at work.


These are my two workspaces, the first one in the office and the other one at home (which is rather bare bones right now, moved in just a couple of days ago!). They're quite similar; both the computers and the chairs are the same kind. I wanted to be even more consistent and get the same type of desk as the office one at home, a decision that was ultimately overruled by my better half (apparently it doesn't go with the rest of the decor).

BACKGROUND


Games have always been a big part of my life. Most of my time growing up was spent either playing games or talking about games. But, for quite a while, my family didn't have a PC. Which meant I was stuck playing all sorts of old, weird games on rapidly aging Apple computers. One of my earliest gaming memories consist of repeatedly failing at air-hockey, losing to a hideous pig-man in Shufflepuck Cafe on my dad's old Macintosh.



Eventually I scraped together enough money to put together my first PC, in front of which I would stay rooted for the following years. In addition to playing, I spent a lot of time creating custom content for games with my friends. It was always quite basic though, as I hadn't learned any programming yet.

For a year or so I studied film and media studies at the university, with a diffuse goal of wanting to work in games down the line. One night my girlfriend gave me a push, and I applied for a three-year game development program at Blekinge Institute of Technology (BTH).

My years at BTH were a mixed bag. On one hand, we had a lot of freedom and got to work on tons of small projects, which was very fun and super rewarding. On the other hand, some courses felt like they were only marginally related to game development. Working on side-projects during your spare time was crucial. I got through it all by finding a good group of like-minded students that I stuck to for the entirety of the education. Our final project was a side-scrolling adventure game called Far Away - you can watch the trailer for it on Youtube.



Perfectly in sync with graduating, I stumbled across a job opening at Frictional and sent in an application. Over the following weeks I answered some additional questions, did a work test and finally had an interview. A couple of days before I would hear from Frictional, I got a job offer from another company in software development. I clumsily explained to them I was waiting on another offer and asked for a few more days. Finally, I got an email from Fredrik and Thomas offering me the job. It was a no-brainer, and I happily accepted.

WHAT I DO


My first few weeks at the company consisted of completing a list of introductory tasks, to learn more about the tools and the engine. This was a lot of fun, and culminated in the creation of a silly mini-game where I got to put everything I had learned to the test.

After I had completed the introductory tasks I got to work on Safe Mode for SOMA, which was something I was really excited about -- contributing to a game I truly thought was great. From the get-go, we felt it was important to maintain the monsters' threatening presence in order for their new behaviours to gel with the overall tone of the game. We couldn't just disable their ability to harm you; doing this would end up breaking immersion (imagine repeatedly throwing a toolbox in Akers' face and him just standing there, taking it). Instead, we tried to focus on how to best tweak each monster's behaviour in a manner that suited that particular encounter. For instance, some might eerily walk up to you and size you up, and can even bluff charge you if you've strayed too close. To further enforce the behaviours fitting with the world, we decided that if you were to actively mess with monsters (like invading their personal space for too long, hurling trash at them and so on), they should still be able to hurt you, just not kill you. Overall it was a very worthwhile experience, and I'm quite happy with how it all turned out.

Now I'm working on one of our secret projects. As the gameplay programmer/designer workflow has already been described in previous posts I won't go into detail, but my days in general are spent designing and scripting events and scenes, as well as programming gameplay systems.

THE OFFICE


Additionally, I thought I'd talk a bit about the differences in working from home compared to working in the office. We're also gonna do a proper office tour later on, so stay tuned!

This is where the magic happens.

This is our office! Currently, we're around seven people occupying this space, probably with more to come. It's quite seldom all of us are here at once though, but there are usually a few people around. And on the off chance that you're here by yourself one day, fear not; there's always the noisy, seemingly stiletto heel-wearing, tap-dancing travel agency crew upstairs to keep you company (seriously).

So, it really isn't all that crowded here. But, seeing as most of us don't work from the office, we often have meetings over Slack. It can easily get annoying for your desk-mates if you keep babbling on and on in various meetings throughout the day, which is why we've set up a separate meeting room. It also moonlights as a test room, complete with a TV, some dev kits and a monster webcam.



The fact that the company is split into people working from home and people working in the office could potentially lead to complications, such as communication issues. In order to prevent this we've made sure that all important decisions and discussions still happen over Slack, to keep everyone in the loop. So far this policy has worked well, and the transition has been quite smooth.

In the end, a typical day of work in the office is very similar to one at home. There is of course the added social aspect of working in the same physical space as you colleagues, which is great, but if you one morning feel like you'd rather stay at home and work, you can. Having this option every day really is quite luxurious.

Other than this, and the requirement to wear pants, the routines of working in the office and and working from home differ very little.

Wanna see who else works at Frictional? Check out the rest of the People of Frictional posts!

Thursday, March 19, 2020

Exploring Monster Taming Mechanics In Final Fantasy XIII-2: Data Collection

The monster taming aspect of Final Fantasy XIII-2 is surprisingly deep and complex, so much so that I'm interested in exploring it in this miniseries by shoving the monster taming data into a database and viewing and analyzing it with a website made in Ruby on Rails. In the last article, we learned what monster taming is all about and what kind of data we would want in the database, basically roughing out the database design. Before we can populate the database and start building the website around it, we need to get that data into a form that's easy to import, so that's what we'll do today.

Starting a Data Parsing Script

We already identified a good source for most of the data we want to use from the Monster Infusion FAQ post on Gamefaqs.com. However, we don't want to type the thousands of lines of data from this FAQ into our database because we would be introducing human error with the data copying, and the writers of this FAQ have already gone through all of the trouble of entering the data the first time, hopefully without mistakes. Besides, why would we go through such a tedious process when we could have fun writing a script to do the work for us? Come on, we're programmers! Let's write this script.

Since the website will eventually be in Ruby on Rails, we might as well write this script in Ruby, too. It's not absolutely necessary to write the script in Ruby because it's a one-off deal that will only be run once (when it works) to convert the text file into a format that we can easily import into a database, but Ruby is pretty darn good at text processing, so let's stick with it. I like writing scripts in stages, breaking things down into simple problems and starting with an easy first step, so let's do that here. The simplest thing we can do is read in the text file after saving the FAQ to a local file. To add a bit of debug to make sure we have the file read in, let's scan through and print out the section header for the data we're looking for in the file:
File.foreach("ffiii2_monster_taming_faq.txt") do |line|
if line.include? "MLTameV"
puts line
end
end
Already, this code gives the basic structure of what we're trying to do. We're going to read in the file, loop through every line, look for certain patterns, and output what we find that matches those patterns. The real deal will be much more complex, but it's always good to have a working starting point.

This code also has a few problems that we may or may not want to do anything about. First, it's just hanging out in the middle of nowhere. It's not in a class or function or anything more structured. If this was going to be a reusable parsing tool for converting various FAQs into rows of data, I would definitely want to engineer this code more robustly. But hey, this is a one-off script, and it doesn't need all of that extra support to make it reusable. Over engineering is just a waste of time so we'll leave this code out in the open.

Second, I've got two constant strings hard-coded in those lines: the file name and the search string. I may want to stick the search string in a variable because it's not terribly obvious what "MLTameV" means. The file name, on the other hand, doesn't need to be in a variable. I plan to keep this part of the code quite simple, and it's the obvious loop where the file is read in. On top of that, this code will be very specific to handling this exact file, so I want the file name to be tightly coupled to this loop. If the script is ever copied and modified to work on a different file, this file name string can be changed in this one place to point to the new file that that script works with. I don't see a need to complicate this code with a variable.

Third, when this code runs, it prints out two lines instead of one because there's another instance of "MLTameV" in the table of contents of the file. For locating the place to start parsing monster data, we want the second instance of this string. One way to accomplish this task is with the following code:
SECTION_TAG = "MLTameV"
section_tag_found = false

File.foreach("ffiii2_monster_taming_faq.txt") do |line|
if section_tag_found and line.include? SECTION_TAG
puts line
elsif line.include? SECTION_TAG
section_tag_found = true
end
end
Now only the section header line is printed when this script is run. However, as what inevitably happens when we add more code, we've introduced a new problem. It may not be obvious right now, but the path that we're on with the section_tag_found variable is not sustainable. This variable is a piece of state that notifies the code when we've seen a particular pattern in the text file so we can do something different afterward. When parsing a text file using state variables like this one, we'll end up needing a lot of state variables, and it gets unmanageable and unreadable fast. What we are going to need instead, to keep track of what we need to do next, is a state machine.

Parsing Text with a Finite State Machine

Finite state machines (FSM) are great for keeping track of where you are in a process and knowing which state to go to next, like we need to know in the case of finding the section header for the list of tamable monsters in this text file. In the FSM we always have a current state that is one of a finite number of states, hence the name. Depending on the input in that state, the FSM will advance to a next state and possibly perform some output task. Here is what that process looks like in Ruby for finding the second section tag:
SECTION_TAG = "MLTameV"

section_tag_found = lambda do |line|
if line.include? SECTION_TAG
puts line
end
return section_tag_found
end

start = lambda do |line|
if line.include? SECTION_TAG
return section_tag_found
end
return start
end

next_state = start
File.foreach("ffiii2_monster_taming_faq.txt") do |line|
next_state = next_state.(line)
end
First, the states are defined as lambda methods so that they can easily be passed around as variables, but still called as functions. These variables have to be declared before they're used, so the section_tag_found method either has to be defined first because the start method uses it, or all methods could be predefined at the start of the file and then redefined with their method bodies in any desired order. Another way to define these states would be to wrap the whole thing in a class so that the states are class members, but that kind of design would be more warranted if this FSM was part of a larger system. As it is, this parser will be almost entirely made up of this FSM, so we don't need to complicate things.

We can also represent this FSM with a diagram:


The FSM starts in the Start state, obviously, and it transitions to the Section Tag Found state when there's a matching SECTION_TAG. The unlabeled lines pointing back to the same states mean that for any other condition, the state remains unchanged. This diagram is quite simple, but when the FSM gets more complex, it will definitely help understanding to see it drawn out.

Notice that running through the lines of the text file in the foreach loop became super simple. All that's necessary is to feed each line into the next_state, and assign the return value as the new next_state. The current state is kind of hidden because we're assigning the next_state to itself. Also notice that we need to be careful to always return a valid state in each path of each state method, even if it's the same state that we're currently in. Inadvertently returning something that was not a valid state would be bad, as the FSM is going to immediately try to call it on the next line.

Now that we have an FSM started, it'll be easy to add more states and start working our way through the tamable monster data. What do we need to look for next? Well, we can take a look at the data for one monster and see if there are any defining characteristics:
...............................................................................

MONSTER 001

Name---------: Apkallu Minimum Base HP------: 1,877
Role---------: Commando Maximum Base HP------: 2,075
Location-----: Academia 500 AF Minimum Base Strength: 99
Max Level----: 45 Maximum Base Strength: 101
Speed--------: 75 Minimum Base Magic---: 60
Tame Rate----: 10% Maximum Base Magic---: 62
Growth-------: Standard
Immune-------: N/A
Resistant----: N/A
Halved-------: All Ailments
Weak---------: Fire, Lightning
Constellation: Sahagin

Feral Link-----: Abyssal Breath
Description----: Inflicts long-lasting status ailments on target and nearby
opponents.
Type-----------: Magic
Effect---------: 5 Hits, Deprotect, Deshell, Wound
Damage Modifier: 1.8
Charge Time----: 1:48
PS3 Combo------: Square
Xbox 360 Combo-: X

Default Passive: Attack: ATB Charge
Default Skill--: Attack
Default Skill--: Ruin
Default Skill--: Area Sweep
Lv. 05 Skill---: Powerchain
Lv. 12 Passive-: Strength +16%
Lv. 18 Skill---: Slow Chaser
Lv. 21 Skill---: Scourge
Lv. 27 Passive-: Strength +20%
Lv. 35 Passive-: Resist Dispel +10%
Lv. 41 Passive-: Strength +25%
Lv. 42 Passive-: Resist Dispel +44%
Lv. 45 Skill---: Ruinga

Special Notes: Apkallu only spawns twice in Academia 500 AF. If you fail to
acquire its Crystal in both encounters, you will have to close
the Time Gate and replay the area again.

...............................................................................
That series of dots at the beginning looks like a good thing to search for. It repeats at the start of every monster, so it's a good marker for going into a monster state. We'll also want to pass in a data structure that will be used to accumulate all of this monster data that we're going to find. To make it easy to export to a .csv file at the end, we're going to make this data structure an array of hashes, and it looks like this with the new state:
SECTION_TAG = "MLTameV"
MONSTER_SEPARATOR = "........................................"

new_monster = lambda do |line, data|
if line.include? MONSTER_SEPARATOR
return new_monster, data << {}
end
return new_monster, data
end

section_tag_found = lambda do |line, data|
if line.include? SECTION_TAG
return new_monster, data
end
return section_tag_found, data
end

start = lambda do |line, data|
if line.include? SECTION_TAG
return section_tag_found, data
end
return start, data
end

next_state = start
data = []
File.foreach("ffiii2_monster_taming_faq.txt") do |line|
next_state, data = next_state.(line, data)
end

puts data.length
I shortened the MONSTER_SEPARATOR pattern in case there were some separators that were shorter than the first one, but it should still be plenty long to catch all of the instances of separators between monsters in the file. Notice that we now have to pass the data array into and out of each state method so that we can accumulate the monster data in it. Right now it simply appends an empty hash for each monster it finds. We'll add to those hashes in a bit. At the end of the script, I print out the number of monsters found, which we expect to be 164, and it turns out to be a whopping 359! That's because that same separator is used more after the tamable monster section of the file, and we didn't stop at the end of the section. That should be easy enough to fix:
SECTION_TAG = "MLTameV"
MONSTER_SEPARATOR = "........................................"
NEXT_SECTION_TAG = "SpecMon"

end_monsters = lambda do |line, data|
return end_monsters, data
end

new_monster = lambda do |line, data|
if line.include? MONSTER_SEPARATOR
return new_monster, data << {}
elsif line.include? NEXT_SECTION_TAG
return end_monsters, data
end
return new_monster, data
end

# ...
I added another state end_monsters that consumes every line to the end of the file, and we enter that state from the new_monster state if we see the NEXT_SECTION_TAG. Now if we run the script again, we get a count of 166 monsters. Close, but still not right. The problem is that there are a couple extra separator lines used in the tamable monster section, one after the last monster and one extra separator after a sub-heading for DLC monsters. We're going to have to get a bit more creative with how we detect a new monster. If we look back at the example of the first monster, we see that after the separator the next text is MONSTER 001. This title for each monster is consistent for all of the monsters, with MONSTER followed by a three digit number. Even the DLC monsters have this tag with DLC in front of it. This pattern is perfect for matching on a regular expression (regex).

Finding Monster Data with Regular Expressions

A regex is a text pattern defined with special symbols that mean various things like "this character is repeated one or more times" or "any of these characters" or "this character is a digit." This pattern can be used to search a string of text, which is called matching the regex. In Ruby a regex pattern is denoted by wrapping it in forward slashes (/), and we can easily define a regex for our MONSTER 001 pattern:
SECTION_TAG = "MLTameV"
MONSTER_SEPARATOR = "........................................"
NEXT_SECTION_TAG = "SpecMon"
NEW_MONSTER_REGEX = /MONSTER\s\d{3}/

find_separator = nil

end_monsters = lambda do |line, data|
return end_monsters, data
end

new_monster = lambda do |line, data|
if NEW_MONSTER_REGEX =~ line
return find_separator, data << {}
elsif line.include? NEXT_SECTION_TAG
return end_monsters, data
end
return new_monster, data
end

find_separator = lambda do |line, data|
if line.include? MONSTER_SEPARATOR
return new_monster, data
end
return find_separator, data
end

# ...
The NEW_MONSTER_REGEX is defined as the characters MONSTER, followed by a space (\s), followed by three digits (\d). I changed the new_monster state to look for a match on our new regex, and added a find_separator state to still search for the MONSTER_SEPARATOR. Notice that the FSM will bounce between these two states, so the state that's defined later has to be declared at the top of the file, otherwise Ruby will complain that find_separator is undefined in new_monster.

These regex patterns are useful and powerful, but they can also be quite tricky to get right, especially when they get long and complicated. We'll be using them to pull out all of the data we want from each monster, but we'll try to keep them as simple as possible. The next regex is more complicated, but it will allow us to pull nearly all of the properties for each monster and put it into the empty hash that was added to the list of hashes for that monster. Ready? Here it is:
MONSTER_PROP_REGEX = /(\w[\w\s\.]*\w)-*:\s(\S+(?:\s\S+)*)/
We'll break this regex apart and figure out what each piece means separately.

The first part of the regex, (\w[\w\s\.]*\w), is surrounded by parentheses and is called a capture. A capture will match on whatever the pattern is inside the parentheses and save that matching text so that it can be accessed later. We'll see how that works in the code a little later, but right now we just need to know that this is how we're going to separate out the property name and its value from the full matching text. This particular capture is the property name, and it starts with a letter or number, symbolized with \w. The stuff in the brackets means that the next character can be a letter or number, a space, or a period. Any of those characters will match. Then the following '*' means that a string of zero or more of the preceding character will match. Finally, the property name must end with a letter or number, symbolized with \w again. The reason this pattern can't just be a string of letters and numbers is because some of the property names are multiple words, and the "Lv. 05 Skill" type properties also have periods in them. We want to match on all of those possibilities.

The next part of the regex is -*:\s, which simply means it will match on zero or more '-', followed by a ':', followed by a space. Reviewing the different lines for the MONSTER 001 example above, we can see that this pattern is indeed what happens. Some cases have multiple dashes after the property name, while others are immediately followed by a colon. The colon is always immediately followed by a single space, so this should work well as our name-value separator. It's also outside of any parentheses because we don't want to save it for later.

The last part of the regex is another capture for the property value: (\S+(?:\s\S+)*). The \S+—note the capital S—will match on one or more characters that are not white space. It's the inverse of \s. The next thing in this regex looks like yet another capture, but it has this special '?:' after the open parenthesis. This special pattern is called a grouping. It allows us to put a repeat pattern after the grouping, like the '*' in this case, so that it will match on zero or more of the entire grouping. It will not save it for later, though. Since this grouping is a space followed by one or more non-space characters, this pattern will match on zero or more words, including special characters. If we look at the example monster above, we see that this pattern is exactly what we want for most of the property values. Special characters are strewn throughout, and it would be too much trouble to enumerate them all without risking missing some so we cover our bases this way.

Fairly simple, really. We're going to match on a property name made up of one or more words, followed by a dash-colon separator, and ending with a property value made up of one or more words potentially including a mess of special characters. Note how we couldn't have used the \S character for the property name because it would have also matched on and consumed the dash-colon separator. We also could not have used the [\s\S]* style pattern for the words in the property value because it would have matched on any number of spaces between words. That wouldn't work for the first few lines of the monster properties because there are two name-value pairs on those lines. Now that we have our regex, how do we use those captured names and values, and how exactly is this going to work for the lines with two pairs of properties on them? Here's what the new add_property state looks like with some additional context:
# ...

MONSTER_PROP_REGEX = /(\w[\w\s\.]*\w)-*:\s(\S+(?:\s\S+)*)/

find_separator = nil
new_monster = nil

end_monsters = lambda do |line, data|
return end_monsters, data
end

add_property = lambda do |line, data|
props = line.scan(MONSTER_PROP_REGEX)
props.each { |prop| data.last[prop[0]] = prop[1] }
return new_monster, data if line.include? MONSTER_SEPARATOR
return add_property, data
end

new_monster = lambda do |line, data|
if NEW_MONSTER_REGEX =~ line
return add_property, data << {}
elsif line.include? NEXT_SECTION_TAG
return end_monsters, data
end
return new_monster, data
end

# ...
The double-property lines are handled with a different type of regex matcher, line.scan(MONSTER_PROP_REGEX). This scan returns an array of all of the substrings that matched the given regex in the string that it was called on. Conveniently, if the regex contains captures, the array elements are themselves arrays of each of the captures. For example, the scan of the first property line of our MONSTER 001 results in this array:
[['Name', 'Apkallu'],['Minimum Base HP', '1,877']]
We can simply loop through this array, adding property name and property value to the last hash in the list of hashes. Then, if the line was actually the MONSTER_SEPARATOR string, it didn't match any properties and we'll move on to the next monster. Otherwise, we stay in the add_property state for the next line.

One last thing that we're not handling is those multi-line descriptions and special notes. We need to append those lines to the correct property when we come across them, but how do we do that? Keep in mind that these extra lines won't match on MONSTER_PROP_REGEX, so we can simply detect that non-match, make sure it's not an empty line, and add it to the special notes if it exists or the description if the special notes doesn't exist. Here's what that code looks like in add_property.
MONSTER_PROP_EXT_REGEX = /\S+(?:\s\S+)*/

# ...

add_property = lambda do |line, data|
props = line.scan(MONSTER_PROP_REGEX)
props.each { |prop| data.last[prop[0]] = prop[1] }
return new_monster, data if line.include? MONSTER_SEPARATOR

ext_line_match = MONSTER_PROP_EXT_REGEX.match(line)
if props.empty? and ext_line_match
if data.last.key? 'Special Notes'
data.last['Special Notes'] += ' ' + ext_line_match[0]
else
data.last['Description'] += ' ' + ext_line_match[0]
end
end

return add_property, data
end
By putting the extra code after the return if the line is the MONSTER_SEPARATOR, we can assume that this line is not the MONSTER_SEPARATOR and just check if the MONSTER_PROP_REGEX didn't match and there's something on the line. Then decide on which property to add the line to, and we're good to go.

Okay, that was a lot of stuff, so let's review. First, we read in the file that we wanted to parse that contains most of the monster taming data we need. Then, we loop through the lines of the file, feeding them into a FSM in order to find the section of the file where the list of monsters is and separate each monster's properties into its own group. Finally, we use a few simple regex patterns to capture each monster's property name-value pairs and add them to a list of hashes that will be fairly easy to print out to a .csv file later. All of this was done in 66 lines of Ruby code! Here's the program in full so we can see how it all fits together:
SECTION_TAG = "MLTameV"
MONSTER_SEPARATOR = "........................................"
NEXT_SECTION_TAG = "SpecMon"
NEW_MONSTER_REGEX = /MONSTER\s\d{3}/
MONSTER_PROP_REGEX = /(\w[\w\s\.]*\w)-*:\s(\S+(?:\s\S+)*)/
MONSTER_PROP_EXT_REGEX = /\S+(?:\s\S+)*/

find_separator = nil
new_monster = nil

end_monsters = lambda do |line, data|
return end_monsters, data
end

add_property = lambda do |line, data|
props = line.scan(MONSTER_PROP_REGEX)
props.each { |prop| data.last[prop[0]] = prop[1] }
return new_monster, data if line.include? MONSTER_SEPARATOR

ext_line_match = MONSTER_PROP_EXT_REGEX.match(line)
if props.empty? and ext_line_match
if data.last.key? 'Special Notes'
data.last['Special Notes'] += ' ' + ext_line_match[0]
else
data.last['Description'] += ' ' + ext_line_match[0]
end
end

return add_property, data
end

new_monster = lambda do |line, data|
if NEW_MONSTER_REGEX =~ line
return add_property, data << {}
elsif line.include? NEXT_SECTION_TAG
return end_monsters, data
end
return new_monster, data
end

find_separator = lambda do |line, data|
if line.include? MONSTER_SEPARATOR
return new_monster, data
end
return find_separator, data
end

section_tag_found = lambda do |line, data|
if line.include? SECTION_TAG
return find_separator, data
end
return section_tag_found, data
end

start = lambda do |line, data|
if line.include? SECTION_TAG
return section_tag_found, data
end
return start, data
end

next_state = start
data = []
File.foreach("ffiii2_monster_taming_faq.txt") do |line|
next_state, data = next_state.(line, data)
end
And here's the corresponding FSM diagram:

Final FSM diagram of tamable monster parser

We still need to write the collected data out to a .csv file so that we can import it into a database, but that is a task for next time. Also, notice that we have done almost no data integrity checks on this input other than what the FSM and regex patterns inherently provide. Any mistakes, typos, or unexpected text in the file will likely result in missing or corrupt data, so we'll need to do some checks on the data as well. Additionally, this data is just the tamable monster data. We still need the other table data for abilities, game areas, monster materials, and monster characteristics. However, this is a great start on the data that was the most difficult to get, and we ended up with quite a few extra properties that we weren't intending to collect in the list. That's okay, I'm sure we'll find a use for them.