Skip to content

Commit

Permalink
Basic atcoder support
Browse files Browse the repository at this point in the history
  • Loading branch information
kunyavskiy committed Aug 26, 2023
1 parent 13058ec commit 0250f5e
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 16 deletions.
5 changes: 2 additions & 3 deletions src/cds/src/main/kotlin/org/icpclive/api/Scoreboard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ package org.icpclive.api

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.icpclive.util.DurationInMillisecondsSerializer
import org.icpclive.util.DurationInMinutesSerializer
import org.icpclive.util.*
import kotlin.time.Duration

@Serializable
Expand All @@ -26,7 +25,7 @@ data class ScoreboardRow(
val teamId: Int,
val rank: Int,
val totalScore: Double,
@Serializable(with = DurationInMinutesSerializer::class)
@Serializable(with = DurationInSecondsSerializer::class)
val penalty: Duration,
val lastAccepted: Long,
val medalType: String?,
Expand Down
140 changes: 140 additions & 0 deletions src/cds/src/main/kotlin/org/icpclive/cds/atcoder/AtcoderDataSource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package org.icpclive.cds.atcoder

import kotlinx.datetime.*
import kotlinx.serialization.Serializable
import org.icpclive.api.*
import org.icpclive.cds.common.*
import org.icpclive.cds.common.ContestParseResult
import org.icpclive.cds.common.FullReloadContestDataSource
import org.icpclive.cds.common.jsonLoader
import org.icpclive.cds.settings.AtcoderSettings
import org.icpclive.util.*
import kotlin.time.Duration
import kotlin.time.Duration.Companion.ZERO
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds

@Serializable
internal class AtcoderTask(
val Assignment: String,
val TaskName: String,
val TaskScreenName: String,
)

@Serializable
internal class AtcoderTaskResult(
val Count: Int,
val Failure: Int,
val Penalty: Int,
val Score: Int,
val Elapsed: Long,
val Pending: Boolean,
)

@Serializable
internal class AtcoderTeam(
val UserScreenName: String,
val TaskResults: Map<String, AtcoderTaskResult>
)

@Serializable
internal class ContestData(
val TaskInfo: List<AtcoderTask>,
val StandingsData: List<AtcoderTeam>
)

internal class AtcoderDataSource(val settings: AtcoderSettings) : FullReloadContestDataSource(5.seconds) {
val teamIds = Enumerator<String>()
val problemIds = Enumerator<String>()
private val loader = jsonLoader<ContestData>(settings.network, ClientAuth.CookieAuth("REVEL_SESSION", settings.sessionCookie)) { "https://atcoder.jp/contests/${settings.contestId}/standings/json" }

var submissionId: Int = 1
val runs = mutableMapOf<Pair<Int, Int>, List<RunInfo>>()

fun addNewRuns(teamId: Int, problemId: Int, result: AtcoderTaskResult) : List<RunInfo> {
val oldRuns = (runs[teamId to problemId] ?: emptyList()).toMutableList()
repeat(result.Count - oldRuns.size) {
oldRuns.add(
RunInfo(
id = submissionId++,
result = null,
percentage = 0.0,
problemId = problemId,
teamId = teamId,
time = minOf(settings.contestLength, Clock.System.now() - settings.startTime)
)
)
}
while (oldRuns.count { (it.result as? IOIRunResult)?.wrongVerdict != null } < result.Penalty) {
val fst = oldRuns.indexOfFirst { it.result == null }
oldRuns[fst] = oldRuns[fst].copy(result = IOIRunResult(score = listOf(0.0), wrongVerdict = Verdict.Rejected))
if (result.Elapsed.nanoseconds != ZERO && oldRuns[fst].time > result.Elapsed.nanoseconds) {
oldRuns[fst] = oldRuns[fst].copy(time = result.Elapsed.nanoseconds)
}
}
if (result.Score > 0) {
if (oldRuns.mapNotNull { it.result as? IOIRunResult }.maxOfOrNull { it.score[0] }?.toInt() != result.Score / 100 && !result.Pending) {
val fst = oldRuns.indexOfFirst { it.result == null }
oldRuns[fst] = oldRuns[fst].copy(result = IOIRunResult(score = listOf(result.Score / 100.0)), time = result.Elapsed.nanoseconds)
}
}
return oldRuns
}

override suspend fun loadOnce(): ContestParseResult {
val data = loader.load()
val problems = data.TaskInfo.mapIndexed { index, task ->
ProblemInfo(
letter = task.Assignment,
name = task.TaskName,
id = problemIds[task.TaskScreenName],
ordinal = index,
contestSystemId = task.TaskScreenName,
minScore = 0.0,
maxScore = (data.StandingsData.maxOfOrNull { it.TaskResults[task.TaskScreenName]?.Score ?: 0 } ?: 0) / 100.0,
scoreMergeMode = ScoreMergeMode.LAST_OK
)
}
val teams = data.StandingsData.map {
TeamInfo(
id = teamIds[it.UserScreenName],
fullName = it.UserScreenName,
displayName = it.UserScreenName,
contestSystemId = it.UserScreenName,
groups = emptyList(),
hashTag = null,
medias = emptyMap(),
isHidden = false,
isOutOfContest = false,
organizationId = null,
)
}
val info = ContestInfo(
name = "",
status = ContestStatus.byCurrentTime(settings.startTime, settings.contestLength),
resultType = ContestResultType.IOI,
startTime = settings.startTime,
contestLength = settings.contestLength,
freezeTime = settings.contestLength,
problemList = problems,
teamList = teams,
groupList = emptyList(),
organizationList = emptyList(),
penaltyRoundingMode = PenaltyRoundingMode.LAST,
penaltyPerWrongAttempt = 5.minutes,
)
val newRuns = buildList {
for (teamResult in data.StandingsData) {
val teamId = teamIds[teamResult.UserScreenName]
for ((problemCdsId, problemResult) in teamResult.TaskResults) {
val problemId = problemIds[problemCdsId]
runs[teamId to problemId] = addNewRuns(teamId, problemId, problemResult).also {
addAll(it)
}
}
}
}
return ContestParseResult(info, newRuns, emptyList())
}
}
11 changes: 10 additions & 1 deletion src/cds/src/main/kotlin/org/icpclive/cds/common/NetworkUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.request.*
import org.icpclive.cds.settings.NetworkSettings
import io.ktor.http.*
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager

Expand All @@ -16,6 +17,8 @@ internal sealed class ClientAuth {

class OAuth(val token: String) : ClientAuth()

class CookieAuth(val name: String, val value: String): ClientAuth()

companion object {
fun BasicOrNull(login: String?, password: String?) = if (login != null && password != null) {
Basic(login, password)
Expand All @@ -38,7 +41,13 @@ internal fun HttpClientConfig<*>.setupAuth(auth: ClientAuth) {

is ClientAuth.OAuth -> {
defaultRequest {
header("Authorization", "OAuth ${auth.token}")
header(HttpHeaders.Authorization, "OAuth ${auth.token}")
}
}

is ClientAuth.CookieAuth -> {
defaultRequest {
header(HttpHeaders.Cookie, "${auth.name}=${auth.value}")
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions src/cds/src/main/kotlin/org/icpclive/cds/settings/CDSSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import kotlinx.serialization.properties.decodeFromStringMap
import org.icpclive.api.ContestResultType
import org.icpclive.cds.ContestUpdate
import org.icpclive.cds.adapters.toEmulationFlow
import org.icpclive.cds.atcoder.AtcoderDataSource
import org.icpclive.cds.cats.CATSDataSource
import org.icpclive.cds.clics.ClicsDataSource
import org.icpclive.cds.clics.FeedVersion
Expand All @@ -27,6 +28,7 @@ import org.icpclive.cds.yandex.YandexDataSource
import org.icpclive.util.*
import java.io.InputStream
import java.nio.file.Path
import kotlin.time.Duration

// I'd like to have them in cds files, but then serializing would be much harder

Expand Down Expand Up @@ -222,6 +224,21 @@ class CodeDrillsSettings(
override fun toDataSource(creds: Map<String, String>) = CodeDrillsDataSource(this, creds)
}

@SerialName("atcoder")
@Serializable
class AtcoderSettings(
val contestId: String,
val sessionCookie: String,
@Serializable(with = HumanTimeSerializer::class)
val startTime: Instant,
@Serializable(with = DurationInSecondsSerializer::class)
@SerialName("contestLengthSeconds") val contestLength: Duration,
override val emulation: EmulationSettings? = null,
override val network: NetworkSettings? = null
) : CDSSettings() {
override fun toDataSource(creds: Map<String, String>) = AtcoderDataSource(this)
}

@OptIn(ExperimentalSerializationApi::class)
fun parseFileToCdsSettings(path: Path) : CDSSettings {
val file = path.toFile()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.icpclive.api.*
internal class IOIScoreboardCalculator : AbstractScoreboardCalculator() {
override val comparator: Comparator<ScoreboardRow> = compareBy(
{ -it.totalScore },
{ it.penalty }
)

override fun ContestInfo.getScoreboardRow(
Expand Down
15 changes: 15 additions & 0 deletions src/frontend/overlay/src/components/atoms/ContestCells.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ export const formatScore = (score, digits = 2) => {
return score?.toFixed((score - Math.floor(score)) > 0 ? digits : 0);
};

export const formatPenalty = (contestInfo, penalty) => {
if (penalty === undefined) {
return "";
}
let mode = contestInfo.penaltyRoundingMode;
if (mode === "sum_in_seconds" || mode === "last") {
return Math.floor(penalty / 60) + ":" + (penalty % 60 < 10 ? "0" : "") + (penalty % 60);
} else {
return Math.floor(penalty / 60);
}
};

export const needPenalty = (contestInfo) => contestInfo?.penaltyRoundingMode !== "zero";


export const ProblemCellWrap = styled(Cell)`
border-bottom: ${props => props.probColor} ${CELL_PROBLEM_LINE_WIDTH} solid;
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { SCOREBOARD_TYPES } from "../../../consts";
import { pushLog } from "../../../redux/debug";
import { Cell } from "../../atoms/Cell";
import { ProblemCell, RankCell, TextShrinkingCell } from "../../atoms/ContestCells";
import { formatPenalty, needPenalty, ProblemCell, RankCell, TextShrinkingCell } from "../../atoms/ContestCells";
import { StarIcon } from "../../atoms/Star";
import { formatScore } from "../../atoms/ContestCells";
import { ScoreboardIOITaskCell } from "../widgets/Scoreboard";
Expand Down Expand Up @@ -201,10 +201,10 @@ export const TeamInfo = ({ teamId }) => {
<ScoreboardStatCell>
{scoreboardData === null ? null : formatScore(scoreboardData?.totalScore, 1)}
</ScoreboardStatCell>
{contestInfo?.resultType !== "IOI" &&
<ScoreboardStatCell>
{scoreboardData?.penalty}
</ScoreboardStatCell>}
{needPenalty(contestInfo) &&
<ScoreboardStatCell>
{scoreboardData === null ? null : formatPenalty(contestInfo, scoreboardData.penalty)}
</ScoreboardStatCell>}

</TeamInfoWrapper>;
};
Expand Down
6 changes: 3 additions & 3 deletions src/frontend/overlay/src/components/organisms/widgets/PVP.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "../../../config";
import { SCOREBOARD_TYPES } from "../../../consts";
import { Cell } from "../../atoms/Cell";
import { formatScore, RankCell, TextShrinkingCell } from "../../atoms/ContestCells";
import { formatPenalty, formatScore, needPenalty, RankCell, TextShrinkingCell } from "../../atoms/ContestCells";
import { StarIcon } from "../../atoms/Star";
import { ScoreboardIOITaskCell } from "./Scoreboard";
import { TeamWebRTCProxyVideoWrapper, TeamWebRTCGrabberVideoWrapper } from "../holder/TeamViewHolder";
Expand Down Expand Up @@ -231,9 +231,9 @@ const TeamInfo = ({ teamId }) => {
<ScoreboardStatCell>
{scoreboardData === null ? null : formatScore(scoreboardData?.totalScore, 1)}
</ScoreboardStatCell>
{contestData.resultType !== "IOI" &&
{needPenalty(contestData) &&
<ScoreboardStatCell>
{scoreboardData?.penalty}
{scoreboardData === null ? null : formatPenalty(contestData, scoreboardData.penalty)}
</ScoreboardStatCell>}

</TeamInfoWrapper>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ import {
VERDICT_UNKNOWN
} from "../../../config";
import { Cell } from "../../atoms/Cell";
import { formatScore, ProblemCell, RankCell, TextShrinkingCell } from "../../atoms/ContestCells";
import {
formatPenalty,
formatScore,
needPenalty,
ProblemCell,
RankCell,
TextShrinkingCell
} from "../../atoms/ContestCells";
import { StarIcon } from "../../atoms/Star";


Expand Down Expand Up @@ -221,8 +228,8 @@ export const ScoreboardRow = ({ teamId, hideTasks, rankWidth, nameWidth, sumPenW
<ScoreboardStatCell width={sumPenWidth ?? SCOREBOARD_SUM_PEN_WIDTH}>
{scoreboardData === null ? null : formatScore(scoreboardData.totalScore)}
</ScoreboardStatCell>
{contestData?.resultType === "ICPC" && <ScoreboardStatCell width={sumPenWidth ?? SCOREBOARD_SUM_PEN_WIDTH}>
{scoreboardData?.penalty}
{needPenalty(contestData) && <ScoreboardStatCell width={sumPenWidth ?? SCOREBOARD_SUM_PEN_WIDTH}>
{scoreboardData === null ? null : formatPenalty(contestData, scoreboardData.penalty)}
</ScoreboardStatCell>}
{!hideTasks && scoreboardData?.problemResults.map((resultsData, i) =>
<RenderScoreboardTaskCell key={i} data={resultsData} minScore={contestData?.problems[i]?.minScore} maxScore={contestData?.problems[i]?.maxScore} />
Expand All @@ -244,7 +251,7 @@ const ScoreboardHeader = ({ problems, rowHeight, name }) => {
return <ScoreboardHeaderWrap rowHeight={rowHeight}>
<ScoreboardHeaderTitle color={color}>{nameTable[name]} STANDINGS</ScoreboardHeaderTitle>
<ScoreboardHeaderStatCell>&#931;</ScoreboardHeaderStatCell>
{contestInfo?.resultType === "ICPC" && <ScoreboardHeaderStatCell>PEN</ScoreboardHeaderStatCell>}
{needPenalty(contestInfo) && <ScoreboardHeaderStatCell>PEN</ScoreboardHeaderStatCell>}
{problems && problems.map((probData) =>
<ScoreboardHeaderProblemCell key={probData.name} probData={probData} canGrow={true} canShrink={true}
basis={"100%"}/>
Expand Down

0 comments on commit 0250f5e

Please sign in to comment.