text:
<repo_name>dannwebster/gurps-status-tracker<file_sep>/src/test/kotlin/settings/FileCampaignSettingsBuilderTest.kt
package settings

import org.junit.Assert.*
import org.junit.Test
import java.io.File
import java.io.InputStream

/**
 */
class FileCampaignSettingsBuilderTest {
    @Test fun shouldHaveDefaultValuesWhenFileNull() {
        // given
        val file: File? = null

        // when
        val subject = FileCampaignSettingsBuilder().build(file)

        // then
        assertEquals(false, subject.showER)
        assertEquals(false, subject.showWP)
    }

    @Test fun shouldHaveDefaultReadValuesWhenFileIsAvailable() {
        // given
        val inputStream: InputStream = this::class.java.getResourceAsStream("/settings.yaml")

        // when
        val subject = FileCampaignSettingsBuilder().build(inputStream)

        // then
        assertEquals(true, subject.showER)
        assertEquals(true, subject.showWP)
    }
}<file_sep>/src/main/kotlin/Start.kt
/**
 */

import com.xenomachina.argparser.ArgParser
import settings.*

class Start() {

    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val programSettings = ArgumentsProgramSettingsBuilder(ArgParser(args)).build()

            val campaignSettings = campaignSettings(ArgParser(args), programSettings)
            val characterSettings = characterSettings(ArgParser(args), programSettings)

            val statusTracker = StatusTracker(campaignSettings, characterSettings)
            val img = ImageVisualization(statusTracker)
            img.exportImage(programSettings)
        }

        private fun characterSettings(argParser: ArgParser, programSettings: ProgramSettings): CharacterSettings {
            val fileCharacterSettings = FileCharacterSettingsBuilder().build(programSettings.characterSettingsFile)
            val characterSettings = ArgumentsCharacterSettingsBuilder(argParser, fileCharacterSettings).build()
            return characterSettings
        }

        private fun campaignSettings(argParser: ArgParser, programSettings: ProgramSettings): CampaignSettings {
            val fileCampaignSettings = FileCampaignSettingsBuilder().build(programSettings.campaignSettingsFile)
            val campaignSettings = ArgumentsCampaignSettingsBuilder(argParser, fileCampaignSettings).build()
            return campaignSettings
        }
    }

}

<file_sep>/src/main/kotlin/settings/Settings.kt
package settings

import java.io.File

/**
 */
data class ProgramSettings(
        val verbose: Boolean = false,
        val imageOutputFile: File?,
        val campaignSettingsFile: File,
        val characterSettingsFile: File?) {
}

data class CampaignSettings(val showER: Boolean, val showWP: Boolean) {
    companion object {
        val DEFAULT = CampaignSettings(false, false)
    }
}

data class CharacterSettings(
        // meta data
        val characterName: String,
        val playerName: String,

        // primary stats
        val ht: Int,
        val st: Int,
        val iq: Int,

        // drived stats
        val hp: Int,
        val fp: Int,
        val wp: Int,
        val er: Int,

        // tracked stats
        val currentHP: Int,
        val currentFP: Int,
        val currentWP: Int,
        val currentER: Int
) {
    companion object {
        val DEFAULT = CharacterSettings("Unknown Character", "Unknown Player",
                10, 10, 10,
                10, 10, 10, 10,
                10, 10, 10, 10
        )
    }
}
<file_sep>/src/test/resources/character.yaml
characterName: KNOWN Character
playerName: KNOWN Player

st: 11
iq: 12
ht: 13

hp: 14
fp: 15
wp: 16
er: 17

currentHP: 18
currentFP: 19
currentWP: 20
currentER: 21<file_sep>/src/main/kotlin/settings/Files.kt
package settings

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import java.io.File
import java.io.InputStream
import java.io.Reader

/**
 */

fun <T> InputStream.yaml( callback : (reader: Reader, ObjectMapper) -> T): T {
    val mapper = ObjectMapper(YAMLFactory()) // Enable YAML parsing
    mapper.registerKotlinModule()
    this.reader().use { reader -> return callback(reader, mapper) }
}

class FileCampaignSettingsBuilder(val defaults: CampaignSettings = CampaignSettings.DEFAULT) {

    fun build(inputStream: InputStream) =
            inputStream.yaml { reader, mapper ->
                val root = mapper.readTree(reader)
                val showER = root["showER"].asBoolean()
                val showWP = root["showWP"].asBoolean()
                CampaignSettings(showER ?: defaults.showER, showWP ?: defaults.showWP)
            }

    fun build(campaignSettingsFile: File?) =
            if (campaignSettingsFile == null || !campaignSettingsFile.exists())
                defaults
            else campaignSettingsFile.inputStream().use { build(it) }
}

class FileCharacterSettingsBuilder(val defaults: CharacterSettings = CharacterSettings.DEFAULT) {

    fun build(characterSettingsFile: File?) =
            if (characterSettingsFile == null)
                defaults
            else characterSettingsFile.inputStream().use { build(it) }

    fun build(inputStream: InputStream) =
            inputStream.yaml { reader, mapper ->
                val root = mapper.readTree(reader)
                CharacterSettings(
                        root["characterName"].asText(defaults.characterName),
                        root["playerName"].asText(defaults.playerName),

                        // primary stats
                        root["ht"].asInt(defaults.ht),
                        root["st"].asInt(defaults.st),
                        root["iq"].asInt(defaults.iq),

                        // drived stats
                        root["hp"].asInt(defaults.hp),
                        root["fp"].asInt(defaults.fp),
                        root["wp"].asInt(defaults.wp),
                        root["er"].asInt(defaults.er),

                        // tracked stats
                        root["currentHP"].asInt(defaults.currentHP),
                        root["currentFP"].asInt(defaults.currentFP),
                        root["currentWP"].asInt(defaults.currentWP),
                        root["currentER"].asInt(defaults.currentER))
            }
}<file_sep>/src/test/kotlin/settings/ArgumentsProgramSettingsBuilderTest.kt
package settings

import com.xenomachina.argparser.ArgParser
import org.junit.Assert.*
import org.junit.Test

/**
 */
class ArgumentsProgramSettingsBuilderTest {
    @Test fun shouldHaveDefaultValuesWhenArrayIsEmpty() {
        // given
        val args: Array<String> = arrayOf()
        val argParser = ArgParser(args)

        // when
        val subject = ArgumentsProgramSettingsBuilder(argParser).build()

        // then
        assertEquals(null, subject.imageOutputFile?.toString())
        assertEquals(ArgumentsProgramSettingsBuilder.DEFAULT_SETTINGS_PATH, subject.campaignSettingsFile.toString())
        assertEquals(null, subject.characterSettingsFile)
        assertEquals(false, subject.verbose)
    }

