#!/usr/bin/env ruby
# coding: utf-8
require 'em-websocket'
require 'json'
require 'csv'
require 'sqlite3'
PORT = 8814
# Interchange of Information is done in JSON form.
# (IN) {"cmd": Command, OtherArgs...}
# (OUT)
#
# Command is one of:
# team
# text
class UserDB
def initialize()
@db = SQLite3::Database.new("users.sq3")
### @db.results_as_hash = true
@expireskey = '+3 days'
@expiretmpkey = '+1 hour'
@textlist = IO.readlines("TEXT")
@from = 'postmaster@koeki-prj.org'
@rank = %w(必須 標準 目標 一流 超一流 頂点)
dbinit
end
def dbinit()
# "at" field is like: YYYY-MM-DDThh:mm:ss at finish time
@db.execute_batch(<<~EOF)
-- DROP TABLE score;
CREATE TABLE IF NOT EXISTS score(
user, at, text,
step INTEGER, score INTEGER, types INTEGER,
miss INTEGER, time INTEGER,
UNIQUE(user, at),
FOREIGN KEY(user) REFERENCES users(user)
ON DELETE CASCADE ON UPDATE CASCADE
);
EOF
end
def email(user)
r = @db.execute("SELECT email FROM users WHERE user = ?", user)
if r[0] && /.+@.+/ =~ r[0][0] then
r[0][0]
else
nil
end
end
def sendmail(rcpt, subj, body)
require 'nkf'
rcpt = rcpt.join(" ") if rcpt.is_a?(Array)
sj = NKF.nkf('-jM', subj)
body = NKF.nkf('-j', body)
open("| sendmail -f#{@from} #{rcpt}", "w") do |m|
m.puts(<<~EOF)
Subject: #{sj}
From: jsTrr Admin <#{@from}>
Mime-Version: 1.0
Content-type: text/plain; charset=iso-2022-jp
EOF
m.puts(body)
end
end
def genToken(user, email)
d1 = ("a".."z").to_a.sample
d2 = ("a".."z").to_a.sample
token = d1 + d2 + sprintf("%02d", rand(100))
sendmail(email, 'jsTrr: passcode',
"Your(%s) passcode = %s\n" % [user, token])
token
end
def genKey(n=50)
rand(10**n).to_s(36)
end
def genSkey(user)
# First, erase expired session keys
@db.execute("DELETE FROM skey WHERE expire < datetime('now','localtime')")
sKey = genKey
@db.execute("INSERT INTO skey VALUES(?, ?,
datetime('now', 'localtime', '#{@expireskey}'));", user, sKey)
sKey
end
def storeTmpKey(user, token)
# First, erase expired session keys
@db.execute("DELETE FROM tmpkey
WHERE expire < datetime('now','localtime')")
tmpkey = genKey
@db.execute("INSERT INTO tmpkey
VALUES(?, ?, ?, datetime('now', 'localtime', '#{@expiretmpkey}'));",
user, tmpkey, token)
tmpkey
end
def authTmpKey(user, tmpkey, token)
r = @db.execute("SELECT user FROM tmpkey
WHERE user=? AND tmpkey=? AND token=?", user, tmpkey, token)
p token
p r[0]
if r && r[0] && r[0].length==1
# Auth OK!
genSkey(user)
end
end
def updateSkey(user, skey)
@db.execute("UPDATE skey
SET expire=datetime('now', 'localtime', '#{@expireskey}')
WHERE user=? AND skey=?",
user, skey)
skey
end
def authKey(user, skey)
r = @db.execute("SELECT user FROM skey
WHERE user=? AND skey=?", user, skey)
if r && r[0] && r[0].length==1
# Auth OK!
updateSkey(user, skey)
end
end
def userScoreInfo(user)
rval = Hash.new
@db.execute("SELECT text, max(score), max(step), count(user) FROM score
WHERE user=? GROUP BY user, text;", user) do |row|
rval[row[0]] = {"highscore" => row[1].to_i, "step" => row[2].to_i,
"trial" => row[3].to_i}
end
if r=@db.execute("SELECT gecos FROM users WHERE user=?", user)
rval["gecos"] = r[0][0]
end
steptrial = Hash.new{|k, v| k[v]=Hash.new}
@db.execute("SELECT text, step, count(step) FROM score
WHERE user=? GROUP BY user, text, step;", user) do |row|
txt, step, trial = row
steptrial[txt][step] = trial
end
rval["steptrial"] = steptrial
p rval
rval
end
################### Trr STEP Calculation ###################
def msgBeginner(s)
if s <= 0
"0点というのは問題だわ。これからかなりの努力が必要よ。道のりは長いけど頑張りましょう。"
elsif s <40
"少なくとも英文字ぐらいは絶対覚えること。業界必須の100点に向けてこれから頑張りましょう。"
elsif s < 80
"キー配置も大分覚えたようだけどまだまだだわ。業界必須の100点に向けてこれから頑張りましょう。"
elsif s < 130
"基礎的な技術は身に付けているようだけどまだまだだわ。業界標準の200点に向けてこれから頑張りましょう。"
elsif s < 180
"なかなかの実力ね。でもスピードと正確さが少し足りないわ。業界標準の200点に向けてもう少し頑張りましょう。"
elsif s < 280
"なかなかやるわね。もう少し頑張れば業界目標の300点をきっと突破できるわ。"
elsif s < 380
"すごいわね。初めてでこれぐらい出せれば十分だわ。でも業界一流の400点に向けてもう少し頑張りましょう。"
elsif s < 480
"すっごい!こんな点を出す人は滅多にいないわよ。ひょっとしてプロではないかしら?"
else
"あまりにも超人的だわ。きっとギネスブックに載るわよ。"
end
end
def msgForRecordBreaker(s)
rank = (s/100).to_i
sxx = s%100
STDERR.printf("s=%d, rank=%d, sxx=%d\n", s, rank, sxx)
if s < 600
sprintf("業界%sの%d点%s。",
@rank[rank], rank*100+100,
sxx < 67 ? "目指して頑張って" : "までもうすぐよ")
else
"よくここまでやるわね。あなたの目標は一体何なの?"
end
end
def msgSpecialForRecordBreaker(s)
rank = (s/100).to_i
sprintf("ついに業界%sの%d点突破ね!これからは業界%sの%d点を目指して頑張りましょう。",
@rank[rank], rank*100+100,
@rank[rank+1], rank*100+200)
end
def msgSuccess(user, step, highscore, score)
diff = [highscore - score, 0].max
trial = 0
if r=@db.execute("SELECT count(score) FROM score
WHERE user=? AND step=? GROUP BY user, step",
user, step)
trial = r[0][0].to_i
end
STDERR.printf("Trial=%d\n", trial)
if (score-step*10) > 100
"あなたには簡単すぎたようね。"
else
m = %w(軽く突破したわね。 わりと簡単に突破したわね。
ちょっと手こずったようね。 だいぶ手こずったようね。
よく頑張ったわね。 随分苦労したようね。 苦しみ抜いたわね。)
m[[Math.log2(trial)-1, m.length-1].min]
end
end
def msgFail(diff, highscore, score, missratio)
if diff<10
"あとほんの少しだったのに...本当に惜しかったわね。"
elsif diff<20
"惜しかったわね。"
elsif diff<30
"その調子よ。"
elsif diff<40
"もう一息だわ。でも息抜きはだめよ。。"
elsif diff<50
"頑張ればきっとできるわ。"
elsif diff < 60
"努力あるのみよ。"
elsif diff < 100 || missratio > 30
if missratio > 60
"練習に練習を重ねなさい。"
elsif missratio > 40
"ミスが多過ぎるわ。初心に帰って一つ一つ慎重に打つ練習をしなさい。"
elsif missratio > 24
"ミスが多いわ。正確に打つ練習をしなさい。"
elsif missratio > 8
"練習に練習を重ねなさい。"
else
"正確に打ってるようだけどスピードが遅すぎるわ。速く打つ練習に励みなさい。"
end
elsif diff < 110
"「trrの道は一日にしてならず」"
elsif diff < 120
"「trrに王道なし」"
elsif diff < 130
"あらまぁ。%d点を出した人がたったの%d点なんていったいどうしたのよ。" \
% [highscore, score]
elsif diff < 140
"%d点はまぐれだったの?" % [highscore]
else
"あなたの実力ってこの程度だったのね。"
end
end
def evalStep(user, h) # h == Hash from client
# Return the Hash whose keys are "step", "message", "time"
highscore = step = 0
update = nil # Break Hiscore
pass = nil # Go to next step
beginner = nil # Beginner flag
time = h["finish"] - h["start"]
types = h["types"].to_i
miss = h["miss"].to_i
text = h["text"]
speed = (60*types/time).to_i
score = [(60*(types-miss)/time).to_i, 0].max
missratio = 1000*h["miss"].to_i/h["types"].to_i # 1/1000!!!
r = @db.execute("SELECT max(score) ms,max(step) mst
FROM score WHERE user=? AND text=?
GROUP BY user, text", user, text)
if r && r[0] && r[0][0]
highscore, step = r[0]
if score >= step*10 # Qualify current step
step += 1; pass = true
end
update = (score > highscore)
else
step = score/10; beginner = true
end
if time <= 1
return {"step" => h["step"],
"message" => "ごめんなさい。時間計るの忘れてたわ。"}
end
# p score
# user, at, text, step, score, types, miss, time
@db.execute(<<~EOF, user, text, step, score, types, miss, time)
REPLACE INTO score VALUES(
?, datetime('now', 'localtime'), ?, ?, ?, ?, ?, ?);
EOF
diff = step*10 - score
recbreaker = (score > highscore && highscore/100 != score/100)
msg = if score > 750
"そんなことでいいの? 恥を知りなさい。"
elsif beginner
msgBeginner(score)
elsif pass # STEP clear!
sprintf("ステップ%d突破%sおめでとう!\n%s",
step,
update ? "そして記録更新" : "",
recbreaker ?
msgSpecialForRecordBreaker(highscore)
: msgForRecordBreaker(highscore)
)
elsif update
"記録更新おめでとう!\n"+msgSuccess(user, step, highscore, score)
else
msgFail(diff, highscore, score, missratio)
end
rv = {"score" => score, "step" => step, "time" => time.round(1),
"speed" => speed, "message" => msg, "types"=>types,
"highscore" => [highscore, score].max}
if false
# Peek trial count of this level
r = @db.execute("SELECT count(*) FROM score
WHERE user=? AND text=? AND step=?
GROUP BY user, text, step;", user, text, step)
rv["strial"] = r[0][0].to_i
r = @db.execute("SELECT count(*) FROM score
WHERE user=? AND text=? GROUP by user, text", user, text)
rv["trial"] = r[0][0].to_i
else
rv["scoreinfo"] = userScoreInfo(user)
end
return rv
end
def ranking(text)
@db.execute(<<~EOF, text)
SELECT user, max(score), max(step), count(score),
cast(round(sum(time)/60) as INT), max(at)
FROM score WHERE text=? GROUP BY user;
EOF
end
end
db = UserDB.new
connections = {}
team = Hash.new{|h,k| h[k]=Hash.new}
broadcast = lambda{|hash|
printf("sending %s\n", hash)
connections.keys.each do |c|
c.send(JSON.generate(hash))
end
}
EM::WebSocket.start({:host => "0.0.0.0", :port => PORT}) do |ws_conn|
Thread.new {
while cmd = gets
case cmd.chop
when 's'
broadcast.call({start: Time.now.to_f})
end
end
}
ws_conn.onopen do
connections[ws_conn] = {}
ws_conn.send(JSON.generate({myid: connections.length}))
STDERR.printf("OP: %d clients\n", connections.length)
end
ws_conn.onclose do
connections.delete(ws_conn)
STDERR.printf("CL: %d clients\n", connections.length)
end
ws_conn.onmessage do |msg|
STDERR.printf("RECV: [%s]\n", msg)
begin
json = JSON.parse(msg)
rescue
return
end
userinfo = connections[ws_conn]
if userinfo["skey"] then
if json["team"] then
#team[json["team"]][json["name"]] ||= nil
userinfo = {"team" => json["team"], "name" => json["name"]}
elsif json["settext"]
# {"松" => {"name1"=>1, "name2" => 2}}
tm = userinfo["team"]
nm = userinfo["name"]
team[tm][nm] = json["text"]
connections.each do |k, v|
p v["team"]
next unless tm == v["team"] # Skip other team
next if ws_conn==k # Skip myself
k.send(JSON.generate({"disable" => json["text"]}))
end
userinfo["text"] = json["text"].to_i
si = {"scoreinfo" => db.userScoreInfo(userinfo["user"])}
ws_conn.send(JSON.generate(si))
elsif json["finish"]
user = userinfo["user"]
if user && user > ""
if false
## start = # json["start"].to_i
start = userinfo["start"]
## finish = json["finish"].to_i
finish = Time.now.to_f
types = json["types"].to_i
STDERR.printf("types=%d", types)
miss = json["miss"].to_i
text = json["text"]
time = finish-start
score = 60*(types - 10*miss)/time
end
json["start"] = userinfo["start"]
json["finish"] = Time.now.to_f
STDERR.printf("Start=%.2f, Finish=%.2f\n", json["start"], json["finish"])
eval = db.evalStep(user, json)
ws_conn.send(JSON.generate(eval))
userinfo.delete("typelist")
end
elsif json["types"]
unless userinfo["typelist"]
p "START!"
userinfo["start"] = Time.now.to_f
userinfo["typelist"] = Hash.new
end
userinfo["typelist"][json["types"]] = Time.now.to_f
elsif json["ranking"]
text = json["ranking"]
p rank=db.ranking(text)
ws_conn.send(JSON.generate({"ranking": db.ranking(text)}))
end
else ########## Before authenticated
if json["Login"]
user = json["Login"]
email = db.email(user)
token = db.genToken(user, email)
tmpkey = db.storeTmpKey(user, token)
j = JSON.generate({"email" => email, "tmpkey" => tmpkey, "user" => user})
p token
ws_conn.send(j)
elsif json["skey"]
user, skey = json["user"], json["skey"]
skey = db.authKey(user, json["skey"])
STDERR.printf("SKey=%s", skey)
userinfo["skey"] = skey # Cache skey
STDERR.printf("CACHE skey for user %s\n", user)
userinfo["user"] = user # Cache User
if skey
si = db.userScoreInfo(user)
STDERR.printf("scoreinfo = %s\n", si)
j = JSON.generate({"user"=>user, "skey"=>skey, "scoreinfo"=>si})
ws_conn.send(j) # Login success by skey
else
ws_conn.send(JSON.generate({"fail" => "nokey"}))
end
elsif json["Passcode"]
token = json["Passcode"]
tmpkey = json["tmpkey"]
user = json["user"]
authkey = db.authTmpKey(user, tmpkey, token)
STDERR.printf("authkey=%s", authkey)
if authkey
userinfo["skey"] = authkey # Cache skey
STDERR.printf("Cache skey for user %s\n", user)
userinfo["user"] = user # Cache User
si = db.userScoreInfo(user)
j = JSON.generate({"user"=>user, "skey"=>authkey, "scoreinfo"=>si})
ws_conn.send(j) # Login success by token+tmpkey
else
j = JSON.generate({"user" => user, "fail" => "fail"})
ws_conn.send(j) # Login failure by token
end
else
ws_conn.send("No such user")
ws_conn.close # Force guest out
end
# broadcast.call({"message" => msg})
end
end
end