Skip to content

Commit 6f1e699

Browse files
committed
Add support for "No Ball" skill
- Tests - Also discovered a bug in throw teammate. It is no longer allowed to pickup ball when landing successfully on it .
1 parent 6bcdc5a commit 6f1e699

File tree

16 files changed

+397
-37
lines changed

16 files changed

+397
-37
lines changed

docs/bb2025/rules-faq-bb2025.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,17 @@ Since BB2025 lack any guidance on this, for now Jervis uses th BB2020 rule, i.e.
151151
a player with PA -, can start a pass action, but the pass will always be a
152152
fumble.
153153

154+
### Page 78 - Landing
155+
In BB2020, it was well-defined what happened if a player landed successfully on
156+
the ball (they were allowed to pick it up).
157+
158+
This wording has been removed from the rulebook in BB2025, and Pickup (page 57)
159+
indicates that any player moving outside their activation is counting as
160+
involuntary movement, thus it is no longer allowed.
161+
162+
It isn't explicitly spelled out this way, but that is the interpretation used
163+
by Jervis.
164+
154165
### Page 124 - Big Hand
155166
Using the NAF interpretation of Secure the Ball as a "pickup". This also means
156167
that Big Hand will work on Secure the Ball rolls.
@@ -169,6 +180,10 @@ deviate die (D6) and then choose to treat the roll as either a D6 or D3.
169180
In the rulebook, D3's are defined as Rolling a D6, taking half the value and
170181
round up.
171182

183+
### Page 132 - No Ball
184+
Technically, you are allowed to user other skills like Extra Arms, but since
185+
it would be pointless, Jervis just ignore these skills.
186+
172187
### Page 135 - Put the Boot In
173188
Technically, using the skill should be optional, but there doesn't seem to be
174189
any use case for this (even a bad one), so until such a use case surfaces, this

docs/bb2025/todo-base-rules-bb2025.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ book.
503503
- [x] Not turnover if landing on player from an opponent team
504504
- [x] No Turnover if a thrown player is knocked down and not holding the ball
505505
- [x] Turnover if the thrown player is knocked down and is holding the ball
506+
- [x] Successful landing on the ball trigger Bounce
506507
- [x] Land in the occupied square with prone player. Roll injury again
507508
- [x] Land in the end-zone with ball trigger touchdown.
508509
- [x] Land on a player with the ball. Ball gets knocked loose. It bounces immediately

