<p>We continue with people, this time focusing on opinions. This episode has somewhat more code than previous one, so following along with the shownotes might be a good idea. I’m trying to minimize amount of code I read out aloud.</p>
<h2 id="intro">Intro</h2>
<p>One person’s opinion of another is expressed as <code>OpinionScore</code> that ranges from -100 to 100.</p>
<p>Computing the score is based on intelligence player has available to them. Internally we have <code>ReportResult</code> that tracks score, reasons for the score and confidence level about the results. It’s defined as:</p>
<pre><code>data ReportResult =
    FeelingLevel OpinionScore
    | ReasonsLevel OpinionScore [OpinionReason]
    | DetailedLevel OpinionScore [OpinionReason]
    deriving (Show, Read, Eq)</code></pre>
<p>We’re going to be adding up these results quite a bit, so we define <code>SemiGroup</code> and <code>Monoid</code> instances for it. When two results are combined, scores are added together, lists of reasons are concatenated and the lowest confidence level is used. This is written as:</p>
<pre><code>instance Semigroup ReportResult where
    (FeelingLevel s1) &lt;&gt; (FeelingLevel s2) = FeelingLevel (s1 &lt;&gt; s2)
    (FeelingLevel s1) &lt;&gt; (ReasonsLevel s2 _) = FeelingLevel (s1 &lt;&gt; s2)
    (FeelingLevel s1) &lt;&gt; (DetailedLevel s2 _) = FeelingLevel (s1 &lt;&gt; s2)
    (ReasonsLevel s1 _) &lt;&gt; (FeelingLevel s2) = FeelingLevel (s1 &lt;&gt; s2)
    (ReasonsLevel s1 r1) &lt;&gt; (ReasonsLevel s2 r2) = ReasonsLevel (s1 &lt;&gt; s2) (r1 &lt;&gt; r2)
    (ReasonsLevel s1 r1) &lt;&gt; (DetailedLevel s2 r2) = ReasonsLevel (s1 &lt;&gt; s2) (r1 &lt;&gt; r2)
    (DetailedLevel s1 _) &lt;&gt; (FeelingLevel s2) = FeelingLevel (s1 &lt;&gt; s2)
    (DetailedLevel s1 r1) &lt;&gt; (ReasonsLevel s2 r2) = ReasonsLevel (s1 &lt;&gt; s2) (r1 &lt;&gt; r2)
    (DetailedLevel s1 r1) &lt;&gt; (DetailedLevel s2 r2) = DetailedLevel (s1 &lt;&gt; s2) (r1 &lt;&gt; r2)


instance Monoid ReportResult where
    mempty = DetailedLevel mempty mempty</code></pre>
<h2 id="opinion-based-on-traits">Opinion based on traits</h2>
<p>Current system compares two lists of traits. For example, two brave characters like each other slightly better than if one of them would be coward. Comparison is done by <code>traitPairOpinion</code> function, which definition I’m omitting as it’s rather long and not too interesting. It’s signature is: <code>traitPairOpinion :: TraitType -&gt; TraitType -&gt; Maybe (OpinionScore, OpinionReason)</code>. So, given two traits, tells how that pair affects to opinion and reasoning for it.</p>
<p>In order to have nicer format for out data, we introduce a helper function:</p>
<pre><code>traitPairScore :: TraitType -&gt; TraitType -&gt; (OpinionScore, [OpinionReason])
traitPairScore a b =
    case traitPairOpinion a b of
            Nothing -&gt;
                mempty

            Just (s, r) -&gt;
                (s, [r])</code></pre>
<p>This is because <code>(OpinionScore, OpinionReason)</code> isn’t monoid, but <code>(OpinionScore, [OpinionReason])</code> is, which means we can combine them with <code>&lt;&gt;</code>.</p>
<p>Actual score calculation based on traits, we do it like this:</p>
<pre><code>traitScore :: [TraitType] -&gt; [PersonIntel] -&gt; [TraitType] -&gt; [PersonIntel] -&gt; ReportResult
traitScore originatorTraits originatorIntel targetTraits targetIntel =
    if (Traits `elem` originatorIntel) &amp;&amp; (Traits `elem` targetIntel)
        then DetailedLevel score reasons
        else FeelingLevel score
    where
        (score, reasons) = mconcat $ traitPairScore &lt;$&gt; originatorTraits &lt;*&gt; targetTraits</code></pre>
<p>The interesting part is <code>mconcat $ traitPairScore &lt;$&gt; originatorTraits &lt;*&gt; targetTraits</code>. Function <code>traitPairScore</code> expects two <code>TraitType</code> values as parameters, but we’re calling it with two lists of such values. First step is to use <code>&lt;$&gt;</code> and list of values, which produces a list of partially applied functions. Second step is to use <code>&lt;*&gt;</code> to call each and every of those functions with values from second list. Result is a list of results that were obtained by calling <code>traitPairScore</code> with every combination of elements from two lists. Final step is to take this list of <code>ReportResult</code> values and combine them to single result with <code>mconcat</code>.</p>
<p>Finally, based on available intel, <code>ReportResult</code> of correct level is created.</p>
<h2 id="opinion-based-on-relations">Opinion based on relations</h2>
<p>Score based on relations is similar, but a bit convoluted (or rather, a lot more).</p>
<p>Intel here has two dimensions. One of them is relationship visibility (is it public, family relation or secret relation), another is level of detail: <code>BaseOpinionIntel</code>, <code>ReasonsForOpinions</code> and <code>DetailedOpinions</code>.</p>
<p><code>relationScore</code> is the entry point for calculation:</p>
<pre><code>relationScore :: [PersonIntel] -&gt; [Relation] -&gt; ReportResult
relationScore intel relations =
    mconcat $ (relReport oIntel score) &lt;$&gt; visibilities
    where
        score = mconcat $ (relationTypeScore . relationType) &lt;$&gt; relations
        visibilities = mkUniq $ relationVisibility &lt;$&gt; relations
        oIntel = mkUniq $ mapMaybe (\case
                                        Opinions x -&gt;
                                            Just x

                                        _ -&gt;
                                            Nothing)
                                   intel</code></pre>