    @Test fun shouldHaveValuesWhenUsingLongArgs() {
        // given
        val args: Array<String> = arrayOf(
                "--image", "image.png",
                "--settings", "settings.yaml",
                "--character", "character.yaml",
                "--verbose"
        )

        val argParser = ArgParser(args)

        // when
        val subject = ArgumentsProgramSettingsBuilder(argParser).build()

        // then
        assertEquals("image.png", subject.imageOutputFile?.name)
        assertEquals("settings.yaml", subject.campaignSettingsFile.name)
        assertEquals("character.yaml", subject.characterSettingsFile?.name)
        assertEquals(true, subject.verbose)
    }

    @Test fun shouldHaveValuesWhenUsingShortArgs() {
        // given
        val args: Array<String> = arrayOf(
                "-i", "image.png",
                "-s", "settings.yaml",
                "-c", "character.yaml",
                "-v"
        )

        val argParser = ArgParser(args)

        // when
        val subject = ArgumentsProgramSettingsBuilder(argParser).build()

        // then
        assertEquals("image.png", subject.imageOutputFile?.name)
        assertEquals("settings.yaml", subject.campaignSettingsFile.name)
        assertEquals("character.yaml", subject.characterSettingsFile?.name)
        assertEquals(true, subject.verbose)
    }

}
<file_sep>/src/main/kotlin/util/GraphicsText.kt
package util

import java.awt.Color
import java.awt.Font
import java.awt.Graphics2D

/**
 */

data class FontSettings(val g: Graphics2D,
                        val textColor: Color,
                        val fontSize: Int,
                        val italics: Boolean,
                        val bold: Boolean,
                        val fontName: String) {

    val style = Font.PLAIN or
            if (italics) Font.ITALIC else Font.PLAIN or
            if (bold) Font.BOLD else Font.PLAIN

    val font = Font(fontName, style, fontSize)

    val metrics = g.getFontMetrics(font)

    fun textW(text: String) : Int = text.split("\n").map{ line -> metrics.stringWidth(line) }.max() ?: 0

    fun textH(text: String) : Int = text.split("\n").map{ line -> lineH(line) }.sum()

    fun lineH(line: String) : Int = metrics.getStringBounds(line, g).height.toInt()

    fun pos(x: Int, y: Int) = PositionSettings(this, x, y, null)

    fun align(hGutter: Int = 0, vGutter: Int = 0, hAlign: HAlign = HAlign.CENTER, vAlign: VAlign= VAlign.MIDDLE) =
            AlignSettings(this, hGutter, vGutter, hAlign, vAlign)
}

data class PositionSettings(val fontSettings: FontSettings, val x0: Int, val y0: Int, val boundsSettings: BoundsSettings?) {
    fun write(text: String) = TextSettings(text, this, boundsSettings).write()

}

enum class HAlign {
    CENTER, LEFT, RIGHT
}

enum class VAlign {
    MIDDLE, TOP, BOTTOM
}

data class AlignSettings(val fontSettings: FontSettings,
                         val hGutter: Int, val vGutter: Int,
                         val hAlign: HAlign, val vAlign: VAlign) {
    val g = fontSettings.g

    fun bounds(w: Int, h: Int) = BoundsSettings(w, h, this)

    fun <T> List<List<T>>.transpose() = this[0].mapIndexed { index, _ ->
            this.map { row -> row[index] }
    }

    fun textDistributions(texts: List<List<String>>, maxWidth: Int): List<Int> =
        rowTextDistributions(
            texts.map { textRow -> textRow.zip(rowTextDistributions(textRow, maxWidth)) }
             .transpose()
             .map { col -> col.maxBy { (text, width) -> width } }
             .filterNotNull()
             .map { (text, width) -> text}, maxWidth
        )

    fun rowTextDistributions(texts: List<String>, maxWidth: Int): List<Int> {
        val textWidths = texts.map { fontSettings.textW(it) }
        val totalWidth = textWidths.sum()
        val usableWidth = maxWidth
        val hBoundWidths = textWidths.map { textWidth -> (1.0 * textWidth * usableWidth) / (1.0 * totalWidth) }.map { it.toInt() }
        return hBoundWidths
    }

    fun distributeTexts(texts: List<String>, x0: Int, y0: Int, hBoundWidths: List<Int>, height: Int) {
        var x = x0
        val textsToHBounds = hBoundWidths.zip(texts)
        textsToHBounds.forEach { (hBound, text) ->
            this.bounds(hBound, height).pos(x, y0).write(text)
            x += hBound
        }
    }
}

data class BoundsSettings(val w: Int, val h: Int, val alignSettings: AlignSettings) {

    fun x(x0: Int, text: String) = with(alignSettings) {
        when (hAlign) {
            HAlign.LEFT -> x0 + hGutter
            HAlign.CENTER -> x0 + w / 2 - fontSettings.textW(text) / 2
            HAlign.RIGHT -> x0 - hGutter - fontSettings.textW(text)
        }
    }

    fun y(y0: Int, text: String) = with(alignSettings) {
        when(vAlign){
            VAlign.TOP -> y0 + vGutter + fontSettings.lineH(text)
            VAlign.MIDDLE -> y0 + h/2 + fontSettings.textH(text)/2
            VAlign.BOTTOM -> y0 + h - vGutter
        }
    }

    fun pos(x: Int, y: Int) = PositionSettings(alignSettings.fontSettings, x, y, this)
}

data class TextSettings(val text: String,
                        val positionSettings: PositionSettings,
                        val boundsSetting: BoundsSettings?) {

    val x = if (boundsSetting == null) positionSettings.x0 else boundsSetting.x(positionSettings.x0, text)
    val y = if (boundsSetting == null) positionSettings.y0 else boundsSetting.y(positionSettings.y0, text)
    val g = positionSettings.fontSettings.g

    fun write() {
        val lines = text.split("\n").map { it.trim() }
        g.setFont(positionSettings.fontSettings.font)
        g.paint = positionSettings.fontSettings.textColor
        var lineY = y
        for (line in lines) {
            g.drawString(line, x, lineY)
            lineY += positionSettings.fontSettings.lineH(line)
        }
    }
}

fun Graphics2D.font(color: Color = Colors.BLACK,
                    fontSize: Int = 10,
                    italics: Boolean = false,
                    bold: Boolean = false,
                    fontName: String = "TimesRoman") = FontSettings(this, color, fontSize, italics, bold, fontName)

