garplab / typingtube

24 stars 1 forks source link

【要望】全ユーザーのタイピング実力ランキングがほしい #70

Open Toshi7878 opened 4 years ago

Toshi7878 commented 4 years ago

ダッシュボードの各グラフの推移から、ユーザーの実力ランキングが作れそうなので、可能であれば検討をお願いします

Chronopolize commented 4 years ago

プレー結果によるレーティングを検討しています。

難易度と得点は似たような計算します。一番重要なのは、どの箇所に重みを置くか。 平均速度は緩い部分が増えると表示値がさがるという問題がある。逆に緩い箇所が付け足さても難易度が下がらないシステムがほしい

ラインを最大速度から降順にソートして前の順ほどより重みを置く。 ※打はこの場面でkeystrokeを意味するだが、分かりやすいからリズムゲームに倣ってnoteで呼んでいます。

実はラインごとじゃなくてノートごとに重みをつけてる。これはラインの長さが変わっても重み付けが変わるのを避けるためです。

難易度の計算

各ノートを速い順から f(x)で重み付ける。 f(x) = 1/(0.1x+8) firefox_2020-06-11_19-48-33

長い曲の値が高くなりすぎないように最も速い1000ノートだけ使って集計する。 ところで1~400ノート目にかけて重みの傾斜が急過ぎるので1~400目のノートを均等に補正を与える(+0.036)。 firefox_2020-06-11_20-41-35

それから「ノート速度×重み」を集計してノーマライズする。

数行だけ速くなっている曲は: 最速×0.9 ~ 最速×1、全体同じ速さの曲は: 最速×1.15~1.25の難易度が出るようにしています。

Chronopolize commented 4 years ago

プレイが採点

難易度計算と同じ重みでプレイが採点される。 つまり速い行が重視されている。 1ラインのスコアは単純にクリア率x重みx速度。これを全行で集計して、100%クリアなら難易度と同じ値になる。見やすさのため、スコア値を10倍することも考えられる。

スコアによるレーティング計算

最後にプレイヤーレーティングを計算する。

プレイヤーの登録された最高記録を全部採点して、その中もっとも点が高い50つのプレイを順番に並べる。これで上から下がっていく重みをつけて集計する。このシステムはOsuというリズムゲームから取っている。(Osu以外リズムゲームに詳しくないので他で使っているかは分からない) firefox_2020-06-13_01-58-36 この方法でそれなりに精確な実力ラインキングを作れるのではないかと。

---

toshiさんが言ったよう平均速度のリーダーボードがよく他のタイピングサイトで見かけますね。 最近の平均速度を使うならゆるプレイやテクニカルな譜面をするとレーティングが下がることは問題にならないでしょうか。登録プレイの上何割を使うことと、曲ごとに速度のラインキングこともありえます。

上に説明したレーティングシステムは曲攻略向けです。簡単な曲で高速で出すより難しい曲を攻略するほうが面白いと思ったりします。そしてレーティングを伸ばせるためにはいろな曲をプレイしなければならない。自然にユーザー保持です(たぶん)。

長々と説明口調ですいません。質問や他の意見があればどうぞ。

garplab commented 4 years ago

ありがとうございます! すいません、なんとなくは読めるのですが、細かいところいろいろとよくわからなかったです。

細かい計算などのまえに、どのような結果を表示したいのか例を出してもらえるといいかもしれないです。また、その理由・目的もお願いしたいです。(正確性と速さをより正確に反映したいなど)

次に、計算方法を議論する場合はどれをどう計算するのかわかりやすく記載していただけると助かります。ノートとはなにか、ラインとは何を指すのかなど用語は曖昧だといけないので、並べて定義していただけると助かります。

など具体的に議論していただけると助かります。 やりたいことがわかればOKなので、計算については引用したり細かい部分は省略でも大丈夫です。

開発を進めるためには、ランキングシステムによる集客効果、開発コスト、運用コスト等を考慮して検討します。

Chronopolize commented 4 years ago

用語: ノート:譜面の中の一つの打 ライン:1行

1行の計算:

f(x) = 1/(0.1x +8) (重みづけ関数) g(x) = 0.1ln(10x+8)-10ln(8)(f(x)の積分) 補正:1から400番目に速いノートに+0.036 (数値はあとで調整できる)

この行の重み = g(40)-g(20) + 20*0.036

1曲の計算: 全行で(行の)重み✕速さを集計する。手に入れる値が難易度だ。

1曲の計算(採点):

全行で(行の)重み✕速さ ✕(行の)クリア率を集計する。 行のクリア率 = タイプした打数 / 行の打数 ; タイプしきったなら1.

レーティング計算 プレイヤーのもっとも点の高い50プレイを降順に並べて下がっていく重みづけで集計する。

集計の範囲(対象曲、期間、対象ユーザー、ランキングの複数投稿への処理)

ランキング結果の更新頻度 10分ごとにランキングを更新すればいい。各プレーヤーのレーティングは最高記録更新をした時計算し直せばいい。

どのような結果を表示するのか

garplab commented 4 years ago

ありがとうございます。 ちょっと正確に理解できてないかもしれませんが気づいたこと書きました。

あとは、この方式だとaaaaaaaaみたいな打ちやすく速度が出やすい文章が拾われやすくなりそうです。 また、得点が上がりやすい曲を探してプレイするみたいな攻略法が出てきて困りそうです。

Toshi7878 commented 4 years ago

ライン毎の速さを速度順に出力するコードを作成しました タイピングページでF12を押すと開かれるConsoleにコードをコピペし実行すると、1000打鍵までの速さ順が表示されます 配列の内容は[ライン番号,ライン打鍵数,ライン速度]です

まだ重み付けや補正の計算はできていないです


var line_length_1 = 0; //曲の合計ライン数
var line_speed_typing_ranking=[];//打鍵速度が早い順で[ライン番号,ライン打鍵数,ラインの必要打鍵速度]が入る(1000notesまで)

$(function () {

line_notes_roma=0; //ライン打鍵数
line_time=0; //入力があるラインの時間

//ラインを1行ずつ見る
$.each(typing_array, function(index, typing_line){

    if(lyrics_array[index][1]!='end' && typing_line != ''){ //ラインに入力する文字が存在したら処理

        //ラインの数・ライン時間を取得
        line_length_1++; //line_length_1を+1する。最終的に曲の合計ライン数になる

        line_time=lyrics_array[index+1][0]-lyrics_array[index][0]//ラインの長さを取得する

        //ライン打鍵数を取得
        line_notes_roma = typing_array_roma[index].join('').length

        //[indexはライン番号,ライン打鍵数,line_notes_roma/line_timeでライン打鍵速度]をline_speed_typing_rankingに格納
        //今の所は入力文字が存在するラインを全て格納させる
        line_speed_typing_ranking.push([index , line_notes_roma , Number((line_notes_roma/line_time).toFixed(3))])

    }else if(lyrics_array[index][1]=='end'){//全てのラインを取得したら処理

        //line_speed_typing_rankingを打鍵速度が速い順に降順でソート
        line_speed_typing_ranking.sort( function(a,b){return(b[2]-a[2]);} )

var sum = 0 //打鍵速度が速い順の合計打数

//line_speed_typing_rankingを上から1行ずつ見る
$.each(line_speed_typing_ranking, function(speed_index,speed_line){

    if(sum+ Number(line_speed_typing_ranking[speed_index][1])<=1000){//1000notesに達するまでnotesを足す

        sum =sum+ Number(line_speed_typing_ranking[speed_index][1]);

    }

    else if(sum+ Number(line_speed_typing_ranking[speed_index][1])>=1000){//1000打鍵に達した場合

        //sumに今度は「1000打鍵に達するまでのライン番号」を代入
        sum=speed_index

        return false
    }

})

//1000打鍵以降のラインをline_speed_typing_rankingから削除
line_speed_typing_ranking.splice(sum-line_speed_typing_ranking.length,9999)
console.log(line_speed_typing_ranking)
return false
      }
        });
      })
Toshi7878 commented 4 years ago

参考画像です 2020-6-14_20-39-17_No-00_NoName

EaOHEPfU0AUN8vG

Chronopolize commented 4 years ago

