Friday, April 22, 2022

Coding in TTS (3)

 And to round things out for this time, this next bit is part of a painfully long function which (as far as I can tell at the moment) is required to be painfully long because it is the global object dropped code, so in it must go every decision related to any time any object is dropped anywhere on the board for any reason. Which is a lot of decisions as it turns out.

I don't like this piece. Not because I think I did it wrong (though it is possible there's a more efficient set of logic to accomplish the same result). I don't like it because I don't like any piece of code that is decision nested inside decision nested inside decision nested inside decision, etc, etc. Once I get a certain amount of layers my head starts to hurt and I begin to become overwhelmed with all of the things I must remember about the prior decision but not code at the moment because I must finish with this next decision and its nested counterparts first. Headaches ensue. 

Also, this is nowhere near the most deeply nested set of if/then statements I've ever written. We don't need to talk about how far this can go.

if obj.tag == 'Card' and obj.hasTag("Battalion") then

    --find battalion card type

    local cardType = Global.call("findBattalionType", {obj.guid})

    --check if the battalion card dropped is in the discard zone

    local discardIndex = {sdiscard, kdiscard}

    local lzIndex = {slaneIndex, klaneIndex}

    local footmanBlockIndex = {sfootmanIndex, kfootmanIndex}

    local breacherBlockIndex = {sbreacherIndex, kbreacherIndex}

    local berserkerBlockIndex = {sberserkerIndex, kberserkerIndex}

    local actionZoneIndex = {sactionZone, kactionZone}

    local typeCount = 0

    local typeAction = 0

    for a = 1, 2 do

      local discardObjects = getObjectFromGUID(discardIndex[a]).getObjects()

      for _, card in ipairs(discardObjects) do

        --if the battalion card is in the discard zone, check if the game is in the skirmish phase

        if card.guid == obj.guid then

          --[battalion, discard, skirmish] find the active lane

          laneZoneIndex = lzIndex[a]

          for d = 1, 5 do

            laneObjects = getObjectFromGUID(laneZoneIndex[d]).getObjects()

            for _, tile in ipairs(laneObjects) do

              if tile.hasTag("lane") then

                if tile.getStateId() == 2 then

                  --[battalion, discard, skirmish, active lane] count battalion cards in the lane and count card type in lane

                  for _, stuffs in ipairs(laneObjects) do

                    if stuffs.tag == 'Card' and stuffs.hasTag("Battalion") and stuffs.hasTag(cardType) then

                      typeCount = typeCount + 1

                    end

                  end

                  --[battalion, discard, skirmish, active lane] set counter block to new value

                  if cardType == "Footman" then

                    blockIdIndex = footmanBlockIndex[a]

                  else

                    if cardType == "Breacher" then

                      blockIdIndex = breacherBlockIndex[a]

                    else

                      if cardType == "Berserker" then

                        blockIdIndex = berserkerBlockIndex[a]

                      else

                        blockIdIndex = nil

                      end

                    end

                  end

                  if blockIdIndex != nil then

                    counterBlock = getObjectFromGUID(blockIdIndex[d])

                    counterBlock.setName(typeCount)

                  end

                  --[battalion, discard, skirmish, active lane] count action dice matching card type in action zone

                  actionZoneObjects = getObjectFromGUID(actionZoneIndex[a]).getObjects()

                  for _, actionDie in ipairs(actionZoneObjects) do

                    if actionDie.getName() == cardType then

                      typeAction = typeAction + 1

                      --secure action die in case one needs to be removed

                      anActionDie = getObjectFromGUID(actionDie.guid)

                    end

                  end

                  --[battalion, discard, skirmish, active lane] remove action dice more than card type count

                  if typeAction > typeCount then

                    anActionDie.destruct()

                  end

                end

              end

            end

          end

        end

      end

    end

  end

Coding in TTS (2)

 The routine for rebuilding the holding zone is similarly simple:

function rebuildHold(nfo)

  --the holding zone GUID is in nfo[1]

  zoneObjects = getObjectFromGUID(nfo[1]).getObjects()

  --the default starting position is in nfo[2]

  sPos = nfo[2]

  --the separation increment is in nfo[3]

  incVal = nfo[3]

  --the action zone is in nfo[4]

  actionObjects = getObjectFromGUID(nfo[4]).getObjects()

  --set up the count variable

  dieCount = 0

  --count the number of vanguard action dice currently in the action zone

  for _, die in ipairs(actionObjects) do

    if die.hasTag("action") then

      if die.getName() != "Footman" and die.getName() != "Breacher" and die.getName() != "Berserker" then

        dieCount = dieCount + 1

      end

    end

  end

  --find new starting position based on the number of dice being returned to the holding zone

  sPos = sPos + (incVal * dieCount)

  --move all objects currently in the holding zone arbitrarily

  for _, obj in ipairs(zoneObjects) do

    vPos = obj.getPosition()

    nPos = {sPos, vPos[2], vPos[3]}

    obj.setPosition(nPos)

    sPos = sPos + incVal

  end

end

This is the first routine I wrote which I passed multiple values through. I never doubted it was possible, I just didn't want to mess with it while trying to test out whether or not things were possible in TTS (so that I wasn't confused about where the errors might be coming from should there be any)

Same as before, there are two scripting zones at play (actionZone and holdingZone).

First, loop through the action zone and simply count any die which is an action die but is not a Battalion die. Since I've named the dice when they are generated, this is being done using the name. You probably can see this plainly, but just a "programming" note is the fact that writing die.getName() is completely arbitrary. If I had written for _, apples in ipairs(actionObjects) do then it would be written apples.getName(). Like I said, I am sure that's patently obvious, but sometimes when I read other people's code I'm like "is he writing die because that's a thing this program understands or just because he decided it would be called die and defined that elsewhere?"

Second, calculate how many die "positions" are going to be used by the number of Champion dice currently in the action zone (using the incVal which is holding the current increment value for die positioning). 

Third, move any dice currently in the holding zone down to make room for the dice which are about to be moved in. I use the vector vPos just becuase it is convenient to grab the current vector of whatever object already exists and simply move the X value to the new starting position without trying to remember or calculate the Y or Z values. Again, this is likely very, very standard practice. But I think very absolutely. I am most comfortable telling the script to put something at exactly these coordinates right here. So, I get really proud of myself any time I remember to program something in a relative way.

Coding in Tabletop Simulator (TTS)

 Sometimes, I feel like my code is really elegant. I have the bits I need and there isn't a ton of logic to flesh out, so something that felt complicated when I was thinking about adding it turns out to be pretty simple when I actually write the bits:


--Build loop index

  holdIndex = {sactionHold, kactionHold}

  actionIndex = {sactionZone, kactionZone}

  startingPosition = {8.5, -9.2}

  incrementValues = {1, -1}

  --loop through both sides to clear action dice

  for i = 1, 2 do

    startPlace = startingPosition[i]

    advanceVal = incrementValues[i]

    actionHold = getObjectFromGUID(holdIndex[i])

    actionObjects = getObjectFromGUID(actionIndex[i]).getObjects()

    --rebuild holding zone

    Global.call("rebuildHold", {holdIndex[i], startPlace, advanceVal, actionIndex[i]})

    for _, die in ipairs(actionObjects) do

      if die.hasTag("action") then

        if die.getName() == "Footman" or die.getName() == "Breacher" or die.getName() == "Berserker" then

          --just remove the action die from the game by destroying it.

          --Battalion cannot change lanes, so when a lane becomes inactive, the Battalion's actions become meaningless until next round

          die.destruct()

        else

          dPos = {startPlace, 1.16, 27}

          die.setPosition(dPos)

          die.setLock(true)

          startPlace = startPlace + advanceVal

        end

      end

    end

  end


This bit uses two scripting zones I placed on the table (four actually, because everything is divided into the two sides). As I've mentioned before, I tend to just focus on getting parts working more than on writing clean or simple or efficient code. In fact, in the past, I've mostly only cleaned up inefficient code when it was actually affecting performance (like calling 10 queries on a database in order to keep the data set small instead of calling 1 query and using a very large data set to resolve everything. I spent a year cleaning this type of methodology up at work)