<file_sep>/status-tracker.py
class Character:
    def __init__(self, name, player, st, hp_adjust, ht, fp_adjust, iq, will_adjust, er):
        hp = st+hp_adjust
        fp = ht+fp_adjust
        will = iq+will_adjust
        self.name=name
        self.player=player
        self.st=st
        self.ht=ht
        self.fp=fp
        self.hp=hp
        self.iq=iq
        self.will=will
        self.fp_adjust=fp_adjust
        self.hp_adjust=hp_adjust
        self.will_adjust=will_adjust
        self.er=er

        self.limb = hp/2
        self.extremity = hp/3
        self.major = hp/2
        self.knockback = st - 2

        self.init_fp_ranges(fp)
        self.init_hp_ranges(hp)
        self.init_will_ranges(will)

    def init_will_ranges(self, will):
        self.will_ok = (will, (2*will)/3)
        self.will_mild =((2*will)/3, will/3)
        self.will_major = (will/3, 0)
        self.will_constant = (0, -will)
        self.will_ranges = [
            self.will_ok,
            self.will_mild,
            self.will_major,
            self.will_constant
        ]

    def init_fp_ranges(self, fp):
        self.fp_ok = (fp, fp/3)
        self.fp_reeling = (fp/3, 0)
        self.fp_ko = (0, (fp*-1)-1)
        self.fp_ranges = [
            self.fp_ok,
            self.fp_reeling,
            self.fp_ko
        ]

    def init_hp_ranges(self, hp):
        self.hp_ranges = []
        ok_hp = (hp, 3)
        reeling_hp = (3, 0)

        self.hp_ranges.append(ok_hp)
        self.hp_ranges.append(reeling_hp)

        for i in range(0, 5):
            top = -1*i*hp
            bottom = -1*(i+1)*hp
            self.hp_ranges.append((top, bottom))
        self.hp_ranges.append((-5*hp, -10*hp))

colormode(RGB, 255)

class Colors:
    WHITE = color(255, 255, 255)
    BLACK = color(0, 0, 0)
    RED = color(255, 0, 0)
    D_GREEN = color(0, 100, 0)
    L_PURPLE = color(147, 112, 219)
    PURPLE = color(102,  51, 153)
    INDIGO = color(75, 0, 130)
    GREEN = color(0, 128, 0)
    AMBER = color(184, 134, 11)
    GOLD = color(255, 215,0)
    YELLOW = color(255, 255, 0)
    ORANGE = color(255, 165, 0)
    CRIMSON = color(220, 20, 60)
    VD_BLUE = color(25,  25, 112)
    D_BLUE = color(00, 00, 128)
    M_BLUE = color(230, 230, 139)
    L_BLUE = color(00, 00, 205)
    VL_BLUE = color(65, 105, 225)
    VVL_BLUE = color(135, 206, 250)

MSG=lambda c, v: "- Roll vs HT-%s or KO each turn\n- At %s, Roll vs HT or Die" % (c, v)

HP_EFFECTS = [
       (Colors.GREEN, lambda c, v: "- OK"),
       (Colors.AMBER, lambda c, v: "- Halve BS, Move, and Dodge"),
       (Colors.GOLD, lambda c, v: "- Roll vs HT or KO each turn"),
       (Colors.YELLOW, MSG),
       (Colors.ORANGE, MSG),
       (Colors.CRIMSON, MSG),
       (Colors.RED, MSG),
       (Colors.BLACK, lambda c, v: "- At %s, Instant Death\n- At %s, Body destroyed" % (v, 2*v), Colors.WHITE)
       ]

FP_EFFECTS = [
    (Colors.L_PURPLE, lambda c, v: "+FP to 1/3: OK", Colors.WHITE),
    (Colors.PURPLE, lambda c, v: "1/3 to 1: Very Tired -\nHalve MS, BS, ST", Colors.WHITE),
    (Colors.INDIGO, lambda c, v: "0 to -FP: Verge of Collapse -\nRoll vs HT or KO", Colors.WHITE)
]

WILL_EFFECTS = [
    (Colors.VL_BLUE, lambda c, v: "+Will to 2/3:\n-0 / No Change / None", Colors.WHITE),
    (Colors.L_BLUE, lambda c, v: "2/3 to 1/3:\n-2 / One Level Harder / Mild", Colors.WHITE),
    (Colors.D_BLUE, lambda c, v: "1/3 to 1:\n-5 / Autofail / Major", Colors.WHITE),
    (Colors.VD_BLUE, lambda c, v: "0 to -Will:\n-7 / Compulsion / Constant", Colors.WHITE)
]

BORDER_H=10
BORDER_W=10
GUTTER=10
STAT_W=200
STAT_H=70
STAT_FONT_SIZE=20
TRACK_HEADER_FONT_SIZE=16
TEXT_FONT_SIZE=12
BUBBLE_FONT_SIZE=8
FONT="Arial"
FONT_BOLD="Arial Bold"

TRACKER_Y = BORDER_H + STAT_H + GUTTER
HEADER_H = 45
BUBBLE_SIZE = 18
BUBBLE_OFFSET = BUBBLE_SIZE+2
TRACKER_H = 9 * HEADER_H

HP_TRACKER_W = 2.5*STAT_W
P_WIDTH = 2*GUTTER + 2*BORDER_W + 2*STAT_W + HP_TRACKER_W
NAME_W = P_WIDTH - (STAT_W*2 + GUTTER*2 + BORDER_W*2)
P_HEIGHT = GUTTER + 2*BORDER_H + TRACKER_H + STAT_H
HP_X = BORDER_W
HP_Y = TRACKER_Y
FP_X = HP_TRACKER_W + GUTTER + BORDER_W
FP_Y = TRACKER_Y
INFO_X = P_WIDTH-STAT_W-BORDER_W
WILL_X = FP_X
WILL_Y = TRACKER_Y + HEADER_H * 4
FP_WIDTH = WIDTH - HP_TRACKER_W - (3 * GUTTER)

size(P_WIDTH, P_HEIGHT)

def draw_hp(char):
    fontsize(STAT_FONT_SIZE)
    stroke(Colors.D_GREEN)
    fill(Colors.D_GREEN)
    rect_x = BORDER_W
    rect_y = BORDER_H
    rect(rect_x, rect_y, STAT_W, STAT_H)
    stroke(Colors.INDIGO)
    txt = "ST:\t%s\nHP:\t%s" % (char.st, char.hp)
    txt_w, txt_h = textmetrics(txt)
    fill(Colors.WHITE)
    font(FONT_BOLD)
    text(txt, rect_x+GUTTER, rect_y+txt_h/2)

def draw_energy_reserve(char):
    if (char.er > 0):
        x = FP_X
        y = TRACKER_Y + 8 * HEADER_H
        w = STAT_W
        h = HEADER_H
        fill(Colors.M_BLUE)
        rect(x, y, w, h)
        txt = "Energy Reserve"
        txt_w, txt_h = textmetrics(txt)
        fontsize(TRACK_HEADER_FONT_SIZE)
        align(CENTER)
        fill(Colors.BLACK)
        text(txt, x, y+txt_h+4, w)
        b_y = y + HEADER_H / 2
        b_x = x + STAT_W/2 - (BUBBLE_OFFSET * char.er)/2
        for i in range(char.er, 0, -1):
            draw_bubble(i, b_x, b_y)
            b_x = b_x + BUBBLE_OFFSET

def draw_fp(char):
    fontsize(STAT_FONT_SIZE)
    stroke(Colors.INDIGO)
    fill(Colors.INDIGO)
    rect_x = FP_X
    rect_y = BORDER_H
    rect(rect_x, rect_y, STAT_W, STAT_H)
    txt = "HT:\t%s\nFP:\t%s" % (char.ht, char.fp)
    txt_w, txt_h = textmetrics(txt)
    fill(Colors.WHITE)
    font(FONT_BOLD)
    text(txt, rect_x+GUTTER, rect_y+txt_h/2)