docs/bb2025/todo-skills-bb2025.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,13 @@ a test class in `modules/jervis-engine/src/commonTest/kotlin/dk/ilios/jervis/bb2
212212
- [x] Cannot Pass
213213
- [x] Cannot Hand-off
214214
- [ ] Cannot Fumblerooskie
215-
- [ ] No Ball*
215+
- [x] No Ball*
216+
- [x] Catch rolls are automatic 1's
217+
- [x] Do not use Extra Arms on Catch
218+
- [x] Pickup rolls are automatic 1's
219+
- [x] Do not use Big Hand / Extra Arms on Pickup
220+
- [x] Secure the Ball rolls are automatic 1's
221+
- [x] Cannot intercept
216222
- [ ] Plague Ridden
217223
- [ ] Pogo Stick
218224
- [ ] Projectile Vomit

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/model/context/PickupRollContext.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.jervisffb.engine.model.context
22

3+
import com.jervisffb.engine.model.Ball
34
import com.jervisffb.engine.model.Player
45
import com.jervisffb.engine.model.modifiers.DiceModifier
56
import com.jervisffb.engine.rules.common.procedures.D6DieRoll
@@ -12,6 +13,7 @@ import com.jervisffb.engine.utils.sum
1213
*/
1314
data class PickupRollContext(
1415
val player: Player,
16+
val ball: Ball,
1517
val useBigHands: Boolean = false,
1618
val useExtraArms: Boolean = false,
1719
val modifiers: List<DiceModifier> = emptyList(),
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.jervisffb.engine.reports
2+
3+
import com.jervisffb.engine.model.Player
4+
5+
class ReportNoBallAffectingAction(
6+
player: Player,
7+
actionType: ActionType,
8+
) : LogEntry() {
9+
enum class ActionType {
10+
CATCH,
11+
PICKUP,
12+
SECURE_THE_BALL
13+
}
14+
override val category: LogCategory = LogCategory.GAME_PROGRESS
15+
override val message: String = buildString {
16+
when (actionType) {
17+
ActionType.CATCH -> append("${player.name} has No Ball so cannot catch the ball")
18+
ActionType.PICKUP -> append("${player.name} has No Ball so cannot pickup the ball")
19+
ActionType.SECURE_THE_BALL -> append("${player.name} has No Ball so cannot Secure the Ball")
20+
}
21+
}
22+
}

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/rules/bb2020/procedures/actions/throwteammate/ThrowPlayerStep.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,8 @@ object ThrowPlayerStep: Procedure() {
517517
object PickupBallAfterLanding: ParentNode() {
518518
override fun onEnterNode(state: Game, rules: Rules): Command {
519519
val throwContext = state.getContext<ThrowTeamMateContext>()
520-
val pickupContext = PickupRollContext(throwContext.thrownPlayer!!)
520+
val ball = state.field[throwContext.thrownPlayer!!.coordinates].balls.single()
521+
val pickupContext = PickupRollContext(throwContext.thrownPlayer, ball)
521522
return SetContext(pickupContext)
522523
}
523524
override fun getChildProcedure(state: Game, rules: Rules): Procedure = Pickup

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/rules/bb2025/BB2025SkillSettings.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.jervisffb.engine.rules.bb2025.skills.Loner
2525
import com.jervisffb.engine.rules.bb2025.skills.MightyBlow
2626
import com.jervisffb.engine.rules.bb2025.skills.MultipleBlock
2727
import com.jervisffb.engine.rules.bb2025.skills.MyBall
28+
import com.jervisffb.engine.rules.bb2025.skills.NoBall
2829
import com.jervisffb.engine.rules.bb2025.skills.Pass
2930
import com.jervisffb.engine.rules.bb2025.skills.PrehensileTail
3031
import com.jervisffb.engine.rules.bb2025.skills.Pro
@@ -541,7 +542,9 @@ class BB2025SkillSettings: SkillSettings() {
541542
}
542543
}
543544
SkillType.NO_BALL -> {
544-
// TODO
545+
addNoValueEntry("No Ball", type, SkillCategory.TRAITS) { player, category,expiresAt ->
546+
NoBall(player, category, expiresAt)
547+
}
545548
}
546549
SkillType.PICK_ME_UP -> {
547550
// TODO()

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/rules/bb2025/procedures/actions/pass/InterceptionStep.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,19 @@ object InterceptionStep: Procedure() {
117117
override fun actionOwner(state: Game, rules: Rules): Team = state.activePlayer!!.team.otherTeam()
118118
override fun getAvailableActions(state: Game, rules: Rules): List<GameActionDescriptor> {
119119
val context = state.getContext<InterceptionContext>()
120-
val candidates = getInterceptionCandidates(rules, context).filter {
121-
when (context.useCloudBurster) {
122-
false -> true // All players are eligible
123-
true -> it.isSkillAvailable(SkillType.VERY_LONG_LEGS) // Only Very Long Legged players are eligible
120+
val candidates = getInterceptionCandidates(rules, context)
121+
.filter { player ->
122+
// Check for CloudBurster and Very Long Legs combination
123+
when (context.useCloudBurster) {
124+
false -> true // All players are eligible
125+
true -> player.isSkillAvailable(SkillType.VERY_LONG_LEGS) // Only Very Long Legged players are eligible if Cloud Burster is used
126+
}
124127
}
125-
}
128+
.filterNot { player ->
129+
// Players with No Ball are not eligible for interception
130+
player.isSkillAvailable(SkillType.NO_BALL)
131+
}
132+
126133
return if (candidates.isNotEmpty()) {
127134
listOf(
128135
SelectPlayer.fromPlayers(candidates),

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/rules/bb2025/procedures/actions/securetheball/SecureTheBallStep.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import com.jervisffb.engine.model.context.getContext
2929
import com.jervisffb.engine.model.isSkillAvailable
3030
import com.jervisffb.engine.model.modifiers.DiceModifier
3131
import com.jervisffb.engine.model.modifiers.SecureTheBallModifier
32+
import com.jervisffb.engine.reports.ReportNoBallAffectingAction
3233
import com.jervisffb.engine.reports.ReportSecuredTheBallResult
3334
import com.jervisffb.engine.reports.ReportSkillUsed
3435
import com.jervisffb.engine.rules.Rules
@@ -50,7 +51,7 @@ import com.jervisffb.engine.rules.common.tables.Weather
5051
* separate, even though it means duplicating logic.
5152
*/
5253
object SecureTheBallStep: Procedure() {
53-
override val initialNode: Node = ChooseToUseBigHand
54+
override val initialNode: Node = CheckForNoBallSkill
5455
override fun onEnterProcedure(state: Game, rules: Rules): Command {
5556
val ball = state.currentBall()
5657
val securingPlayer = state.field[ball.location].player!!
@@ -71,6 +72,23 @@ object SecureTheBallStep: Procedure() {
7172
}
7273
}
7374

75+
object CheckForNoBallSkill: ComputationNode() {
76+
override fun apply(state: Game, rules: Rules): Command {
77+
val context = state.getContext<SecureTheBallRollContext>()
78+
val player = context.player
79+
val hasNoBall = player.isSkillAvailable(SkillType.NO_BALL)
80+
return if (hasNoBall) {
81+
compositeCommandOf(
82+
SetBallState.bouncing(context.ball),
83+
ReportNoBallAffectingAction(player, ReportNoBallAffectingAction.ActionType.SECURE_THE_BALL),
84+
GotoNode(SecuringTheBallFailed)
85+
)
86+
} else {
87+
GotoNode(ChooseToUseBigHand)
88+
}
89+
}
90+
}
91+
7492
object ChooseToUseBigHand: ActionNode() {
7593
override fun actionOwner(state: Game, rules: Rules): Team {
7694
return state.getContext<SecureTheBallRollContext>().player.team

modules/jervis-engine/src/commonMain/kotlin/com/jervisffb/engine/rules/bb2025/procedures/actions/throwteammate/ThrowPlayerStep.kt

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import com.jervisffb.engine.model.Team
3737
import com.jervisffb.engine.model.TurnOver
3838
import com.jervisffb.engine.model.context.LandingRollContext
3939
import com.jervisffb.engine.model.context.MovePlayerIntoSquareContext
40-
import com.jervisffb.engine.model.context.PickupRollContext
4140
import com.jervisffb.engine.model.context.ScoringATouchDownContext
4241
import com.jervisffb.engine.model.context.getContext
4342
import com.jervisffb.engine.model.modifiers.DiceModifier
@@ -460,23 +459,37 @@ object ThrowPlayerStep: Procedure() {
460459
playerHasBall && !isTurnOver -> GotoNode(CheckForScoring)
461460
playerHasBall && isTurnOver -> ExitProcedure()
462461
!playerHasBall && isTurnOver -> ExitProcedure()
463-
!playerHasBall && ballInSquare -> GotoNode(PickupBallAfterLanding)
462+
!playerHasBall && ballInSquare -> {
463+
// Ball in square always bounce when landing
464+
val ball = state.balls.first { it.state == BallState.ON_GROUND && it.location == context.target }
465+
compositeCommandOf(
466+
SetBallState.bouncing(ball),
467+
GotoNode(BounceBallOnLandingSquare)
468+
)
469+
}
464470
!playerHasBall && !ballInSquare -> ExitProcedure()
465471
else -> INVALID_GAME_STATE("Invalid state for landing player: hasBall[$playerHasBall], ballInSquare[$ballInSquare], turnOver[$isTurnOver]")
466472
}
467473
}
468474
}
469475

476+
// It is currently a bit unclear if it is allowed to pick up the ball when landing on it. RAW it seems "no", but it is
477+
// a change from BB2020 and the rulebook doesn't explicitly say so.
470478
object PickupBallAfterLanding: ParentNode() {
471479
override fun onEnterNode(state: Game, rules: Rules): Command {
472480
val throwContext = state.getContext<ThrowTeamMateContext>()
473-
val pickupContext = PickupRollContext(throwContext.thrownPlayer!!)
474-
return SetContext(pickupContext)
481+
val ball = state.field[throwContext.thrownPlayer!!.coordinates].balls.single()
482+
return compositeCommandOf(
483+
SetCurrentBall(ball),
484+
)
475485
}
476486
override fun getChildProcedure(state: Game, rules: Rules): Procedure = Pickup
477487
override fun onExitNode(state: Game, rules: Rules): Command {
478488
// All possible results are calculated inside Pickup, so just end here
479-
return ExitProcedure()
489+
return compositeCommandOf(
490+
SetCurrentBall(null),
491+
ExitProcedure()
492+
)
480493
}
481494
}
482495

@@ -529,7 +542,7 @@ object ThrowPlayerStep: Procedure() {
529542
isThrown = false,
530543
),
531544
state.balls.firstOrNull { it.state == BallState.ON_GROUND && it.location == throwContext.target }?.let {
532-
SetBallState.Companion.bouncing(it)
545+
SetBallState.bouncing(it)
533546
},
534547
SetPlayerState(thrownPlayer, PlayerState.KNOCKED_DOWN),
535548
SetContext(RiskingInjuryContext(thrownPlayer, mode = RiskingInjuryMode.BAD_LANDING))
@@ -565,7 +578,7 @@ object ThrowPlayerStep: Procedure() {
565578
add(RemoveContext<RiskingInjuryContext>())
566579
if (ball != null) {
567580
addAll(
568-
SetBallState.Companion.bouncing(ball),
581+
SetBallState.bouncing(ball),
569582
GotoNode(BounceBallOnLandingSquare)
570583
)
571584
} else {

0 commit comments

Comments
 (0)