The first, obvious thing you'll notice if you're opening up my code is that most of the foundational bits I've written are divided into two parts - one for one side of the table and the second for the other side. It still works this way, of course, but now I am (duh) using a loop and a couple indexes to divide up the unique identities for each side. 

actionHold is the part on the side of the table where the action dice are initially set up for the round. 

actionZone is the actual tile between the lane Guardians where players can interact with their action dice. 

All this routine is doing is taking all of the action dice off of the action tile and placing them back into the holding area. This is to support the concept that a Champion which still possesses an action may be moved to an active lane and still use their action. So, I figured the logic for this was to 1) establish a die at a set point in the game which either exists or doesn't exist, 2) move that die into the action zone any time the Champion is moved into an active lane, 3) preserve the die whenever a lane goes inactive without the Champion using their action, 4) remove all remaining dice at the end of the round prior to setting up the next round. That's why I figured a holding zone and an active zone would be ideal.

As far as the holding zone being visible to the players, I am not sure what direction would be best on that. I definitely considered keeping the holding zone hidden, so that the only time the players see the action dice is when they are placed on the action tile. This could streamline player recognition of what the dice are and what to use them for. On the other hand, there is benefit to having them visible to both players throughout the round. It gives an overall view of what the round will look like. 

We talked about the die removal (which is not the above function). I am still torn about just assuming that any battalion being removed from the board is going to take an action with them. This is actually based more on the times when a player might choose which of their own Battalion to remove than on when their opponent will defeat one. There are circumstances, like using a Paragon reaction or just dealing with untargeted damage, that a player will decide which battalion to remove. For these circumstances, it is best to let the player manage the removal of the dice. 

This, of course, means that there is potential confusion. If a player does not remove a die when they are supposed to do so, then they might mistakenly (or purposely) gain extra actions that they should not have just because the dice exist for the sake of clarity (clarity causing obfuscation). This is really a result of the fact that is is INCREDIBLY inconvenient to tie action dice to any specific card in the game. There is an expansion concept which would give all battalion individual names and portraits. In this version, sure, it would be easy to tie the dice to the cards. But, right now, not so much. It's definitely something that could be handled very cleanly outside of TTS.

Anyway, the basics here are:

Find where the first die should be placed  when moved to the holding zone: startingPosition

Find out how many units to separate the dice when subsequent dice are moved: incrementValues

Call a different routine which counts how many dice are going to be moved and moves everything already in the holding zone enough distance away from the starting position to make room for the dice being moved in: rebuildHold

Loop through all dice in the action zone and destroy any battalion dice while moving any Champion dice to the holding zone.

When a die is moved, advance the startingPosition value so the next die which gets moved will not get moved to the exact same position (not really a problem other than the visual confusion it causes and the fact that it sort of defeats the whole purpose of having the dice in the holding zone visible to the players in the first place.) If I were hiding the dice being held, then this whole thing would be a bit simpler.

Monday, February 7, 2022

Logitech Mouse Follow-Up

 Well, I've had this Logitech G502 SE for about a year and a half now. I wasn't super sold on it in the first place, but there are some failings that have really stuck out over its lifespan which have finally made me desperate to move away from it to a different mouse. Sadly, I cannot move back to the Razer Lancehead that I loved so much, because it was poorly made and lasted less than a year before it stopped functioning correctly. I am not about to buy another Razer mouse at this point, because I don't have the money to do so. However, thankfully, I do have a few mice laying around. So I will be transitioning between them until I settle on the correct course of action. 

In reality, the mouse I've used the most over the past year or so had been the Logitech M590, which can easily switch between my laptop and my main system. I have also come to realize that, for whatever reason, I really love mice with silent clicks. I might have simply moved to the M590 as my main mouse except for the fact that it is much too small. I can only use it for a few hours before my hand starts to cramp from the tight claw-grip it forces me into. I didn't prefer large mice in the past, but after the Roccat Kone XTD showed me what I'd been missing, I've been enamored.