def draw_name(char):
    fontsize(STAT_FONT_SIZE)
    stroke(Colors.BLACK)
    fill(Colors.WHITE)
    rect_x = STAT_W+BORDER_W+GUTTER
    rect_y = BORDER_H
    w = NAME_W
    h = STAT_H
    rect(rect_x, rect_y, w, h)
    fill(Colors.BLACK)
    txt = "Character:\t%s\nPlayer:\t\t%s" % (char.name, char.player)
    txt_w, txt_h = textmetrics(txt)
    font(FONT_BOLD)
    text(txt, rect_x+GUTTER, rect_y+txt_h/2)

def fp_col(index, x_0, y_0, txt, hue, txt_hue, range, offset_range):
    c_x_delta = (STAT_W / 3)
    c_w = c_x_delta + 1
    c_x = x_0 + c_x_delta * index

    # header
    fill(hue)
    stroke(Colors.BLACK)
    rect(c_x, y_0, c_w, HEADER_H)

    # column
    fill(hue)
    stroke(Colors.BLACK)
    rect(c_x, y_0+HEADER_H, c_w, TRACKER_H - 2 * HEADER_H)

    # header text
    font(FONT_BOLD)
    fontsize(TEXT_FONT_SIZE)
    txt_w, txt_h = textmetrics(txt)
    txt_y = y_0 + 0.5 * HEADER_H - txt_h/5
    fill(txt_hue)
    align(CENTER)
    text(txt, c_x, txt_y, c_w)

    # bubbles
    stroke(Colors.BLACK)
    fill(Colors.WHITE)
    b_x = c_x + c_x_delta/2 - BUBBLE_SIZE/2
    b_y_0 = y_0 + HEADER_H + BUBBLE_SIZE/2
    b_y = b_y_0
    for i in offset_range:
        b_y = b_y + (BUBBLE_OFFSET)

    for i in range:
        draw_bubble(i, b_x, b_y)
        b_y = b_y + (BUBBLE_SIZE+2)

def draw_bubble(i, b_x, b_y, hue = Colors.WHITE, txt_hue = Colors.BLACK):
    fontsize(BUBBLE_FONT_SIZE)
    b_txt_w, b_txt_h = textmetrics(i)
    stroke(Colors.BLACK)
    fill(hue)
    oval(b_x, b_y, BUBBLE_SIZE, BUBBLE_SIZE)
    fill(txt_hue)
    align(CENTER)
    font(FONT)
    text(i, b_x-BUBBLE_SIZE/2, b_y+3*BUBBLE_SIZE/4, 2*BUBBLE_SIZE)

def draw_fp_track_vertical(char):
    rect_x = FP_X
    rect_y = TRACKER_Y
    w = STAT_W
    h = TRACKER_H
    rect(rect_x, rect_y, w, h)
    # header
    fill(Colors.INDIGO)
    rect(rect_x, rect_y, w, HEADER_H)
    fontsize(TRACK_HEADER_FONT_SIZE)
    draw_track_header("Fatigue (FP) Tracking", rect_x, rect_y, w, Colors.INDIGO)

    col_y = rect_y + HEADER_H
    # columns
    fp_col(0, rect_x, col_y, "+FP to\n1/3", Colors.L_PURPLE, Colors.WHITE, char.fp_ok, range(0))
    fp_col(1, rect_x, col_y, "1/3 to 1", Colors.PURPLE, Colors.WHITE, char.fp_reeling, char.fp_ok)
    fp_col(2, rect_x, col_y, "0 to -FP", Colors.INDIGO, Colors.WHITE, char.fp_ko, range(0))

    draw_energy_reserve(char)

def draw_fp_track_horizontal(char):
    draw_track(FP_X, FP_Y, FP_WIDTH , 0.5, FP_EFFECTS, char.fp_ranges, "Fatigue (FP) Tracking", "Effects", False, Colors.INDIGO, "unconscious.png")

def draw_will_track_horizontal(char):
    draw_track(FP_X, WILL_Y, FP_WIDTH, 0.5, WILL_EFFECTS, char.will_ranges, "Will Tracking", "Will / SC / Interference", False, Colors.VD_BLUE, "supernatural.png")

def draw_hp_track(char):
    draw_track(HP_X, HP_Y, HP_TRACKER_W, 0.6, HP_EFFECTS, char.hp_ranges, "Hit Point (HP) Tracking", "Effects", True, Colors.D_GREEN, "skull.jpg")

def draw_track(start_x, start_y, width, col_1_ratio, effects_list, stat_ranges, header, effects, offset_second_row, header_color, img):
    x = start_x
    y = start_y
    w = width
    h = TRACKER_H
    col_1_w = w * col_1_ratio
    col_2_w = width - col_1_w
    col_1_x = x
    col_2_x = col_1_x + col_1_w
    rect(x, y, w, h)

    # headers
    draw_track_header(header, col_1_x, y, col_1_w, header_color)
    draw_track_header(effects, col_2_x, y, col_2_w, header_color)

    # rows
    fontsize(TEXT_FONT_SIZE)
    for i, row in enumerate(effects_list):
        y_c = y + HEADER_H * (i + 1)
        stat_range = stat_ranges[i]
        bottom = stat_range[0]
        top = stat_range[1]

        fill(row[0])
        # col 1
        rect(col_1_x, y_c, col_1_w, HEADER_H)

        # col 2
        rect(col_2_x, y_c, col_2_w, HEADER_H)
        try: txt_hue = effects_list[i][2]
        except: txt_hue = Colors.BLACK
        txt_func=row[1]
        txt = txt_func(i-2, bottom)
        txt_w, txt_h = textmetrics(txt)
        fill(txt_hue)
        fontsize(TEXT_FONT_SIZE)
        font(FONT)
        text(txt, col_2_x+GUTTER, y_c + HEADER_H/2)

        b_y = y_c + HEADER_H / 2 - BUBBLE_SIZE/2
        # bubbles
        if (i == 1 and offset_second_row):
            offset = b_x + BUBBLE_OFFSET
        else:
            offset = x
        if (i < len(effects_list) - 1):
            for b, value in enumerate(range(stat_range[0], stat_range[1], -1)):
                b_x = offset + BUBBLE_OFFSET * b
                if (i > 2 and b == 0):
                    draw_bubble(value, b_x, b_y, Colors.BLACK, Colors.WHITE)
                else:
                    draw_bubble(value, b_x, b_y, Colors.WHITE, Colors.BLACK)
        else:
            image(img, offset + BUBBLE_SIZE/2, y_c+BUBBLE_SIZE/3, HEADER_H * 0.75, HEADER_H * 0.75)

def draw_track_header(txt, x0, y0, w, hue):
    fill(hue)
    rect(x0, y0, w, HEADER_H)
    fontsize(TRACK_HEADER_FONT_SIZE)
    align(CENTER)
    head_w, head_h = textmetrics(txt)
    font(FONT_BOLD)
    fill(Colors.WHITE)
    text(txt, x0, y0+HEADER_H-head_h, w)