<p>Code has to take into account of what level of intel we have about opinions and on what detail: <code>oIntel</code>. On the other hand, <code>visibilities</code> is unique relation visibilities that exists in relations in this particular case and <code>score</code> is computed based on relations.</p>
<p>Function <code>relReport</code> creates final report. It takes into account on what level of intel we have, by doing: <code>matching = safeHead $ reverse $ sort $ filter (\x -&gt; opinionIntelVisibility x == visibility) intel</code>. This finds highest level intel we have about this particular relationship visibility. Based on the highest level of available intel <code>ReportResult</code> is created with correct confidence level. Ie. if there’s no specific intel, we get <code>FeelingLevel</code> report. If there’s intel about why particular person has certain opinion, we get <code>ReasonsLevel</code> report. Whole definition of function is below:</p>
<pre><code>relReport :: [OpinionIntel]
    -&gt; (OpinionScore, [OpinionReason])
    -&gt; RelationVisibility
    -&gt; ReportResult
relReport intel (score, reasons) visibility =
    case matching of
        Nothing -&gt;
            FeelingLevel score

        Just (BaseOpinionIntel _) -&gt;
            FeelingLevel score

        Just (ReasonsForOpinions _) -&gt;
            ReasonsLevel score reasons

        Just (DetailedOpinions _) -&gt;
            DetailedLevel score reasons
    where
        matching = safeHead $ reverse $ sort $ filter (\x -&gt; opinionIntelVisibility x == visibility) intel</code></pre>
<h2 id="opinion-report">Opinion report</h2>
<p>To pull all this together, we combine results of these two functions. Based on given information, it’ll compute <code>traitsRep</code> and <code>relationsRep</code>. These two are combined with <code>&lt;&gt;</code> as explained earlier in episode:</p>
<ul>
<li>scores are summed up</li>
<li>reason lists are concatenated</li>
<li>confidence level is lowest of two</li>
</ul>
<pre><code>opinionReport :: [TraitType]
    -&gt; [PersonIntel]
    -&gt; [TraitType]
    -&gt; [PersonIntel]
    -&gt; [Relation]
    -&gt; OpinionReport
opinionReport originatorTraits originatorIntel targetTraits targetIntel targetRelations =
    reportResultToOpinionResult $ traitsRep &lt;&gt; relationsRep
    where
        traitsRep = traitScore originatorTraits originatorIntel targetTraits targetIntel
        relationsRep = relationScore originatorIntel targetRelations</code></pre>
<p>Finally <code>ReportResult</code> is transformed to <code>OpinionReport</code>, which can be sent to client.</p>
<p><code>OpinionReport</code> has three levels:</p>
<ul>
<li><code>BaseOpinionReport</code> only tells if feeling is positive, neutral or negative</li>
<li><code>OpinionReasonReport</code> has feeling and in addition to reasoning</li>
<li><code>DetailedOpinionReport</code> has exact (more or less) score and reasoning</li>
</ul>
<pre><code>data OpinionReport =
    BaseOpinionReport OpinionFeeling
    | OpinionReasonReport OpinionFeeling [OpinionReason]
    | DetailedOpinionReport OpinionScore [OpinionReason]
    deriving (Show, Read, Eq)</code></pre>
<p>Actual transformation is shown here:</p>
<pre><code>reportResultToOpinionResult :: ReportResult -&gt; OpinionReport
reportResultToOpinionResult (FeelingLevel score) =
    BaseOpinionReport $ scoreFeeling score

reportResultToOpinionResult (ReasonsLevel score reasons) =
    OpinionReasonReport (scoreFeeling score) reasons

reportResultToOpinionResult (DetailedLevel score reasons) =
    DetailedOpinionReport (clamp (-100) 100 score) reasons</code></pre>
<h2 id="note-about-incorrectness">Note about incorrectness</h2>
<p>Reports are based on intel and this might lead into incorrect results. In case of player’s own avatar, they have full intel (ie. they know all relations, all traits and so on.) Therefore opinion about some other person is based wholly on what we know about them.</p>
<p>But in case of gauging somebody else’s opinion about us or person A’s opinion of person B, when A or B isn’t us, there’s chance of misjudging things. We might not know everything about them, or we might know more about A than B knows about them. In short, opinion shown for player, is just best effort guess.</p>
<h2 id="in-closing">In closing</h2>
<p>Questions, comments and feedback is welcome. Even better is if you record your own HPR episode. Best way to reach me nowadays is by email or in fediverse, where I’m <code>Tuula@mastodon.social</code>.</p>
<p>ad astra!</p>