This brings me to the first failing of the Logitech G502: it's too small. I was wary of moving to an ergonomic mouse in the first place, since I've long preferred ambidextrous layouts. The ergonomic shaping has not been a large hindrance and I can even see how it might be nice to use, however the mouse is too small for my hand to get any real benefit from its angled shape. It is some relief from the tiny size of the M590, but not really very much.

The second failing for Logitech is their software. I originally complained about the trash software that Razer forces on all of its customers. I stand by that complaint. However, I had no concept that Synapse in all of its revolting bloaty spyware glory could possibly be BETTER than its competition. Logitech's G HUB is everything bad that Synapse is, plus it doesn't even work correctly. At least Synapse has the good grace to function as expected most of the time. G HUB seems to want to change my profile to useless defaults every time I press Alt-Tab. As if Logitech's software engineers failed to consider a world where computer users might switch between tasks. It is infuriating and frustrating. There's also no way to stop it beyond simply turning the software off. You can retain profiles, if you like going into G HUB settings any time you want to switch profiles to change the default to the one you currently deisre. It is clunky, unnecessary and disheartening. 

I will say that the G502 at least gave me what I expected: it has lasted. There's nothing wrong with the mouse on the hardware side. Well, at least nothing wrong with it that wasn't already wrong with it when I first opened the box. See, the G502 line has an incredibly annoying mouse button pullback issue (the button sticks to your finger and then snaps off, resulting in an irritatingly pingy slapback click after each press). It is atrocious. I've attempted to ignore it for a year and a half. 

I am so ready to be done with this mouse. Logitech is great, but their mice are just as big of failures as any other company's. 

Wednesday, January 12, 2022

2022 Chromebooks 02

My initial reintroduction (it's been over 6 months) to the world of Chromebooks was perhaps a little disappointing. I made a lot out of my inescapable longing to move back to a Chromebook as my primary computer in my last missive, but in actual practice the time away had stolen some of the glimmer I expected to experience. 

First, I was unable to connect to my Citrix desktop, so the Chromebook was immediately out as a machine available for work. This was disconcerting, as its ability to supplement my desktop for this task was one of the more attractive benefits. I wouldn't be lost without it, certainly, as my workplace has already provided quite a powerful laptop to meet this need. I could absolutely take that laptop out and about as needed to work away from my desktop. I would imagine my workplace would prefer I take this approach. However, the work laptop is a bit unwieldly and it is actively monitored by my employer. As much as I am sure it isn't a big deal, I wouldn't want to browse Amazon or write a blog post while logged in on my work laptop. I'd only want to enter bills, which is my job. In that regard it is perhaps even healthier for me to use my work laptop for work. But I am selfish person. I like my freedom, even if I don't actually intend to use it.

Then, the Android game I've been fiddling with wasn't authorized to run on Chromebooks. Phones only. Which is dumb and totally not my Chromebook's fault. Even so, it was a disappointment. And it made me sad.

Then, all the Android games on every device I own went ahead and installed themselves on my Chromebook. I had to uninstall them all. And then they were all uninstalled from all my other Android devices. I mean... I like the automatic synchronization, but the implementation is a little awkward in this context. I don't actually know how to separate specific devices from what appears to be the communal install pool. I should probably figure this out.

Finally, I was thinking all the extra power under the hood would make for a more fluid and precise drawing experience that what has been afforded by my Chromebook Duet. It does. However, the screen is a bit tackier than the glossy coat on the Duet. This is actually nice when using the Chromebook as ... a Chromebook. But the stickiness isn't super pleasant when using a pen. And then, also (and perhaps super obviously), the Chromebook is a good deal heavier than the Duet. It's not pleasant to write on unless it is sitting on a tabletop. The Duet, on the other hand is light and easy to handle. 

So, with such an array of disappointments, perhaps I should be filled with regret. I am not. But maybe I should.

Thursday, January 6, 2022

On Chromebooks in a new year

The central question to my first analysis of Chromebooks revolved around whether or not I could use the platform to replace my dependency on Windows as an operating system to enable my activities. While this was, perhaps, an unfair premise for evaluation, it was also necessary. As Microsoft has more openly discarded any concern for individual freedom or anonymity, the pressing desire to abandon their product has grown. Yet there are very pronounced dependencies to attend. Steam gaming habits. Photoshop projects. File storage. Video editing. Audio editing. None of these pastimes could be abandoned and all would require replacement. Even so, no suitable replacement is found. This was rendered frustrating because of the perceived necessity of migration.

Yet, with honest evaluation, is it really fair to laud the Chromebook as a more private alternative to Windows in its most invasive form? Not really. Google's nascent OS initiative is built upon a Faustian disregard for personal privacy. If my primary concern is being tracked, traced and catalogued then Chromebooks simply do not apply. 

So, in the air of this reality, I tried to disregard the death of my Chromebook. I complained to my wife on the day it refused to charge or boot again, thinking perhaps we would agree to find room in the budget for a replacement. However, we were in dire financial straits at that moment. The idea of purchasing a new computing device was laughable at best - especially when weighed against the fact that I posses plenty of devices, including another Chromebook (tablet). No deprivation would actually occur. These realizations settled in quickly and I sought to save face by not pressing the issue. The machine I once considered my main system ignobly passed from paperweight to trash bin without further remark. And this was meant to be the end of it. 

So, why am I continuing to discuss Chromebooks? As it turns out, Chromebooks are worthy of consideration for their own merits. They do not need to be positioned as a reasonable replacement for something Windows or Mac. They are just great in and of themselves. In the weeks which followed where I attempted to acclimate myself to life without a Chromebook, I found my reliance on Chromebook tablet greatly increased and my reliance on Windows computer unchanged. Windows did not move in to fill the gaps left behind by my deceased Chromebook. In many ways, it couldn't.

That statement probably strikes readers as odd. Rightfully so. Summarily, I just stated that Chromebooks are incapable of replicating many of the necessary tasks performed by Windows-based computers. It is well known that the base functions Chromebooks do offer are also available in the Windows ecosystem. This is the core argument of all those who insist Chromebooks are only good as cheap alternatives for people who couldn't (or shouldn't) afford better. The people who insist this are MANY. There are entire websites run by them. People who own and love iPads and attempt to review Chromebook tablets - inevitably coming to the conclusion that the Chromebook tablet is, in fact, a tablet but not actually an iPad and therefore unworthy of money. People who own and love Windows gaming computers who insist that Chromebooks can play games, but they still fail at running the games which only run on their Windows gaming computer. People who aggregate such sentiments to state that Chromebooks with powerful hardware are merely silly excess because Chromebooks are obviously nothing more than a niche alternative for people who can't afford the real thing.