def draw_info_msg(i, x0, y0, col_1_txt, col_2_txt, hue=Colors.WHITE, txt_hue=Colors.BLACK):
    font(FONT)
    fontsize(TEXT_FONT_SIZE)
    y = y0 + HEADER_H * i
    fill(hue)
    rect(x0, y, STAT_W, HEADER_H)

    # col 2
    fill(txt_hue)
    col_1_w=STAT_W/3
    col_1_txt_x = x0 + GUTTER
    col_1_txt_y = valign_middle(y, HEADER_H, col_1_txt)
    font(FONT)
    text(col_1_txt, col_1_txt_x, col_1_txt_y)

    # col 2
    col_2_x = x0 + col_1_w
    fill(hue)
    rect(col_2_x, y, STAT_W-col_1_w, HEADER_H)
    fill(txt_hue)
    col_2_txt_y = valign_middle(y, HEADER_H, col_2_txt)
    font(FONT)
    text(col_2_txt, col_2_x+GUTTER, col_2_txt_y)

def valign_middle(y0, h, txt):
    txt_w, txt_h = textmetrics(txt)
    return y0 + h/2# + txt_h/4

def draw_info(char):
    fill(Colors.WHITE)
    stroke(Colors.BLACK)
    x = INFO_X
    y = TRACKER_Y
    w = STAT_W
    h = TRACKER_H
    rect(x, y, w, h)

    draw_track_header("Fatigue Effects", x, y, w, Colors.INDIGO)
    draw_info_msg(1, x, y, "+FP to\n1/3", "OK", Colors.L_PURPLE, Colors.WHITE)
    draw_info_msg(2, x, y, "1/3 to 1", "Very Tired:\nHalve MS, BS, ST", Colors.PURPLE, Colors.WHITE)
    draw_info_msg(3, x, y, "0 to -FP", "Verge of Collapse:\nRoll vs HT or KO", Colors.INDIGO, Colors.WHITE)

    draw_track_header("Injury & Wounds", x, y+HEADER_H*4, w, Colors.D_GREEN)
    draw_info_msg(5, x, y, "%s pts" % char.limb, "Verge of Collapse:\nRoll vs HT or KO")
    draw_info_msg(6, x, y, "%s pts" % char.extremity, "Verge of Collapse:\nRoll vs HT or KO")
    draw_info_msg(7, x, y, "%s pts" % char.major, "Verge of Collapse:\nRoll vs HT or KO")
    draw_info_msg(8, x, y, "%s pts" % char.knockback, "Verge of Collapse:\nRoll vs HT or KO")

def draw_layout(char):
    draw_hp(char)
    draw_name(char)
    draw_fp(char)
    draw_hp_track(char)
    draw_fp_track_horizontal(char)
    draw_will_track_horizontal(char)
    # draw_info(char)

# character = Character("RC Cleveland", "<NAME>", 10, 1, 11, 1, 14, 1, 0)
# character = Character("<NAME>", "<NAME>", 10, 0, 10, 0, 13, -1, 0)
# character = Character("<NAME>", "<NAME>", 10, 1, 12, -1, 14, 0, 0)
character = Character("<NAME>", "<NAME>", 12, 0, 12, 0, 15, -5, 0)

draw_layout(character)

<file_sep>/src/test/kotlin/settings/ArgumentsCharacterSettingsBuilderTest.kt
package settings

import com.xenomachina.argparser.ArgParser
import org.junit.Assert.*
import org.junit.Test
import settings.ArgumentsCharacterSettingsBuilder

/**
 */
class ArgumentsCharacterSettingsBuilderTest {
    @Test fun shouldHaveDefaultValuesWhenArrayIsEmpty() {
        // given
        val args: Array<String> = arrayOf()
        val argParser = ArgParser(args)

        // when
        val subject = ArgumentsCharacterSettingsBuilder(argParser).build()

        // then
        assertEquals("Unknown Character", subject.characterName)
        assertEquals("Unknown Player", subject.playerName)
        assertEquals(10, subject.st)
        assertEquals(10, subject.iq)
        assertEquals(10, subject.ht)

        assertEquals(10, subject.hp)
        assertEquals(10, subject.fp)
        assertEquals(10, subject.wp)
        assertEquals(10, subject.er)

        assertEquals(10, subject.currentHP)
        assertEquals(10, subject.currentFP)
        assertEquals(10, subject.currentWP)
        assertEquals(10, subject.currentER)
    }

    @Test fun shouldHaveValuesWhenUsingLongArgs() {
        // given
        val args: Array<String> = arrayOf(
                "--name", "KNOWN Character",
                "--player", "KNOWN Player",

                "--st", "11",
                "--iq", "12",
                "--ht", "13",

                "--hp", "14",
                "--fp", "15",
                "--wp", "16",
                "--er", "17",

                "--currentHP", "18",
                "--currentFP", "19",
                "--currentWP", "20",
                "--currentER", "21"
        )

        val argParser = ArgParser(args)

        // when
        val subject = ArgumentsCharacterSettingsBuilder(argParser).build()

        // then
        assertEquals("KNOWN Character", subject.characterName)
        assertEquals("KNOWN Player", subject.playerName)

        assertEquals(11, subject.st)
        assertEquals(12, subject.iq)
        assertEquals(13, subject.ht)

        assertEquals(14, subject.hp)
        assertEquals(15, subject.fp)
        assertEquals(16, subject.wp)
        assertEquals(17, subject.er)

        assertEquals(18, subject.currentHP)
        assertEquals(19, subject.currentFP)
        assertEquals(20, subject.currentWP)
        assertEquals(21, subject.currentER)
    }
}
<file_sep>/src/test/resources/settings.yaml
showWP: true
showER: true
<file_sep>/settings.gradle
rootProject.name = 'gurps-status-tracker'

<file_sep>/src/test/kotlin/settings/FileCharacterSettingsBuilderTest.kt
package settings

import org.junit.Assert.*
import org.junit.Test
import java.io.File
import java.io.InputStream

/**
 */
class FileCharacterSettingsBuilderTest {
    @Test fun shouldHaveDefaultValuesWhenFileNull() {
        // given
        val file: File? = null

        // when
        val subject = FileCharacterSettingsBuilder().build(file)

        // then
        assertEquals("Unknown Character", subject.characterName)
        assertEquals("Unknown Player", subject.playerName)
        assertEquals(10, subject.st)
        assertEquals(10, subject.iq)
        assertEquals(10, subject.ht)

        assertEquals(10, subject.hp)
        assertEquals(10, subject.fp)
        assertEquals(10, subject.wp)
        assertEquals(10, subject.er)

        assertEquals(10, subject.currentHP)
        assertEquals(10, subject.currentFP)
        assertEquals(10, subject.currentWP)
        assertEquals(10, subject.currentER)
    }

