import { Actor } from './actor.js'; import { Graph } from './graph.js'; import { Action } from './action.js'; import { CryptoUtil } from './crypto.js'; import params from '../params.js'; class Post extends Actor { constructor(forum, authorPublicKey, postContent) { const index = forum.posts.countVertices(); const name = `Post${index + 1}`; super(name, forum.scene); this.id = postContent.id ?? CryptoUtil.randomUUID(); this.authorPublicKey = authorPublicKey; this.value = 0; this.citations = postContent.citations; this.title = postContent.title; this.totalCitationWeight = this.citations.reduce((total, { weight }) => total += weight, 0); if (this.totalCitationWeight > params.revaluationLimit) { throw new Error('Post total citation weight exceeds revaluation limit ' + `(${this.totalCitationWeight} > ${params.revaluationLimit})`); } if (this.citations.some(({ weight }) => Math.abs(weight) > 1)) { throw new Error('Each citation weight must be in the range [-1, 1]'); } } } /** * Purpose: Maintain a directed, acyclic, weighted graph of posts referencing other posts */ export class Forum extends Actor { constructor(name, scene) { super(name, scene); this.posts = new Graph(scene); this.actions = { addPost: new Action('add post', scene), propagateValue: new Action('propagate value', this.scene), }; } async addPost(authorId, postContent) { const post = new Post(this, authorId, postContent); await this.actions.addPost.log(this, post); this.posts.addVertex(post.id, post, post.title); if (this.scene.flowchart) { this.scene.flowchart.log(`${post.id} -- value --> ${post.id}_value[0]`); } for (const { postId: citedPostId, weight } of post.citations) { this.posts.addEdge('citation', post.id, citedPostId, { weight }); if (this.scene.flowchart) { this.scene.flowchart.log(`${post.id} -- ${weight} --> ${citedPostId}`); } } return post.id; } getPost(postId) { return this.posts.getVertexData(postId); } getPosts() { return this.posts.getVerticesData(); } async setPostValue(postId, value) { const post = this.getPost(postId); post.value = value; await post.setValue('value', value); if (this.scene.flowchart) { this.scene.flowchart.log(`${post.id}_value[${value}]`); } } getPostValue(postId) { const post = this.getPost(postId); return post.value; } getTotalValue() { return this.getPosts().reduce((total, { value }) => total += value, 0); } async propagateValue(fromActor, postId, increment, depth = 0) { if (depth === 0 && this.scene.flowchart) { const randomId = CryptoUtil.randomUUID().replaceAll('-', '').slice(0, 8); this.scene.flowchart.log(`${postId}_initial_value_${randomId}[${increment}] -- initial value --> ${postId}`); } if (params.maxPropagationDepth >= 0 && depth > params.maxPropagationDepth) { return []; } const post = this.getPost(postId); this.actions.propagateValue.log(fromActor, post, `(increment: ${increment})`); const adjustedIncrement = increment * (1 - params.leachingValue * post.totalCitationWeight); const rewards = new Map(); const addReward = (id, value) => rewards.set(id, (rewards.get(id) ?? 0) + value); const addRewards = (r) => { for (const [id, value] of r) { addReward(id, value); } }; // Increment the value of the post // Apply leaching value const currentValue = this.getPostValue(postId); const newValue = currentValue + adjustedIncrement; await this.setPostValue(postId, newValue); // Award reputation to post author console.log('reward for post author', post.authorPublicKey, adjustedIncrement); addReward(post.authorPublicKey, adjustedIncrement); // Recursively distribute reputation to citations, according to weights for (const { postId: citedPostId, weight } of post.citations) { addRewards(await this.propagateValue(post, citedPostId, weight * increment, depth + 1)); } return rewards; } }