It is tempting to see the logic in these statements. Indeed, I have bought the false syllogism personally. Yet, one thing remained. I missed my Chromebook. Daily. 

Oddly, when my Windows computer stopped working, I forged ahead and managed to only miss it very circumstantially until I had no choice but to replace it. I have used both iPad and Macbook and somehow manage to live without missing either at all. But my niche, unnecessary alternative Chromebook I've missed daily. Fervently. Passionately. Annoyingly. 

I didn't replace my Chromebook because there were just so many things I couldn't do without it. I replaced it because I wanted to have it for the things I can do with it. I am not sure what that means, but certainly, for myself, I have to recognize that ChromeOS is not just a cheap, less effective alternative to other so-called "full" OS experiences. ChromeOS is a full OS in its own right, worth my money and my consideration.

So, perhaps I should continue to explore why.

Thursday, December 3, 2020

Logitech

 I wouldn't say that I am a fan of Logitech's mice for gaming. Generally, I think they are gaudy and poorly designed. I am saying this as a person who has loved Razer mice for over a decade. So... take that as you will. However, Razer has let me down, Roccat doesn't sell the mouse I liked any more and the Corsair mouse I tried died within 2 months. 

My options are very limited.

I don't want to rest on my pride and prejudices, so I decided to set all that aside and get a Logitech mouse, since Logitech seemed to be my only remaining option. I got the G502 SE, which was the cheapest decent gaming mouse they offered. It is not loaded with buttons, but it has a few.

One thing I feel I can expect from Logitech, however, is reliability. Reliability is what my mice have been lacking lately, so I believe this choice will be best in the end.