    @Test fun shouldHaveDefaultReadValuesWhenFileIsAvailable() {
        // given
        val inputStream: InputStream = this::class.java.getResourceAsStream("/character.yaml")

        // when
        val subject = FileCharacterSettingsBuilder().build(inputStream)

        // then
        assertEquals("KNOWN Character", subject.characterName)
        assertEquals("KNOWN Player", subject.playerName)

        assertEquals(11, subject.st)
        assertEquals(12, subject.iq)
        assertEquals(13, subject.ht)

        assertEquals(14, subject.hp)
        assertEquals(15, subject.fp)
        assertEquals(16, subject.wp)
        assertEquals(17, subject.er)

        assertEquals(18, subject.currentHP)
        assertEquals(19, subject.currentFP)
        assertEquals(20, subject.currentWP)
        assertEquals(21, subject.currentER)
    }
}<file_sep>/src/main/kotlin/ImageVisualization.kt
import settings.ProgramSettings
import util.Colors
import util.*
import java.awt.Color
import java.awt.Graphics2D
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO

/**
 */
val HP_HEADER_COLOR = Colors.DARK_GREEN
val WP_HEADER_COLOR = Colors.VD_BLUE
val FP_HEADER_COLOR = Colors.INDIGO

val HP_COLORS = listOf(
        Colors.GREEN,
        Colors.AMBER,
        Colors.GOLD,
        Colors.YELLOW,
        Colors.ORANGE,
        Colors.RED,
        Colors.CRIMSON,
        Colors.BLACK,
        Colors.BLACK)

val FP_COLORS = listOf(
    Colors.L_PURPLE,
    Colors.PURPLE,
    Colors.INDIGO,
    Colors.BLACK
)

val WP_COLORS = listOf(
        Colors.VL_BLUE,
        Colors.L_BLUE,
        Colors.D_BLUE,
        Colors.BLACK
)

fun String.camelCase() = this.capitalize().replace("[^\\w]*".toRegex(), "")

class ImageVisualization(val statusTracker : StatusTracker) {

    companion object {
        val DPI = 300

        fun Double.inches() = (this * DPI).toInt()

        val PAGE_WIDTH_PX = 8.5.inches()
        val PAGE_HEIGHT_PX = 11.0.inches()

        val MARGIN_PX = 1.0.inches()
        val GUTTER_PX = 0.125.inches()

        val MAX_WIDTH_PX = PAGE_WIDTH_PX - ( 2 * MARGIN_PX )
        val MAX_HEIGHT_PX = PAGE_HEIGHT_PX - ( 2 * MARGIN_PX )

        val ROW_WIDTH_PX = MAX_WIDTH_PX - (2 * GUTTER_PX)
        val ROW_HEIGHT_PX = 0.30.inches()

        val FONT_PX = 0.1.inches()
        val TEXT_V_GUTTER = (0.1 * FONT_PX).toInt()
    }

    fun exportImage(programSettings: ProgramSettings) {
        val bi = construct(statusTracker)
        writeImage(bi, programSettings)
    }

    fun construct(statusTracker: StatusTracker) : BufferedImage {
        val bi = BufferedImage(MAX_WIDTH_PX, MAX_HEIGHT_PX, BufferedImage.TYPE_INT_RGB)
        val g = bi.createGraphics()
        var startY = GUTTER_PX
        g.paint = Color.white
        g.fillRect(0, 0, MAX_WIDTH_PX, MAX_HEIGHT_PX)
        startY = drawHeaders(startY, g, statusTracker) + GUTTER_PX
        startY = drawTrack(startY, g, statusTracker.hpTrack, HP_HEADER_COLOR, HP_COLORS) + GUTTER_PX
        startY = drawTrack(startY, g, statusTracker.fpTrack, FP_HEADER_COLOR, FP_COLORS) + GUTTER_PX
        startY = drawTrack(startY, g, statusTracker.wpTrack, WP_HEADER_COLOR, WP_COLORS) + GUTTER_PX
        return bi
    }

    fun drawNamesBar(startY: Int, g: Graphics2D, statusTracker: StatusTracker): Int {
        var y = startY
        var h = ROW_HEIGHT_PX
        var w = ROW_WIDTH_PX
        var textW = w/2
        var textH = h
        var text1X = GUTTER_PX
        var text2X = GUTTER_PX + w / 2

        g.r(GUTTER_PX, startY, ROW_WIDTH_PX, ROW_HEIGHT_PX)
        val headerStyle = g
                .font(fontSize = FONT_PX, bold = true)
                .align(hGutter = GUTTER_PX, vGutter = 0, hAlign = HAlign.CENTER, vAlign = VAlign.MIDDLE)
        val textBounds = headerStyle.bounds(textW, textH)

        textBounds.pos(text1X, y).write("Character Name: ${statusTracker.characterName}")
        textBounds.pos(text2X, y).write("Player Name: ${statusTracker.characterSettings.playerName}")

        y += ROW_HEIGHT_PX

        return y
    }

    fun drawStatsBar(startY: Int, g: Graphics2D, statusTracker: StatusTracker): Int {
        var y = startY

        return y
    }

    fun drawHeaders(startY: Int, g: Graphics2D, statusTracker: StatusTracker): Int {
        var y = startY
        y = drawNamesBar(y, g, statusTracker)
        y = drawStatsBar(y, g, statusTracker)
        return y
    }

    fun drawTrack(startY: Int, g: Graphics2D, track: Track, headerColor: Color, colors: List<Color>): Int {
        val x = GUTTER_PX
        var y = startY
        val w = ROW_WIDTH_PX
        val h = ROW_HEIGHT_PX
        val fontSize = FONT_PX
        val col0Width = (0.3 * w).toInt()
        val remainingColWidth = w - col0Width
        g.r(x, y, w, h, headerColor, Colors.BLACK)

        val headerStyle = g.font(Colors.WHITE, fontSize = fontSize, bold = true)
                .align(hGutter = GUTTER_PX, vGutter = 0, hAlign = HAlign.LEFT, vAlign = VAlign.MIDDLE)

        val textStyle = g.font(Colors.WHITE, fontSize = fontSize, bold = false)
                .align(hGutter = GUTTER_PX, vGutter = TEXT_V_GUTTER, hAlign = HAlign.LEFT, vAlign = VAlign.TOP)

        val info = listOf(track.headers.drop(1)) + track.rows.map { row -> row.info }
        val headerHBounds = listOf(col0Width) + headerStyle.textDistributions(info, remainingColWidth)

        drawBorders(g, x, y, headerHBounds, h, headerColor, Color.BLACK)
        track.headers.forEachIndexed { index, header ->
            headerStyle.distributeTexts(track.headers, x, y, headerHBounds, h)
        }
        y += h
        track.rows.forEachIndexed { index, trackRow ->
            val trackRowInfo = listOf("") + trackRow.info
            val fillColor = colors.getOrElse(index) { i -> Colors.BLACK }
            drawBorders(g, x, y, headerHBounds, h, fillColor, Color.BLACK)
            textStyle.distributeTexts(trackRowInfo, x, y, headerHBounds, h)
            y += h
        }
        return y
    }