あと計算コスト上、複雑な計算は全部スコア登録時ではなく曲投稿・編成時に計算できます。行の重みづけは一度しか計算する必要がありません。

それから、聞きたかったけどTypingTubeはどのフレームワークで作られていますか?

Chronopolize commented 4 years ago

@Toshi1999 よく再現しました。参考のために計算コードを上げます。(計算以外のコードは省略しました。変えた定数もあります。)

private static final float DESCALING_FACTOR = 24.25292187f;
private static final float EXTRA_WEIGHT_PER_CHARACTER = 0.022f;// 0.036f;
private static final int EXTRA_WEIGHT_NUM_CHARACTERS = 400;

private static int CHAR_COUNT_TO_CONSIDER = 600; // consider the fastest N characters
public final static float X_COEFFICIENT = 0.15f; //  value of a in 1/(ax+b)

public float calculateDifficultyAndRecordInfo(SongInfo song) {
    var lines = song.getLinesByDifficulty();
    for (int i = 0; i < lines.size(); i++) {
        Line line = lines.get(i);

        float weight = calculateWeightOfLine(line, charsProcessed);
        float contribution = weight * line.getSpeed();

        totalDifficulty += contribution;
        charsProcessed += line.noteCount;

        // Record the line weight
        line.scoreWeight = weight / DESCALING_FACTOR;
    }
    float difficulty = totalDifficulty / DESCALING_FACTOR;
}
private static float calculateWeightOfLine(Line line, int charsProcessed) {
    int start = charsProcessed;
    int end = Math.min(charsProcessed + line.noteCount, CHAR_COUNT_TO_CONSIDER);
    if (start >= CHAR_COUNT_TO_CONSIDER) {
        return 0;
    }
    return defIntegralOfWeightFn(start, end) + calculateBonusWeightOfLine(start, end);
}

private static float calculateBonusWeightOfLine(int start, int end) {
    float bonusWeight = 0;
    if (start < EXTRA_WEIGHT_NUM_CHARACTERS) {
        int extraWeightedChars = Math.min(end, EXTRA_WEIGHT_NUM_CHARACTERS) - start;
        bonusWeight += extraWeightedChars * EXTRA_WEIGHT_PER_CHARACTER;
    }
    return bonusWeight;
}

private static float defIntegralOfWeightFn(int start, int end) {
    return weightFnIntegral(end) - weightFnIntegral(start);
}

private static float weightFnIntegral(int x) { //積分
    return (float) (1 / X_COEFFICIENT * Math.log(X_COEFFICIENT * x + 8) - 1 / X_COEFFICIENT * Math.log(8));
}
Chronopolize commented 4 years ago

スコア計算にもう二つの用事が考えられます。

遅いノート抜けへのペナルティー

現時点は遅いノートの抜けはスコアを影響しないという問題があります。遅いノートが難易度に影響が少ないのは望ましいんだが、遅いノートでも、抜けた時ちゃんとしたペナルティーを与えたい。とりあえず通常スコアと同じ重さのペナルティーを使用してみましょう。

行の最大ペナルティー(全字抜けた時の) =  採点の満点値 * 行の打数 / 曲の打数

これは、最も速い1000打を含むライン以外でも適用される。そのラインは得点にならないが、抜けが罰される。

ただし、必要以上に重みづけと増やしたくないので:

上記のペナルティー以上の重みづけを持ってるラインは:   最大ペナルティー=0 上記のペナルティー未満の重みづけ(0含め)を持つラインは: 最大ペナルティー = 行の重みづけ*速度 - 上記のペナルティー

詳細を理解する必要はありません。要はこれまでの採点にできるだけ影響せずに遅いラインの抜けを罰することです。

正確度の採点:

これまでの採点は速度に関するものでした。Accの採点について意見を聞きたいです。

適当に提案すると:

100% = 20% A ; 100%丁度ならさらに + 2.5% 99% = 15% A 97% = 10% A 95% = 7% A 90% = 0% Aとは速度の満点値。Accの得点は難易度に比例します。


採点手順の図を作ってみました。 http://go.bubbl.us/a72da8/40b8?/採点計算概要図