    fun drawBubbles(g: Graphics2D, startX: Int, startY: Int, range: IntProgression) {
        val diameter = 10
        range.forEach {
            val y = startY + (diameter * 2)
            g.background = Colors.WHITE
            g.stroke = Colors.B
            g.fillOval(startX, y, diameter, diameter)
        }

    }

    fun drawBorders(g: Graphics2D, x0: Int, y0: Int, headerHBounds: List<Int>, h: Int, fillColor: Color, borderColor: Color) {
        var boundX = x0
        for (hBound in headerHBounds) {
            g.r(boundX, y0, hBound, h, fillColor, borderColor)
            boundX += hBound
        }
    }

    fun writeImage(bi: BufferedImage, programSettings: ProgramSettings) {
        var imgFile = getValidFile(programSettings)
        ImageIO.write(bi, "png", imgFile)
    }

    fun getValidFile(programSettings: ProgramSettings): File {
        val imgFile = programSettings.imageOutputFile ?: File.createTempFile(statusTracker.characterName.camelCase(), ".png")
        if (imgFile.exists()) {
            if (!imgFile.canWrite()) throw IllegalArgumentException("Image Output File '${imgFile.canonicalPath}' is not writable")
        }
        println("Outputting image to '${imgFile.canonicalPath}'")
        return imgFile
    }
}<file_sep>/src/main/kotlin/settings/Arguments.kt
package settings

import com.xenomachina.argparser.ArgParser
import com.xenomachina.argparser.default
import java.io.File

/**
 */

class ArgumentsProgramSettingsBuilder(val parser: ArgParser) {
    companion object {
        val DEFAULT_SETTINGS_PATH = "~/.gurps/settings.yaml"
    }
    val verbose by parser.flagging("-v", "--verbose", help = "verbose mode")

    val imageOutputFile by parser.storing("-i", "--image", help = "Image Output Path")
        { File(this) }.default(File("/tmp/character.png"))

    val campaignSettingsFile by parser.storing("-s", "--settings", help = "Campaign settings file ")
        { File(this) }.default(File(DEFAULT_SETTINGS_PATH))

    val characterSettingsFile by parser.storing("-c", "--character", help = "Character file")
        { File(this) }.default(null)

    fun build() = ProgramSettings(verbose, imageOutputFile, campaignSettingsFile, characterSettingsFile)
}

class ArgumentsCampaignSettingsBuilder(val parser: ArgParser, val defaults: CampaignSettings = CampaignSettings.DEFAULT) {
    // flags
    val showER by parser.flagging("--showER", help = "").default(defaults.showER)
    val showWP by parser.flagging("--showWP", help = "").default(defaults.showWP)

    fun build() = CampaignSettings(showER, showWP)
}

class ArgumentsCharacterSettingsBuilder(val parser: ArgParser, val defaults: CharacterSettings = CharacterSettings.DEFAULT) {
    // meta data
    val characterName by parser.storing("--name", help = "Character name. overrides values in file").default(defaults.characterName)
    val playerName by parser.storing("--player", help = "Player name. overrides values in file").default(defaults.playerName)

    // primary stats
    val ht by parser.storing("--ht", help = "Character's Health (HT). overrides values in file"){ toInt()}.default(defaults.ht)
    val st by parser.storing("--st", help = "Character's Strength (ST). overrides values in file"){ toInt()}.default(defaults.st)
    val iq by parser.storing("--iq", help = "Character's Intelligence (IQ). overrides values in file"){ toInt()}.default(defaults.iq)

    // drived stats
    val hp by parser.storing("--hp", help = "Character's Hit Points (HP). overrides values in file"){ toInt()}.default(defaults.hp)
    val fp by parser.storing("--fp", help = "Character's Fatigue Points (FP). overrides values in file"){ toInt()}.default(defaults.fp)
    val wp by parser.storing("--wp", help = "Character's Will Points (WP). overrides values in file"){ toInt() }.default(defaults.wp)
    val er by parser.storing("--er", help = "Character's Energy Reserve (ER). overrides values in file"){ toInt() }.default(defaults.er)

    // tracked stats
    val currentHP by parser.storing("--currentHP", help = "Character's Current Hit Points (HP). overrides values in file")
    { toInt() }
            .default(defaults.currentHP)
    val currentFP by parser.storing("--currentFP", help = "Character's Current Fatigue Points (FP). overrides values in file")
    { toInt() }
            .default(defaults.currentFP)
    val currentWP by parser.storing("--currentWP", help = "Character's Current Will Points (WP). overrides values in file")
    { toInt() }
            .default(defaults.currentWP)
    val currentER by parser.storing("--currentER", help = "Character's Current Energy Reserve (ER). overrides values in file")
    { toInt() }
            .default(defaults.currentER)

    fun build() = CharacterSettings(
            characterName,
            playerName,

            // primary stats
            ht,
            st,
            iq,

            // drived stats
            hp,
            fp,
            wp,
            er,

            // tracked stats
            currentHP,
            currentFP,
            currentWP,
            currentER)

}<file_sep>/src/test/kotlin/settings/ArgumentsCampaignSettingsBuilderTest.kt
package settings

import com.xenomachina.argparser.ArgParser
import org.junit.Assert.*
import org.junit.Test
import settings.ArgumentsCampaignSettingsBuilder

/**
 */
class ArgumentsCampaignSettingsBuilderTest {
    @Test fun shouldHaveDefaultValuesWhenArrayIsEmpty() {
        // given
        val args: Array<String> = arrayOf()
        val argParser = ArgParser(args)

        // when
        val subject = ArgumentsCampaignSettingsBuilder(argParser).build()

        // then
        assertEquals(false, subject.showER)
        assertEquals(false, subject.showWP)
    }

    @Test fun shouldHaveValuesWhenUsingLongArgs() {
        // given
        val args: Array<String> = arrayOf(
                "--showER", "--showWP"
        )

        val argParser = ArgParser(args)

        // when
        val subject = ArgumentsCampaignSettingsBuilder(argParser).build()

        // then
        assertEquals(true, subject.showER)
        assertEquals(true, subject.showWP)
    }
}
<file_sep>/src/main/kotlin/Track.kt
import settings.CampaignSettings
import settings.CharacterSettings

/**
 */

data class TrackRow(val info: List<String>, val range: IntProgression? = null) {
}

data class Track(val headers: List<String>, val rows: List<TrackRow>) {
}

data class StatusTracker(val campaignSettings: CampaignSettings, val characterSettings: CharacterSettings) {
    val characterName = characterSettings.characterName
    val hp = characterSettings.hp
    val wp = characterSettings.wp
    val fp = characterSettings.fp
    val ht = characterSettings.ht

    val hpTrack = hpTrack()
    val fpTrack = fpTrack()
    val wpTrack = wpTrack()

    fun koOrDieDesc(mod: Int) = "-${mod}*HP to -${mod+1}*HP"
    fun koEffect(mod: Int, ht: Int) = "Each Turn, roll vs. HT-${mod} (${ht-mod}) KO"
    fun dieEffect(mod: Int, ht: Int, hp: Int) = "At -${mod*hp} HP, roll vs. HT (${ht}) or Die"
    fun koOrDieEffect(mod: Int, ht: Int, hp: Int) = koEffect(mod, ht) + "\n" + dieEffect(mod, ht, hp)
    fun koOrDieRow (mod: Int, ht: Int, hp: Int) = TrackRow(listOf(koOrDieDesc(mod), "Dying", koOrDieEffect(mod, ht, hp)), -mod*hp downTo -(mod+1)*hp)

    fun hpTrack() : Track {
        return Track(
                listOf("Hit Point (HP) Tracking", "Range", "Description", "Effects"),
                listOf(
                        TrackRow(listOf("+HP to +1/3 HP", "OK", "None"), hp downTo (wp/3)),
                        TrackRow(listOf("+1/3 HP to 1", "Reeling", "1/2 BS, Move, Dodge"), (hp/3) downTo 1),
                        TrackRow(listOf("+0 to -HP", "Verge of Collapse", koEffect(0, hp)), 0 downTo (-1*hp)),
                        koOrDieRow(1, hp, ht),
                        koOrDieRow(2, hp, ht),
                        koOrDieRow(3, hp, ht),
                        koOrDieRow(4, hp, ht),
                        TrackRow(listOf("-5*HP\n-10*HP", "Instant Death\nTotal Destruction", "At -${5*hp}, Instant Death\nAt -${10*hp} Body Destroyed"))
                )
        )
    }

    fun fpTrack() : Track {
        return Track(
                listOf("Fatigue Point (FP) Tracking", "Range", "Description", "Effects"),
                listOf(
                        TrackRow(listOf("+FP to +1/3 FP", "OK", "None"), fp downTo fp/3),
                        TrackRow(listOf("+1/3 FP to 1", "Very Tired", "Halve MS, BS, ST"), fp/3 downTo 1),
                        TrackRow(listOf("0 to -FP", "Verge of Collapse", "Each Turn, roll vs HT or KO"), 0 downTo -fp),
                        TrackRow(listOf("-FP", "Unconscious", "At -${fp}, Unconscious"))

        ))

    }

    fun wpTrack() : Track {
        return Track(
                listOf("Will Point (HP) Tracking", "Range", "Will Mod", "Self Control", "Supernatural Interference"),
                listOf(
                        TrackRow(listOf("+Will to 2/3 Will", "-0", "No Effect", "None"), wp downTo ((2*wp)/3)),
                        TrackRow(listOf("+2/3 Will to 1/3 Will", "-2", "One Level Harder", "Mild"), ((2*wp)/3) downTo (wp/3)),
                        TrackRow(listOf("+1/3 Will to 1", "-5", "Auto-fail", "Major"), ((wp)/3) downTo 1),
                        TrackRow(listOf("+0 to -Will", "-7", "Compulsion", "Constant"), 0 downTo (-1*wp))
                )
        )

    }
}<file_sep>/src/main/kotlin/util/Graphics.kt
package util

import java.awt.Color
import java.awt.Graphics2D

/**
 */
class Colors {
    companion object {
        val AMBER = Color(184, 134, 11)
        val GOLD = Color(255, 215,0)
        val CRIMSON = Color(220, 20, 6)
        val GREEN = Color(0, 128, 0)
        val DARK_GREEN = Color(0, 100, 0)
        val YELLOW = Color.YELLOW
        val ORANGE = Color.ORANGE
        val RED = Color.RED
        val BLACK = Color.BLACK
        val WHITE = Color.WHITE
        val L_PURPLE = Color(147, 112, 219)
        val PURPLE = Color(102,  51, 153)
        val INDIGO = Color(75, 0, 130)
        val VD_BLUE = Color(25,  25, 112)
        val D_BLUE = Color(0, 0, 128)
        val M_BLUE = Color(230, 230, 139)
        val L_BLUE = Color(0, 0, 205)
        val VL_BLUE = Color(65, 105, 225)
        val VVL_BLUE = Color(135, 206, 250)
    }
}

fun Graphics2D.r(x: Int, y: Int, w: Int, h: Int, fillColor: Color = Colors.WHITE, borderColor: Color = Colors.BLACK) {
    this.paint = fillColor
    this.fillRect(x, y, w, h)
    this.paint = borderColor
    this.drawRect(x, y, w, h)
}

languages: Kotlin Gradle YAML Python

repo_language: Kotlin

repo_name: dannwebster/gurps-status-tracker


text:
<file_sep># Example project to get started with puppeteer

To get started, clone this repository or download the code, then run

```
npm install
```

in the project directory.

To run the script, run

```
node index.js
```

in the project directory.

This should create two files, `screenshot.png` and `headlines.txt` in the project directory, and output the text of all headlines on the page to the console.

See comments in index.js for further explanation.
<file_sep>
// ATTENTION: this script uses ES6 module syntax in order
// to be able to use the "await" keyword on the top level of the script.
// (This is configured by setting "type": "module" in the package.json)
// This means you are not able to use require(...) to import modules,
// but have to use the new ES6 syntax "import ... from ...".

// For an explanation see
// https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1

import puppeteer from "puppeteer";
import { promises as fs } from "fs";

const browser = await puppeteer.launch({
  defaultViewport: { // browser window size
    width: 1024,
    height: 6000
  },
  devtools: false   // set to true for debugging *on the page*
});

const page = await browser.newPage();

await page.goto('https://cartography.tuwien.ac.at/');

// wait for a specific element to be created on the page,
// in case some content is rendered by JavaScript
await page.waitForSelector('#masthead');

// Saving a screenshot is really easy - note the large window size
// we have defined above in order to capture the whole page
await page.screenshot({ path: 'screenshot.png' });

// The following function will be run *on the page in the browser*
// Attention: you cannot use any functions or data defined elsewhere
// in this script inside this function, since the function is transferred
// to the browser and executed there and not in the context of the current script!
// Any data you need has to be passed in by arguments, and passed out by return value.

function getAllText(selector) {
  // Collect the text content of all elements identified by selector

  // uncomment next line and set "debugger: true" in puppeteer.launch() above
  // to launch debugger in the browser! (Useful to try things out and investigate page structure)

  // debugger;

  let elements = document.querySelectorAll(selector);
  let result = [];
  for (let el of elements) {
    result.push(el.textContent);
  }
  return result;
}

// Run the function defined above above on the page
// (function is followed by the arguments that get passed to the function)
let allHeadlines = await page.evaluate(getAllText, "h1,h2,h3,h4,h5,h6" );

console.log();
console.log("Today's Headlines:");
console.log("==================");
console.log(allHeadlines.join("\n"));
console.log();

await fs.writeFile("headlines.txt", allHeadlines.join("\n"));
console.log("Screenshot saved to 'screenshot.png', headlines saved to 'headlines.txt'.");
console.log();

await browser.close();

branch_name: refs/heads/main

languages: Markdown JavaScript

repo_language: Markdown

repo_name: floledermann/puppeteer-example